mirror of
https://github.com/esphome/esphome.git
synced 2025-07-29 22:56:37 +00:00
Merge branch 'dev' into revert-9439-lib_compat_mode_fix
This commit is contained in:
commit
01ac30f210
@ -1 +1 @@
|
||||
90bb12a42dfe2a13378fb292fd67a5fd503b689bcec2be034be366758a5f69c6
|
||||
4df2fc55e977ba821978fac5f1e721ce2338e23647050b7005b4c801b1770739
|
||||
|
826
.github/workflows/auto-label-pr.yml
vendored
826
.github/workflows/auto-label-pr.yml
vendored
@ -11,51 +11,10 @@ permissions:
|
||||
contents: read
|
||||
|
||||
env:
|
||||
TARGET_PLATFORMS: |
|
||||
esp32
|
||||
esp8266
|
||||
rp2040
|
||||
libretiny
|
||||
bk72xx
|
||||
rtl87xx
|
||||
ln882x
|
||||
nrf52
|
||||
host
|
||||
PLATFORM_COMPONENTS: |
|
||||
alarm_control_panel
|
||||
audio_adc
|
||||
audio_dac
|
||||
binary_sensor
|
||||
button
|
||||
canbus
|
||||
climate
|
||||
cover
|
||||
datetime
|
||||
display
|
||||
event
|
||||
fan
|
||||
light
|
||||
lock
|
||||
media_player
|
||||
microphone
|
||||
number
|
||||
one_wire
|
||||
ota
|
||||
output
|
||||
packet_transport
|
||||
select
|
||||
sensor
|
||||
speaker
|
||||
stepper
|
||||
switch
|
||||
text
|
||||
text_sensor
|
||||
time
|
||||
touchscreen
|
||||
update
|
||||
valve
|
||||
SMALL_PR_THRESHOLD: 30
|
||||
MAX_LABELS: 15
|
||||
TOO_BIG_THRESHOLD: 1000
|
||||
COMPONENT_LABEL_THRESHOLD: 10
|
||||
|
||||
jobs:
|
||||
label:
|
||||
@ -65,24 +24,6 @@ jobs:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4.2.2
|
||||
|
||||
- name: Get changes
|
||||
id: changes
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
run: |
|
||||
# Get PR number
|
||||
pr_number="${{ github.event.pull_request.number }}"
|
||||
|
||||
# Get list of changed files using gh CLI
|
||||
files=$(gh pr diff $pr_number --name-only)
|
||||
echo "files<<EOF" >> $GITHUB_OUTPUT
|
||||
echo "$files" >> $GITHUB_OUTPUT
|
||||
echo "EOF" >> $GITHUB_OUTPUT
|
||||
|
||||
# Get file stats (additions + deletions) using gh CLI
|
||||
stats=$(gh pr view $pr_number --json files --jq '.files | map(.additions + .deletions) | add')
|
||||
echo "total_changes=${stats:-0}" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Generate a token
|
||||
id: generate-token
|
||||
uses: actions/create-github-app-token@v2
|
||||
@ -97,73 +38,466 @@ jobs:
|
||||
script: |
|
||||
const fs = require('fs');
|
||||
|
||||
// Constants
|
||||
const SMALL_PR_THRESHOLD = parseInt('${{ env.SMALL_PR_THRESHOLD }}');
|
||||
const MAX_LABELS = parseInt('${{ env.MAX_LABELS }}');
|
||||
const TOO_BIG_THRESHOLD = parseInt('${{ env.TOO_BIG_THRESHOLD }}');
|
||||
const COMPONENT_LABEL_THRESHOLD = parseInt('${{ env.COMPONENT_LABEL_THRESHOLD }}');
|
||||
const BOT_COMMENT_MARKER = '<!-- auto-label-pr-bot -->';
|
||||
const CODEOWNERS_MARKER = '<!-- codeowners-request -->';
|
||||
const TOO_BIG_MARKER = '<!-- too-big-request -->';
|
||||
|
||||
const MANAGED_LABELS = [
|
||||
'new-component',
|
||||
'new-platform',
|
||||
'new-target-platform',
|
||||
'merging-to-release',
|
||||
'merging-to-beta',
|
||||
'core',
|
||||
'small-pr',
|
||||
'dashboard',
|
||||
'github-actions',
|
||||
'by-code-owner',
|
||||
'has-tests',
|
||||
'needs-tests',
|
||||
'needs-docs',
|
||||
'needs-codeowners',
|
||||
'too-big',
|
||||
'labeller-recheck'
|
||||
];
|
||||
|
||||
const DOCS_PR_PATTERNS = [
|
||||
/https:\/\/github\.com\/esphome\/esphome-docs\/pull\/\d+/,
|
||||
/esphome\/esphome-docs#\d+/
|
||||
];
|
||||
|
||||
// Global state
|
||||
const { owner, repo } = context.repo;
|
||||
const pr_number = context.issue.number;
|
||||
|
||||
// Get current labels
|
||||
// Get current labels and PR data
|
||||
const { data: currentLabelsData } = await github.rest.issues.listLabelsOnIssue({
|
||||
owner,
|
||||
repo,
|
||||
issue_number: pr_number
|
||||
});
|
||||
const currentLabels = currentLabelsData.map(label => label.name);
|
||||
|
||||
// Define managed labels that this workflow controls
|
||||
const managedLabels = currentLabels.filter(label =>
|
||||
label.startsWith('component: ') ||
|
||||
[
|
||||
'new-component',
|
||||
'new-platform',
|
||||
'new-target-platform',
|
||||
'merging-to-release',
|
||||
'merging-to-beta',
|
||||
'core',
|
||||
'small-pr',
|
||||
'dashboard',
|
||||
'github-actions',
|
||||
'by-code-owner',
|
||||
'has-tests',
|
||||
'needs-tests',
|
||||
'needs-docs',
|
||||
'too-big',
|
||||
'labeller-recheck'
|
||||
].includes(label)
|
||||
label.startsWith('component: ') || MANAGED_LABELS.includes(label)
|
||||
);
|
||||
|
||||
// Check for mega-PR early - if present, skip most automatic labeling
|
||||
const isMegaPR = currentLabels.includes('mega-pr');
|
||||
|
||||
// Get all PR files with automatic pagination
|
||||
const prFiles = await github.paginate(
|
||||
github.rest.pulls.listFiles,
|
||||
{
|
||||
owner,
|
||||
repo,
|
||||
pull_number: pr_number
|
||||
}
|
||||
);
|
||||
|
||||
// Calculate data from PR files
|
||||
const changedFiles = prFiles.map(file => file.filename);
|
||||
const totalChanges = prFiles.reduce((sum, file) => sum + (file.additions || 0) + (file.deletions || 0), 0);
|
||||
|
||||
console.log('Current labels:', currentLabels.join(', '));
|
||||
console.log('Managed labels:', managedLabels.join(', '));
|
||||
|
||||
// Get changed files
|
||||
const changedFiles = `${{ steps.changes.outputs.files }}`.split('\n').filter(f => f.length > 0);
|
||||
const totalChanges = parseInt('${{ steps.changes.outputs.total_changes }}') || 0;
|
||||
|
||||
console.log('Changed files:', changedFiles.length);
|
||||
console.log('Total changes:', totalChanges);
|
||||
if (isMegaPR) {
|
||||
console.log('Mega-PR detected - applying limited labeling logic');
|
||||
}
|
||||
|
||||
const labels = new Set();
|
||||
// Fetch API data
|
||||
async function fetchApiData() {
|
||||
try {
|
||||
const response = await fetch('https://data.esphome.io/components.json');
|
||||
const componentsData = await response.json();
|
||||
return {
|
||||
targetPlatforms: componentsData.target_platforms || [],
|
||||
platformComponents: componentsData.platform_components || []
|
||||
};
|
||||
} catch (error) {
|
||||
console.log('Failed to fetch components data from API:', error.message);
|
||||
return { targetPlatforms: [], platformComponents: [] };
|
||||
}
|
||||
}
|
||||
|
||||
// Get environment variables
|
||||
const targetPlatforms = `${{ env.TARGET_PLATFORMS }}`.split('\n').filter(p => p.trim().length > 0).map(p => p.trim());
|
||||
const platformComponents = `${{ env.PLATFORM_COMPONENTS }}`.split('\n').filter(p => p.trim().length > 0).map(p => p.trim());
|
||||
const smallPrThreshold = parseInt('${{ env.SMALL_PR_THRESHOLD }}');
|
||||
const maxLabels = parseInt('${{ env.MAX_LABELS }}');
|
||||
// Strategy: Merge branch detection
|
||||
async function detectMergeBranch() {
|
||||
const labels = new Set();
|
||||
const baseRef = context.payload.pull_request.base.ref;
|
||||
|
||||
// Strategy: Merge to release or beta branch
|
||||
const baseRef = context.payload.pull_request.base.ref;
|
||||
if (baseRef !== 'dev') {
|
||||
if (baseRef === 'release') {
|
||||
labels.add('merging-to-release');
|
||||
} else if (baseRef === 'beta') {
|
||||
labels.add('merging-to-beta');
|
||||
}
|
||||
|
||||
// When targeting non-dev branches, only use merge warning labels
|
||||
const finalLabels = Array.from(labels);
|
||||
return labels;
|
||||
}
|
||||
|
||||
// Strategy: Component and platform labeling
|
||||
async function detectComponentPlatforms(apiData) {
|
||||
const labels = new Set();
|
||||
const componentRegex = /^esphome\/components\/([^\/]+)\//;
|
||||
const targetPlatformRegex = new RegExp(`^esphome\/components\/(${apiData.targetPlatforms.join('|')})/`);
|
||||
|
||||
for (const file of changedFiles) {
|
||||
const componentMatch = file.match(componentRegex);
|
||||
if (componentMatch) {
|
||||
labels.add(`component: ${componentMatch[1]}`);
|
||||
}
|
||||
|
||||
const platformMatch = file.match(targetPlatformRegex);
|
||||
if (platformMatch) {
|
||||
labels.add(`platform: ${platformMatch[1]}`);
|
||||
}
|
||||
}
|
||||
|
||||
return labels;
|
||||
}
|
||||
|
||||
// Strategy: New component detection
|
||||
async function detectNewComponents() {
|
||||
const labels = new Set();
|
||||
const addedFiles = prFiles.filter(file => file.status === 'added').map(file => file.filename);
|
||||
|
||||
for (const file of addedFiles) {
|
||||
const componentMatch = file.match(/^esphome\/components\/([^\/]+)\/__init__\.py$/);
|
||||
if (componentMatch) {
|
||||
try {
|
||||
const content = fs.readFileSync(file, 'utf8');
|
||||
if (content.includes('IS_TARGET_PLATFORM = True')) {
|
||||
labels.add('new-target-platform');
|
||||
}
|
||||
} catch (error) {
|
||||
console.log(`Failed to read content of ${file}:`, error.message);
|
||||
}
|
||||
labels.add('new-component');
|
||||
}
|
||||
}
|
||||
|
||||
return labels;
|
||||
}
|
||||
|
||||
// Strategy: New platform detection
|
||||
async function detectNewPlatforms(apiData) {
|
||||
const labels = new Set();
|
||||
const addedFiles = prFiles.filter(file => file.status === 'added').map(file => file.filename);
|
||||
|
||||
for (const file of addedFiles) {
|
||||
const platformFileMatch = file.match(/^esphome\/components\/([^\/]+)\/([^\/]+)\.py$/);
|
||||
if (platformFileMatch) {
|
||||
const [, component, platform] = platformFileMatch;
|
||||
if (apiData.platformComponents.includes(platform)) {
|
||||
labels.add('new-platform');
|
||||
}
|
||||
}
|
||||
|
||||
const platformDirMatch = file.match(/^esphome\/components\/([^\/]+)\/([^\/]+)\/__init__\.py$/);
|
||||
if (platformDirMatch) {
|
||||
const [, component, platform] = platformDirMatch;
|
||||
if (apiData.platformComponents.includes(platform)) {
|
||||
labels.add('new-platform');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return labels;
|
||||
}
|
||||
|
||||
// Strategy: Core files detection
|
||||
async function detectCoreChanges() {
|
||||
const labels = new Set();
|
||||
const coreFiles = changedFiles.filter(file =>
|
||||
file.startsWith('esphome/core/') ||
|
||||
(file.startsWith('esphome/') && file.split('/').length === 2)
|
||||
);
|
||||
|
||||
if (coreFiles.length > 0) {
|
||||
labels.add('core');
|
||||
}
|
||||
|
||||
return labels;
|
||||
}
|
||||
|
||||
// Strategy: PR size detection
|
||||
async function detectPRSize() {
|
||||
const labels = new Set();
|
||||
const testChanges = prFiles
|
||||
.filter(file => file.filename.startsWith('tests/'))
|
||||
.reduce((sum, file) => sum + (file.additions || 0) + (file.deletions || 0), 0);
|
||||
|
||||
const nonTestChanges = totalChanges - testChanges;
|
||||
|
||||
if (totalChanges <= SMALL_PR_THRESHOLD) {
|
||||
labels.add('small-pr');
|
||||
}
|
||||
|
||||
// Don't add too-big if mega-pr label is already present
|
||||
if (nonTestChanges > TOO_BIG_THRESHOLD && !isMegaPR) {
|
||||
labels.add('too-big');
|
||||
}
|
||||
|
||||
return labels;
|
||||
}
|
||||
|
||||
// Strategy: Dashboard changes
|
||||
async function detectDashboardChanges() {
|
||||
const labels = new Set();
|
||||
const dashboardFiles = changedFiles.filter(file =>
|
||||
file.startsWith('esphome/dashboard/') ||
|
||||
file.startsWith('esphome/components/dashboard_import/')
|
||||
);
|
||||
|
||||
if (dashboardFiles.length > 0) {
|
||||
labels.add('dashboard');
|
||||
}
|
||||
|
||||
return labels;
|
||||
}
|
||||
|
||||
// Strategy: GitHub Actions changes
|
||||
async function detectGitHubActionsChanges() {
|
||||
const labels = new Set();
|
||||
const githubActionsFiles = changedFiles.filter(file =>
|
||||
file.startsWith('.github/workflows/')
|
||||
);
|
||||
|
||||
if (githubActionsFiles.length > 0) {
|
||||
labels.add('github-actions');
|
||||
}
|
||||
|
||||
return labels;
|
||||
}
|
||||
|
||||
// Strategy: Code owner detection
|
||||
async function detectCodeOwner() {
|
||||
const labels = new Set();
|
||||
|
||||
try {
|
||||
const { data: codeownersFile } = await github.rest.repos.getContent({
|
||||
owner,
|
||||
repo,
|
||||
path: 'CODEOWNERS',
|
||||
});
|
||||
|
||||
const codeownersContent = Buffer.from(codeownersFile.content, 'base64').toString('utf8');
|
||||
const prAuthor = context.payload.pull_request.user.login;
|
||||
|
||||
const codeownersLines = codeownersContent.split('\n')
|
||||
.map(line => line.trim())
|
||||
.filter(line => line && !line.startsWith('#'));
|
||||
|
||||
const codeownersRegexes = codeownersLines.map(line => {
|
||||
const parts = line.split(/\s+/);
|
||||
const pattern = parts[0];
|
||||
const owners = parts.slice(1);
|
||||
|
||||
let regex;
|
||||
if (pattern.endsWith('*')) {
|
||||
const dir = pattern.slice(0, -1);
|
||||
regex = new RegExp(`^${dir.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}`);
|
||||
} else if (pattern.includes('*')) {
|
||||
// First escape all regex special chars except *, then replace * with .*
|
||||
const regexPattern = pattern
|
||||
.replace(/[.+?^${}()|[\]\\]/g, '\\$&')
|
||||
.replace(/\*/g, '.*');
|
||||
regex = new RegExp(`^${regexPattern}$`);
|
||||
} else {
|
||||
regex = new RegExp(`^${pattern.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}$`);
|
||||
}
|
||||
|
||||
return { regex, owners };
|
||||
});
|
||||
|
||||
for (const file of changedFiles) {
|
||||
for (const { regex, owners } of codeownersRegexes) {
|
||||
if (regex.test(file) && owners.some(owner => owner === `@${prAuthor}`)) {
|
||||
labels.add('by-code-owner');
|
||||
return labels;
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.log('Failed to read or parse CODEOWNERS file:', error.message);
|
||||
}
|
||||
|
||||
return labels;
|
||||
}
|
||||
|
||||
// Strategy: Test detection
|
||||
async function detectTests() {
|
||||
const labels = new Set();
|
||||
const testFiles = changedFiles.filter(file => file.startsWith('tests/'));
|
||||
|
||||
if (testFiles.length > 0) {
|
||||
labels.add('has-tests');
|
||||
}
|
||||
|
||||
return labels;
|
||||
}
|
||||
|
||||
// Strategy: Requirements detection
|
||||
async function detectRequirements(allLabels) {
|
||||
const labels = new Set();
|
||||
|
||||
// Check for missing tests
|
||||
if ((allLabels.has('new-component') || allLabels.has('new-platform')) && !allLabels.has('has-tests')) {
|
||||
labels.add('needs-tests');
|
||||
}
|
||||
|
||||
// Check for missing docs
|
||||
if (allLabels.has('new-component') || allLabels.has('new-platform')) {
|
||||
const prBody = context.payload.pull_request.body || '';
|
||||
const hasDocsLink = DOCS_PR_PATTERNS.some(pattern => pattern.test(prBody));
|
||||
|
||||
if (!hasDocsLink) {
|
||||
labels.add('needs-docs');
|
||||
}
|
||||
}
|
||||
|
||||
// Check for missing CODEOWNERS
|
||||
if (allLabels.has('new-component')) {
|
||||
const codeownersModified = prFiles.some(file =>
|
||||
file.filename === 'CODEOWNERS' &&
|
||||
(file.status === 'modified' || file.status === 'added') &&
|
||||
(file.additions || 0) > 0
|
||||
);
|
||||
|
||||
if (!codeownersModified) {
|
||||
labels.add('needs-codeowners');
|
||||
}
|
||||
}
|
||||
|
||||
return labels;
|
||||
}
|
||||
|
||||
// Generate review messages
|
||||
function generateReviewMessages(finalLabels) {
|
||||
const messages = [];
|
||||
const prAuthor = context.payload.pull_request.user.login;
|
||||
|
||||
// Too big message
|
||||
if (finalLabels.includes('too-big')) {
|
||||
const testChanges = prFiles
|
||||
.filter(file => file.filename.startsWith('tests/'))
|
||||
.reduce((sum, file) => sum + (file.additions || 0) + (file.deletions || 0), 0);
|
||||
const nonTestChanges = totalChanges - testChanges;
|
||||
|
||||
const tooManyLabels = finalLabels.length > MAX_LABELS;
|
||||
const tooManyChanges = nonTestChanges > TOO_BIG_THRESHOLD;
|
||||
|
||||
let message = `${TOO_BIG_MARKER}\n### 📦 Pull Request Size\n\n`;
|
||||
|
||||
if (tooManyLabels && tooManyChanges) {
|
||||
message += `This PR is too large with ${nonTestChanges} line changes (excluding tests) and affects ${finalLabels.length} different components/areas.`;
|
||||
} else if (tooManyLabels) {
|
||||
message += `This PR affects ${finalLabels.length} different components/areas.`;
|
||||
} else {
|
||||
message += `This PR is too large with ${nonTestChanges} line changes (excluding tests).`;
|
||||
}
|
||||
|
||||
message += ` Please consider breaking it down into smaller, focused PRs to make review easier and reduce the risk of conflicts.\n\n`;
|
||||
message += `For guidance on breaking down large PRs, see: https://developers.esphome.io/contributing/submitting-your-work/#how-to-approach-large-submissions`;
|
||||
|
||||
messages.push(message);
|
||||
}
|
||||
|
||||
// CODEOWNERS message
|
||||
if (finalLabels.includes('needs-codeowners')) {
|
||||
const message = `${CODEOWNERS_MARKER}\n### 👥 Code Ownership\n\n` +
|
||||
`Hey there @${prAuthor},\n` +
|
||||
`Thanks for submitting this pull request! Can you add yourself as a codeowner for this integration? ` +
|
||||
`This way we can notify you if a bug report for this integration is reported.\n\n` +
|
||||
`In \`__init__.py\` of the integration, please add:\n\n` +
|
||||
`\`\`\`python\nCODEOWNERS = ["@${prAuthor}"]\n\`\`\`\n\n` +
|
||||
`And run \`script/build_codeowners.py\``;
|
||||
|
||||
messages.push(message);
|
||||
}
|
||||
|
||||
return messages;
|
||||
}
|
||||
|
||||
// Handle reviews
|
||||
async function handleReviews(finalLabels) {
|
||||
const reviewMessages = generateReviewMessages(finalLabels);
|
||||
const hasReviewableLabels = finalLabels.some(label =>
|
||||
['too-big', 'needs-codeowners'].includes(label)
|
||||
);
|
||||
|
||||
const { data: reviews } = await github.rest.pulls.listReviews({
|
||||
owner,
|
||||
repo,
|
||||
pull_number: pr_number
|
||||
});
|
||||
|
||||
const botReviews = reviews.filter(review =>
|
||||
review.user.type === 'Bot' &&
|
||||
review.state === 'CHANGES_REQUESTED' &&
|
||||
review.body && review.body.includes(BOT_COMMENT_MARKER)
|
||||
);
|
||||
|
||||
if (hasReviewableLabels) {
|
||||
const reviewBody = `${BOT_COMMENT_MARKER}\n\n${reviewMessages.join('\n\n---\n\n')}`;
|
||||
|
||||
if (botReviews.length > 0) {
|
||||
// Update existing review
|
||||
await github.rest.pulls.updateReview({
|
||||
owner,
|
||||
repo,
|
||||
pull_number: pr_number,
|
||||
review_id: botReviews[0].id,
|
||||
body: reviewBody
|
||||
});
|
||||
console.log('Updated existing bot review');
|
||||
} else {
|
||||
// Create new review
|
||||
await github.rest.pulls.createReview({
|
||||
owner,
|
||||
repo,
|
||||
pull_number: pr_number,
|
||||
body: reviewBody,
|
||||
event: 'REQUEST_CHANGES'
|
||||
});
|
||||
console.log('Created new bot review');
|
||||
}
|
||||
} else if (botReviews.length > 0) {
|
||||
// Dismiss existing reviews
|
||||
for (const review of botReviews) {
|
||||
try {
|
||||
await github.rest.pulls.dismissReview({
|
||||
owner,
|
||||
repo,
|
||||
pull_number: pr_number,
|
||||
review_id: review.id,
|
||||
message: 'Review dismissed: All requirements have been met'
|
||||
});
|
||||
console.log(`Dismissed bot review ${review.id}`);
|
||||
} catch (error) {
|
||||
console.log(`Failed to dismiss review ${review.id}:`, error.message);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Main execution
|
||||
const apiData = await fetchApiData();
|
||||
const baseRef = context.payload.pull_request.base.ref;
|
||||
|
||||
// Early exit for non-dev branches
|
||||
if (baseRef !== 'dev') {
|
||||
const branchLabels = await detectMergeBranch();
|
||||
const finalLabels = Array.from(branchLabels);
|
||||
|
||||
console.log('Computed labels (merge branch only):', finalLabels.join(', '));
|
||||
|
||||
// Add new labels
|
||||
// Apply labels
|
||||
if (finalLabels.length > 0) {
|
||||
console.log(`Adding labels: ${finalLabels.join(', ')}`);
|
||||
await github.rest.issues.addLabels({
|
||||
owner,
|
||||
repo,
|
||||
@ -172,13 +506,9 @@ jobs:
|
||||
});
|
||||
}
|
||||
|
||||
// Remove old managed labels that are no longer needed
|
||||
const labelsToRemove = managedLabels.filter(label =>
|
||||
!finalLabels.includes(label)
|
||||
);
|
||||
|
||||
// Remove old managed labels
|
||||
const labelsToRemove = managedLabels.filter(label => !finalLabels.includes(label));
|
||||
for (const label of labelsToRemove) {
|
||||
console.log(`Removing label: ${label}`);
|
||||
try {
|
||||
await github.rest.issues.removeLabel({
|
||||
owner,
|
||||
@ -191,235 +521,78 @@ jobs:
|
||||
}
|
||||
}
|
||||
|
||||
return; // Exit early, don't process other strategies
|
||||
return;
|
||||
}
|
||||
|
||||
// Strategy: Component and Platform labeling
|
||||
const componentRegex = /^esphome\/components\/([^\/]+)\//;
|
||||
const targetPlatformRegex = new RegExp(`^esphome\/components\/(${targetPlatforms.join('|')})/`);
|
||||
// Run all strategies
|
||||
const [
|
||||
branchLabels,
|
||||
componentLabels,
|
||||
newComponentLabels,
|
||||
newPlatformLabels,
|
||||
coreLabels,
|
||||
sizeLabels,
|
||||
dashboardLabels,
|
||||
actionsLabels,
|
||||
codeOwnerLabels,
|
||||
testLabels
|
||||
] = await Promise.all([
|
||||
detectMergeBranch(),
|
||||
detectComponentPlatforms(apiData),
|
||||
detectNewComponents(),
|
||||
detectNewPlatforms(apiData),
|
||||
detectCoreChanges(),
|
||||
detectPRSize(),
|
||||
detectDashboardChanges(),
|
||||
detectGitHubActionsChanges(),
|
||||
detectCodeOwner(),
|
||||
detectTests()
|
||||
]);
|
||||
|
||||
for (const file of changedFiles) {
|
||||
// Check for component changes
|
||||
const componentMatch = file.match(componentRegex);
|
||||
if (componentMatch) {
|
||||
const component = componentMatch[1];
|
||||
labels.add(`component: ${component}`);
|
||||
}
|
||||
// Combine all labels
|
||||
const allLabels = new Set([
|
||||
...branchLabels,
|
||||
...componentLabels,
|
||||
...newComponentLabels,
|
||||
...newPlatformLabels,
|
||||
...coreLabels,
|
||||
...sizeLabels,
|
||||
...dashboardLabels,
|
||||
...actionsLabels,
|
||||
...codeOwnerLabels,
|
||||
...testLabels
|
||||
]);
|
||||
|
||||
// Check for target platform changes
|
||||
const platformMatch = file.match(targetPlatformRegex);
|
||||
if (platformMatch) {
|
||||
const targetPlatform = platformMatch[1];
|
||||
labels.add(`platform: ${targetPlatform}`);
|
||||
// Detect requirements based on all other labels
|
||||
const requirementLabels = await detectRequirements(allLabels);
|
||||
for (const label of requirementLabels) {
|
||||
allLabels.add(label);
|
||||
}
|
||||
|
||||
let finalLabels = Array.from(allLabels);
|
||||
|
||||
// For mega-PRs, exclude component labels if there are too many
|
||||
if (isMegaPR) {
|
||||
const componentLabels = finalLabels.filter(label => label.startsWith('component: '));
|
||||
if (componentLabels.length > COMPONENT_LABEL_THRESHOLD) {
|
||||
finalLabels = finalLabels.filter(label => !label.startsWith('component: '));
|
||||
console.log(`Mega-PR detected - excluding ${componentLabels.length} component labels (threshold: ${COMPONENT_LABEL_THRESHOLD})`);
|
||||
}
|
||||
}
|
||||
|
||||
// Get PR files for new component/platform detection
|
||||
const { data: prFiles } = await github.rest.pulls.listFiles({
|
||||
owner,
|
||||
repo,
|
||||
pull_number: pr_number
|
||||
});
|
||||
// Handle too many labels (only for non-mega PRs)
|
||||
const tooManyLabels = finalLabels.length > MAX_LABELS;
|
||||
|
||||
const addedFiles = prFiles.filter(file => file.status === 'added').map(file => file.filename);
|
||||
|
||||
// Strategy: New Component detection
|
||||
for (const file of addedFiles) {
|
||||
// Check for new component files: esphome/components/{component}/__init__.py
|
||||
const componentMatch = file.match(/^esphome\/components\/([^\/]+)\/__init__\.py$/);
|
||||
if (componentMatch) {
|
||||
try {
|
||||
// Read the content directly from the filesystem since we have it checked out
|
||||
const content = fs.readFileSync(file, 'utf8');
|
||||
|
||||
// Strategy: New Target Platform detection
|
||||
if (content.includes('IS_TARGET_PLATFORM = True')) {
|
||||
labels.add('new-target-platform');
|
||||
}
|
||||
labels.add('new-component');
|
||||
} catch (error) {
|
||||
console.log(`Failed to read content of ${file}:`, error.message);
|
||||
// Fallback: assume it's a new component if we can't read the content
|
||||
labels.add('new-component');
|
||||
}
|
||||
}
|
||||
if (tooManyLabels && !isMegaPR && !finalLabels.includes('too-big')) {
|
||||
finalLabels = ['too-big'];
|
||||
}
|
||||
|
||||
// Strategy: New Platform detection
|
||||
for (const file of addedFiles) {
|
||||
// Check for new platform files: esphome/components/{component}/{platform}.py
|
||||
const platformFileMatch = file.match(/^esphome\/components\/([^\/]+)\/([^\/]+)\.py$/);
|
||||
if (platformFileMatch) {
|
||||
const [, component, platform] = platformFileMatch;
|
||||
if (platformComponents.includes(platform)) {
|
||||
labels.add('new-platform');
|
||||
}
|
||||
}
|
||||
|
||||
// Check for new platform files: esphome/components/{component}/{platform}/__init__.py
|
||||
const platformDirMatch = file.match(/^esphome\/components\/([^\/]+)\/([^\/]+)\/__init__\.py$/);
|
||||
if (platformDirMatch) {
|
||||
const [, component, platform] = platformDirMatch;
|
||||
if (platformComponents.includes(platform)) {
|
||||
labels.add('new-platform');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const coreFiles = changedFiles.filter(file =>
|
||||
file.startsWith('esphome/core/') ||
|
||||
(file.startsWith('esphome/') && file.split('/').length === 2)
|
||||
);
|
||||
|
||||
if (coreFiles.length > 0) {
|
||||
labels.add('core');
|
||||
}
|
||||
|
||||
// Strategy: Small PR detection
|
||||
if (totalChanges <= smallPrThreshold) {
|
||||
labels.add('small-pr');
|
||||
}
|
||||
|
||||
// Strategy: Dashboard changes
|
||||
const dashboardFiles = changedFiles.filter(file =>
|
||||
file.startsWith('esphome/dashboard/') ||
|
||||
file.startsWith('esphome/components/dashboard_import/')
|
||||
);
|
||||
|
||||
if (dashboardFiles.length > 0) {
|
||||
labels.add('dashboard');
|
||||
}
|
||||
|
||||
// Strategy: GitHub Actions changes
|
||||
const githubActionsFiles = changedFiles.filter(file =>
|
||||
file.startsWith('.github/workflows/')
|
||||
);
|
||||
|
||||
if (githubActionsFiles.length > 0) {
|
||||
labels.add('github-actions');
|
||||
}
|
||||
|
||||
// Strategy: Code Owner detection
|
||||
try {
|
||||
// Fetch CODEOWNERS file from the repository (in case it was changed in this PR)
|
||||
const { data: codeownersFile } = await github.rest.repos.getContent({
|
||||
owner,
|
||||
repo,
|
||||
path: '.github/CODEOWNERS',
|
||||
ref: context.payload.pull_request.head.sha
|
||||
});
|
||||
|
||||
const codeownersContent = Buffer.from(codeownersFile.content, 'base64').toString('utf8');
|
||||
const prAuthor = context.payload.pull_request.user.login;
|
||||
|
||||
// Parse CODEOWNERS file
|
||||
const codeownersLines = codeownersContent.split('\n')
|
||||
.map(line => line.trim())
|
||||
.filter(line => line && !line.startsWith('#'));
|
||||
|
||||
let isCodeOwner = false;
|
||||
|
||||
// Precompile CODEOWNERS patterns into regex objects
|
||||
const codeownersRegexes = codeownersLines.map(line => {
|
||||
const parts = line.split(/\s+/);
|
||||
const pattern = parts[0];
|
||||
const owners = parts.slice(1);
|
||||
|
||||
let regex;
|
||||
if (pattern.endsWith('*')) {
|
||||
// Directory pattern like "esphome/components/api/*"
|
||||
const dir = pattern.slice(0, -1);
|
||||
regex = new RegExp(`^${dir.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}`);
|
||||
} else if (pattern.includes('*')) {
|
||||
// Glob pattern
|
||||
const regexPattern = pattern
|
||||
.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
|
||||
.replace(/\\*/g, '.*');
|
||||
regex = new RegExp(`^${regexPattern}$`);
|
||||
} else {
|
||||
// Exact match
|
||||
regex = new RegExp(`^${pattern.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}$`);
|
||||
}
|
||||
|
||||
return { regex, owners };
|
||||
});
|
||||
|
||||
for (const file of changedFiles) {
|
||||
for (const { regex, owners } of codeownersRegexes) {
|
||||
if (regex.test(file)) {
|
||||
// Check if PR author is in the owners list
|
||||
if (owners.some(owner => owner === `@${prAuthor}`)) {
|
||||
isCodeOwner = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (isCodeOwner) break;
|
||||
}
|
||||
|
||||
if (isCodeOwner) {
|
||||
labels.add('by-code-owner');
|
||||
}
|
||||
} catch (error) {
|
||||
console.log('Failed to read or parse CODEOWNERS file:', error.message);
|
||||
}
|
||||
|
||||
// Strategy: Test detection
|
||||
const testFiles = changedFiles.filter(file =>
|
||||
file.startsWith('tests/')
|
||||
);
|
||||
|
||||
if (testFiles.length > 0) {
|
||||
labels.add('has-tests');
|
||||
} else {
|
||||
// Only check for needs-tests if this is a new component or new platform
|
||||
if (labels.has('new-component') || labels.has('new-platform')) {
|
||||
labels.add('needs-tests');
|
||||
}
|
||||
}
|
||||
|
||||
// Strategy: Documentation check for new components/platforms
|
||||
if (labels.has('new-component') || labels.has('new-platform')) {
|
||||
const prBody = context.payload.pull_request.body || '';
|
||||
|
||||
// Look for documentation PR links
|
||||
// Patterns to match:
|
||||
// - https://github.com/esphome/esphome-docs/pull/1234
|
||||
// - esphome/esphome-docs#1234
|
||||
const docsPrPatterns = [
|
||||
/https:\/\/github\.com\/esphome\/esphome-docs\/pull\/\d+/,
|
||||
/esphome\/esphome-docs#\d+/
|
||||
];
|
||||
|
||||
const hasDocsLink = docsPrPatterns.some(pattern => pattern.test(prBody));
|
||||
|
||||
if (!hasDocsLink) {
|
||||
labels.add('needs-docs');
|
||||
}
|
||||
}
|
||||
|
||||
// Convert Set to Array
|
||||
let finalLabels = Array.from(labels);
|
||||
|
||||
console.log('Computed labels:', finalLabels.join(', '));
|
||||
|
||||
// Don't set more than max labels
|
||||
if (finalLabels.length > maxLabels) {
|
||||
const originalLength = finalLabels.length;
|
||||
console.log(`Not setting ${originalLength} labels because out of range`);
|
||||
finalLabels = ['too-big'];
|
||||
// Handle reviews
|
||||
await handleReviews(finalLabels);
|
||||
|
||||
// Request changes on the PR
|
||||
await github.rest.pulls.createReview({
|
||||
owner,
|
||||
repo,
|
||||
pull_number: pr_number,
|
||||
body: `This PR is too large and affects ${originalLength} different components/areas. Please consider breaking it down into smaller, focused PRs to make review easier and reduce the risk of conflicts.`,
|
||||
event: 'REQUEST_CHANGES'
|
||||
});
|
||||
}
|
||||
|
||||
// Add new labels
|
||||
// Apply labels
|
||||
if (finalLabels.length > 0) {
|
||||
console.log(`Adding labels: ${finalLabels.join(', ')}`);
|
||||
await github.rest.issues.addLabels({
|
||||
@ -430,11 +603,8 @@ jobs:
|
||||
});
|
||||
}
|
||||
|
||||
// Remove old managed labels that are no longer needed
|
||||
const labelsToRemove = managedLabels.filter(label =>
|
||||
!finalLabels.includes(label)
|
||||
);
|
||||
|
||||
// Remove old managed labels
|
||||
const labelsToRemove = managedLabels.filter(label => !finalLabels.includes(label));
|
||||
for (const label of labelsToRemove) {
|
||||
console.log(`Removing label: ${label}`);
|
||||
try {
|
||||
|
324
.github/workflows/codeowner-review-request.yml
vendored
Normal file
324
.github/workflows/codeowner-review-request.yml
vendored
Normal file
@ -0,0 +1,324 @@
|
||||
# This workflow automatically requests reviews from codeowners when:
|
||||
# 1. A PR is opened, reopened, or synchronized (updated)
|
||||
# 2. A PR is marked as ready for review
|
||||
#
|
||||
# It reads the CODEOWNERS file and matches all changed files in the PR against
|
||||
# the codeowner patterns, then requests reviews from the appropriate owners
|
||||
# while avoiding duplicate requests for users who have already been requested
|
||||
# or have already reviewed the PR.
|
||||
|
||||
name: Request Codeowner Reviews
|
||||
|
||||
on:
|
||||
# Needs to be pull_request_target to get write permissions
|
||||
pull_request_target:
|
||||
types: [opened, reopened, synchronize, ready_for_review]
|
||||
|
||||
permissions:
|
||||
pull-requests: write
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
request-codeowner-reviews:
|
||||
name: Run
|
||||
if: ${{ !github.event.pull_request.draft }}
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Request reviews from component codeowners
|
||||
uses: actions/github-script@v7.0.1
|
||||
with:
|
||||
script: |
|
||||
const owner = context.repo.owner;
|
||||
const repo = context.repo.repo;
|
||||
const pr_number = context.payload.pull_request.number;
|
||||
|
||||
console.log(`Processing PR #${pr_number} for codeowner review requests`);
|
||||
|
||||
// Hidden marker to identify bot comments from this workflow
|
||||
const BOT_COMMENT_MARKER = '<!-- codeowner-review-request-bot -->';
|
||||
|
||||
try {
|
||||
// Get the list of changed files in this PR
|
||||
const { data: files } = await github.rest.pulls.listFiles({
|
||||
owner,
|
||||
repo,
|
||||
pull_number: pr_number
|
||||
});
|
||||
|
||||
const changedFiles = files.map(file => file.filename);
|
||||
console.log(`Found ${changedFiles.length} changed files`);
|
||||
|
||||
if (changedFiles.length === 0) {
|
||||
console.log('No changed files found, skipping codeowner review requests');
|
||||
return;
|
||||
}
|
||||
|
||||
// Fetch CODEOWNERS file from root
|
||||
const { data: codeownersFile } = await github.rest.repos.getContent({
|
||||
owner,
|
||||
repo,
|
||||
path: 'CODEOWNERS',
|
||||
ref: context.payload.pull_request.base.sha
|
||||
});
|
||||
const codeownersContent = Buffer.from(codeownersFile.content, 'base64').toString('utf8');
|
||||
|
||||
// Parse CODEOWNERS file to extract all patterns and their owners
|
||||
const codeownersLines = codeownersContent.split('\n')
|
||||
.map(line => line.trim())
|
||||
.filter(line => line && !line.startsWith('#'));
|
||||
|
||||
const codeownersPatterns = [];
|
||||
|
||||
// Convert CODEOWNERS pattern to regex (robust glob handling)
|
||||
function globToRegex(pattern) {
|
||||
// Escape regex special characters except for glob wildcards
|
||||
let regexStr = pattern
|
||||
.replace(/([.+^=!:${}()|[\]\\])/g, '\\$1') // escape regex chars
|
||||
.replace(/\*\*/g, '.*') // globstar
|
||||
.replace(/\*/g, '[^/]*') // single star
|
||||
.replace(/\?/g, '.'); // question mark
|
||||
return new RegExp('^' + regexStr + '$');
|
||||
}
|
||||
|
||||
// Helper function to create comment body
|
||||
function createCommentBody(reviewersList, teamsList, matchedFileCount, isSuccessful = true) {
|
||||
const reviewerMentions = reviewersList.map(r => `@${r}`);
|
||||
const teamMentions = teamsList.map(t => `@${owner}/${t}`);
|
||||
const allMentions = [...reviewerMentions, ...teamMentions].join(', ');
|
||||
|
||||
if (isSuccessful) {
|
||||
return `${BOT_COMMENT_MARKER}\n👋 Hi there! I've automatically requested reviews from codeowners based on the files changed in this PR.\n\n${allMentions} - You've been requested to review this PR as codeowner(s) of ${matchedFileCount} file(s) that were modified. Thanks for your time! 🙏`;
|
||||
} else {
|
||||
return `${BOT_COMMENT_MARKER}\n👋 Hi there! This PR modifies ${matchedFileCount} file(s) with codeowners.\n\n${allMentions} - As codeowner(s) of the affected files, your review would be appreciated! 🙏\n\n_Note: Automatic review request may have failed, but you're still welcome to review._`;
|
||||
}
|
||||
}
|
||||
|
||||
for (const line of codeownersLines) {
|
||||
const parts = line.split(/\s+/);
|
||||
if (parts.length < 2) continue;
|
||||
|
||||
const pattern = parts[0];
|
||||
const owners = parts.slice(1);
|
||||
|
||||
// Use robust glob-to-regex conversion
|
||||
const regex = globToRegex(pattern);
|
||||
codeownersPatterns.push({ pattern, regex, owners });
|
||||
}
|
||||
|
||||
console.log(`Parsed ${codeownersPatterns.length} codeowner patterns`);
|
||||
|
||||
// Match changed files against CODEOWNERS patterns
|
||||
const matchedOwners = new Set();
|
||||
const matchedTeams = new Set();
|
||||
const fileMatches = new Map(); // Track which files matched which patterns
|
||||
|
||||
for (const file of changedFiles) {
|
||||
for (const { pattern, regex, owners } of codeownersPatterns) {
|
||||
if (regex.test(file)) {
|
||||
console.log(`File '${file}' matches pattern '${pattern}' with owners: ${owners.join(', ')}`);
|
||||
|
||||
if (!fileMatches.has(file)) {
|
||||
fileMatches.set(file, []);
|
||||
}
|
||||
fileMatches.get(file).push({ pattern, owners });
|
||||
|
||||
// Add owners to the appropriate set (remove @ prefix)
|
||||
for (const owner of owners) {
|
||||
const cleanOwner = owner.startsWith('@') ? owner.slice(1) : owner;
|
||||
if (cleanOwner.includes('/')) {
|
||||
// Team mention (org/team-name)
|
||||
const teamName = cleanOwner.split('/')[1];
|
||||
matchedTeams.add(teamName);
|
||||
} else {
|
||||
// Individual user
|
||||
matchedOwners.add(cleanOwner);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (matchedOwners.size === 0 && matchedTeams.size === 0) {
|
||||
console.log('No codeowners found for any changed files');
|
||||
return;
|
||||
}
|
||||
|
||||
// Remove the PR author from reviewers
|
||||
const prAuthor = context.payload.pull_request.user.login;
|
||||
matchedOwners.delete(prAuthor);
|
||||
|
||||
// Get current reviewers to avoid duplicate requests (but still mention them)
|
||||
const { data: prData } = await github.rest.pulls.get({
|
||||
owner,
|
||||
repo,
|
||||
pull_number: pr_number
|
||||
});
|
||||
|
||||
const currentReviewers = new Set();
|
||||
const currentTeams = new Set();
|
||||
|
||||
if (prData.requested_reviewers) {
|
||||
prData.requested_reviewers.forEach(reviewer => {
|
||||
currentReviewers.add(reviewer.login);
|
||||
});
|
||||
}
|
||||
|
||||
if (prData.requested_teams) {
|
||||
prData.requested_teams.forEach(team => {
|
||||
currentTeams.add(team.slug);
|
||||
});
|
||||
}
|
||||
|
||||
// Check for completed reviews to avoid re-requesting users who have already reviewed
|
||||
const { data: reviews } = await github.rest.pulls.listReviews({
|
||||
owner,
|
||||
repo,
|
||||
pull_number: pr_number
|
||||
});
|
||||
|
||||
const reviewedUsers = new Set();
|
||||
reviews.forEach(review => {
|
||||
reviewedUsers.add(review.user.login);
|
||||
});
|
||||
|
||||
// Check for previous comments from this workflow to avoid duplicate pings
|
||||
const comments = await github.paginate(
|
||||
github.rest.issues.listComments,
|
||||
{
|
||||
owner,
|
||||
repo,
|
||||
issue_number: pr_number
|
||||
}
|
||||
);
|
||||
|
||||
const previouslyPingedUsers = new Set();
|
||||
const previouslyPingedTeams = new Set();
|
||||
|
||||
// Look for comments from github-actions bot that contain our bot marker
|
||||
const workflowComments = comments.filter(comment =>
|
||||
comment.user.type === 'Bot' &&
|
||||
comment.body.includes(BOT_COMMENT_MARKER)
|
||||
);
|
||||
|
||||
// Extract previously mentioned users and teams from workflow comments
|
||||
for (const comment of workflowComments) {
|
||||
// Match @username patterns (not team mentions)
|
||||
const userMentions = comment.body.match(/@([a-zA-Z0-9_.-]+)(?![/])/g) || [];
|
||||
userMentions.forEach(mention => {
|
||||
const username = mention.slice(1); // remove @
|
||||
previouslyPingedUsers.add(username);
|
||||
});
|
||||
|
||||
// Match @org/team patterns
|
||||
const teamMentions = comment.body.match(/@[a-zA-Z0-9_.-]+\/([a-zA-Z0-9_.-]+)/g) || [];
|
||||
teamMentions.forEach(mention => {
|
||||
const teamName = mention.split('/')[1];
|
||||
previouslyPingedTeams.add(teamName);
|
||||
});
|
||||
}
|
||||
|
||||
console.log(`Found ${previouslyPingedUsers.size} previously pinged users and ${previouslyPingedTeams.size} previously pinged teams`);
|
||||
|
||||
// Remove users who have already been pinged in previous workflow comments
|
||||
previouslyPingedUsers.forEach(user => {
|
||||
matchedOwners.delete(user);
|
||||
});
|
||||
|
||||
previouslyPingedTeams.forEach(team => {
|
||||
matchedTeams.delete(team);
|
||||
});
|
||||
|
||||
// Remove only users who have already submitted reviews (not just requested reviewers)
|
||||
reviewedUsers.forEach(reviewer => {
|
||||
matchedOwners.delete(reviewer);
|
||||
});
|
||||
|
||||
// For teams, we'll still remove already requested teams to avoid API errors
|
||||
currentTeams.forEach(team => {
|
||||
matchedTeams.delete(team);
|
||||
});
|
||||
|
||||
const reviewersList = Array.from(matchedOwners);
|
||||
const teamsList = Array.from(matchedTeams);
|
||||
|
||||
if (reviewersList.length === 0 && teamsList.length === 0) {
|
||||
console.log('No eligible reviewers found (all may already be requested, reviewed, or previously pinged)');
|
||||
return;
|
||||
}
|
||||
|
||||
const totalReviewers = reviewersList.length + teamsList.length;
|
||||
console.log(`Requesting reviews from ${reviewersList.length} users and ${teamsList.length} teams for ${fileMatches.size} matched files`);
|
||||
|
||||
// Request reviews
|
||||
try {
|
||||
const requestParams = {
|
||||
owner,
|
||||
repo,
|
||||
pull_number: pr_number
|
||||
};
|
||||
|
||||
// Filter out users who are already requested reviewers for the API call
|
||||
const newReviewers = reviewersList.filter(reviewer => !currentReviewers.has(reviewer));
|
||||
const newTeams = teamsList.filter(team => !currentTeams.has(team));
|
||||
|
||||
if (newReviewers.length > 0) {
|
||||
requestParams.reviewers = newReviewers;
|
||||
}
|
||||
|
||||
if (newTeams.length > 0) {
|
||||
requestParams.team_reviewers = newTeams;
|
||||
}
|
||||
|
||||
// Only make the API call if there are new reviewers to request
|
||||
if (newReviewers.length > 0 || newTeams.length > 0) {
|
||||
await github.rest.pulls.requestReviewers(requestParams);
|
||||
console.log(`Successfully requested reviews from ${newReviewers.length} new users and ${newTeams.length} new teams`);
|
||||
} else {
|
||||
console.log('All codeowners are already requested reviewers or have reviewed');
|
||||
}
|
||||
|
||||
// Only add a comment if there are new codeowners to mention (not previously pinged)
|
||||
if (reviewersList.length > 0 || teamsList.length > 0) {
|
||||
const commentBody = createCommentBody(reviewersList, teamsList, fileMatches.size, true);
|
||||
|
||||
await github.rest.issues.createComment({
|
||||
owner,
|
||||
repo,
|
||||
issue_number: pr_number,
|
||||
body: commentBody
|
||||
});
|
||||
console.log(`Added comment mentioning ${reviewersList.length} users and ${teamsList.length} teams`);
|
||||
} else {
|
||||
console.log('No new codeowners to mention in comment (all previously pinged)');
|
||||
}
|
||||
} catch (error) {
|
||||
if (error.status === 422) {
|
||||
console.log('Some reviewers may already be requested or unavailable:', error.message);
|
||||
|
||||
// Only try to add a comment if there are new codeowners to mention
|
||||
if (reviewersList.length > 0 || teamsList.length > 0) {
|
||||
const commentBody = createCommentBody(reviewersList, teamsList, fileMatches.size, false);
|
||||
|
||||
try {
|
||||
await github.rest.issues.createComment({
|
||||
owner,
|
||||
repo,
|
||||
issue_number: pr_number,
|
||||
body: commentBody
|
||||
});
|
||||
console.log(`Added fallback comment mentioning ${reviewersList.length} users and ${teamsList.length} teams`);
|
||||
} catch (commentError) {
|
||||
console.log('Failed to add comment:', commentError.message);
|
||||
}
|
||||
} else {
|
||||
console.log('No new codeowners to mention in fallback comment');
|
||||
}
|
||||
} else {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.log('Failed to process codeowner review requests:', error.message);
|
||||
console.error(error);
|
||||
}
|
26
.github/workflows/external-component-bot.yml
vendored
26
.github/workflows/external-component-bot.yml
vendored
@ -61,7 +61,8 @@ jobs:
|
||||
}
|
||||
|
||||
async function createComment(octokit, owner, repo, prNumber, esphomeChanges, componentChanges) {
|
||||
const commentMarker = "<!-- This comment was generated automatically by a GitHub workflow. -->";
|
||||
const commentMarker = "<!-- This comment was generated automatically by the external-component-bot workflow. -->";
|
||||
const legacyCommentMarker = "<!-- This comment was generated automatically by a GitHub workflow. -->";
|
||||
let commentBody;
|
||||
if (esphomeChanges.length === 1) {
|
||||
commentBody = generateExternalComponentInstructions(prNumber, componentChanges, owner, repo);
|
||||
@ -71,14 +72,23 @@ jobs:
|
||||
commentBody += `\n\n---\n(Added by the PR bot)\n\n${commentMarker}`;
|
||||
|
||||
// Check for existing bot comment
|
||||
const comments = await github.rest.issues.listComments({
|
||||
owner: owner,
|
||||
repo: repo,
|
||||
issue_number: prNumber,
|
||||
});
|
||||
const comments = await github.paginate(
|
||||
github.rest.issues.listComments,
|
||||
{
|
||||
owner: owner,
|
||||
repo: repo,
|
||||
issue_number: prNumber,
|
||||
per_page: 100,
|
||||
}
|
||||
);
|
||||
|
||||
const botComment = comments.data.find(comment =>
|
||||
comment.body.includes(commentMarker)
|
||||
const sorted = comments.sort((a, b) => new Date(b.updated_at) - new Date(a.updated_at));
|
||||
|
||||
const botComment = sorted.find(comment =>
|
||||
(
|
||||
comment.body.includes(commentMarker) ||
|
||||
comment.body.includes(legacyCommentMarker)
|
||||
) && comment.user.type === "Bot"
|
||||
);
|
||||
|
||||
if (botComment && botComment.body === commentBody) {
|
||||
|
163
.github/workflows/issue-codeowner-notify.yml
vendored
Normal file
163
.github/workflows/issue-codeowner-notify.yml
vendored
Normal file
@ -0,0 +1,163 @@
|
||||
# This workflow automatically notifies codeowners when an issue is labeled with component labels.
|
||||
# It reads the CODEOWNERS file to find the maintainers for the labeled components
|
||||
# and posts a comment mentioning them to ensure they're aware of the issue.
|
||||
|
||||
name: Notify Issue Codeowners
|
||||
|
||||
on:
|
||||
issues:
|
||||
types: [labeled]
|
||||
|
||||
permissions:
|
||||
issues: write
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
notify-codeowners:
|
||||
name: Run
|
||||
if: ${{ startsWith(github.event.label.name, format('component{0} ', ':')) }}
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Notify codeowners for component issues
|
||||
uses: actions/github-script@v7.0.1
|
||||
with:
|
||||
script: |
|
||||
const owner = context.repo.owner;
|
||||
const repo = context.repo.repo;
|
||||
const issue_number = context.payload.issue.number;
|
||||
const labelName = context.payload.label.name;
|
||||
|
||||
console.log(`Processing issue #${issue_number} with label: ${labelName}`);
|
||||
|
||||
// Hidden marker to identify bot comments from this workflow
|
||||
const BOT_COMMENT_MARKER = '<!-- issue-codeowner-notify-bot -->';
|
||||
|
||||
// Extract component name from label
|
||||
const componentName = labelName.replace('component: ', '');
|
||||
console.log(`Component: ${componentName}`);
|
||||
|
||||
try {
|
||||
// Fetch CODEOWNERS file from root
|
||||
const { data: codeownersFile } = await github.rest.repos.getContent({
|
||||
owner,
|
||||
repo,
|
||||
path: 'CODEOWNERS'
|
||||
});
|
||||
const codeownersContent = Buffer.from(codeownersFile.content, 'base64').toString('utf8');
|
||||
|
||||
// Parse CODEOWNERS file to extract component mappings
|
||||
const codeownersLines = codeownersContent.split('\n')
|
||||
.map(line => line.trim())
|
||||
.filter(line => line && !line.startsWith('#'));
|
||||
|
||||
let componentOwners = null;
|
||||
|
||||
for (const line of codeownersLines) {
|
||||
const parts = line.split(/\s+/);
|
||||
if (parts.length < 2) continue;
|
||||
|
||||
const pattern = parts[0];
|
||||
const owners = parts.slice(1);
|
||||
|
||||
// Look for component patterns: esphome/components/{component}/*
|
||||
const componentMatch = pattern.match(/^esphome\/components\/([^\/]+)\/\*$/);
|
||||
if (componentMatch && componentMatch[1] === componentName) {
|
||||
componentOwners = owners;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!componentOwners) {
|
||||
console.log(`No codeowners found for component: ${componentName}`);
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(`Found codeowners for '${componentName}': ${componentOwners.join(', ')}`);
|
||||
|
||||
// Separate users and teams
|
||||
const userOwners = [];
|
||||
const teamOwners = [];
|
||||
|
||||
for (const owner of componentOwners) {
|
||||
const cleanOwner = owner.startsWith('@') ? owner.slice(1) : owner;
|
||||
if (cleanOwner.includes('/')) {
|
||||
// Team mention (org/team-name)
|
||||
teamOwners.push(`@${cleanOwner}`);
|
||||
} else {
|
||||
// Individual user
|
||||
userOwners.push(`@${cleanOwner}`);
|
||||
}
|
||||
}
|
||||
|
||||
// Remove issue author from mentions to avoid self-notification
|
||||
const issueAuthor = context.payload.issue.user.login;
|
||||
const filteredUserOwners = userOwners.filter(mention =>
|
||||
mention !== `@${issueAuthor}`
|
||||
);
|
||||
|
||||
// Check for previous comments from this workflow to avoid duplicate pings
|
||||
const comments = await github.paginate(
|
||||
github.rest.issues.listComments,
|
||||
{
|
||||
owner,
|
||||
repo,
|
||||
issue_number: issue_number
|
||||
}
|
||||
);
|
||||
|
||||
const previouslyPingedUsers = new Set();
|
||||
const previouslyPingedTeams = new Set();
|
||||
|
||||
// Look for comments from github-actions bot that contain codeowner pings for this component
|
||||
const workflowComments = comments.filter(comment =>
|
||||
comment.user.type === 'Bot' &&
|
||||
comment.body.includes(BOT_COMMENT_MARKER) &&
|
||||
comment.body.includes(`component: ${componentName}`)
|
||||
);
|
||||
|
||||
// Extract previously mentioned users and teams from workflow comments
|
||||
for (const comment of workflowComments) {
|
||||
// Match @username patterns (not team mentions)
|
||||
const userMentions = comment.body.match(/@([a-zA-Z0-9_.-]+)(?![/])/g) || [];
|
||||
userMentions.forEach(mention => {
|
||||
previouslyPingedUsers.add(mention); // Keep @ prefix for easy comparison
|
||||
});
|
||||
|
||||
// Match @org/team patterns
|
||||
const teamMentions = comment.body.match(/@[a-zA-Z0-9_.-]+\/[a-zA-Z0-9_.-]+/g) || [];
|
||||
teamMentions.forEach(mention => {
|
||||
previouslyPingedTeams.add(mention);
|
||||
});
|
||||
}
|
||||
|
||||
console.log(`Found ${previouslyPingedUsers.size} previously pinged users and ${previouslyPingedTeams.size} previously pinged teams for component ${componentName}`);
|
||||
|
||||
// Remove previously pinged users and teams
|
||||
const newUserOwners = filteredUserOwners.filter(mention => !previouslyPingedUsers.has(mention));
|
||||
const newTeamOwners = teamOwners.filter(mention => !previouslyPingedTeams.has(mention));
|
||||
|
||||
const allMentions = [...newUserOwners, ...newTeamOwners];
|
||||
|
||||
if (allMentions.length === 0) {
|
||||
console.log('No new codeowners to notify (all previously pinged or issue author is the only codeowner)');
|
||||
return;
|
||||
}
|
||||
|
||||
// Create comment body
|
||||
const mentionString = allMentions.join(', ');
|
||||
const commentBody = `${BOT_COMMENT_MARKER}\n👋 Hey ${mentionString}!\n\nThis issue has been labeled with \`component: ${componentName}\` and you've been identified as a codeowner of this component. Please take a look when you have a chance!\n\nThanks for maintaining this component! 🙏`;
|
||||
|
||||
// Post comment
|
||||
await github.rest.issues.createComment({
|
||||
owner,
|
||||
repo,
|
||||
issue_number: issue_number,
|
||||
body: commentBody
|
||||
});
|
||||
|
||||
console.log(`Successfully notified new codeowners: ${mentionString}`);
|
||||
|
||||
} catch (error) {
|
||||
console.log('Failed to process codeowner notifications:', error.message);
|
||||
console.error(error);
|
||||
}
|
@ -11,7 +11,7 @@ ci:
|
||||
repos:
|
||||
- repo: https://github.com/astral-sh/ruff-pre-commit
|
||||
# Ruff version.
|
||||
rev: v0.12.4
|
||||
rev: v0.12.5
|
||||
hooks:
|
||||
# Run the linter.
|
||||
- id: ruff
|
||||
|
@ -9,6 +9,7 @@
|
||||
pyproject.toml @esphome/core
|
||||
esphome/*.py @esphome/core
|
||||
esphome/core/* @esphome/core
|
||||
.github/** @esphome/core
|
||||
|
||||
# Integrations
|
||||
esphome/components/a01nyub/* @MrSuicideParrot
|
||||
@ -245,6 +246,7 @@ esphome/components/lcd_menu/* @numo68
|
||||
esphome/components/ld2410/* @regevbr @sebcaps
|
||||
esphome/components/ld2420/* @descipher
|
||||
esphome/components/ld2450/* @hareeshmu
|
||||
esphome/components/ld24xx/* @kbx81
|
||||
esphome/components/ledc/* @OttoWinter
|
||||
esphome/components/libretiny/* @kuba2k2
|
||||
esphome/components/libretiny_pwm/* @kuba2k2
|
||||
@ -292,6 +294,7 @@ esphome/components/microphone/* @jesserockz @kahrendt
|
||||
esphome/components/mics_4514/* @jesserockz
|
||||
esphome/components/midea/* @dudanov
|
||||
esphome/components/midea_ir/* @dudanov
|
||||
esphome/components/mipi_dsi/* @clydebarrow
|
||||
esphome/components/mipi_spi/* @clydebarrow
|
||||
esphome/components/mitsubishi/* @RubyBailey
|
||||
esphome/components/mixer/speaker/* @kahrendt
|
||||
|
@ -2,6 +2,7 @@
|
||||
import argparse
|
||||
from datetime import datetime
|
||||
import functools
|
||||
import getpass
|
||||
import importlib
|
||||
import logging
|
||||
import os
|
||||
@ -34,6 +35,7 @@ from esphome.const import (
|
||||
CONF_PORT,
|
||||
CONF_SUBSTITUTIONS,
|
||||
CONF_TOPIC,
|
||||
ENV_NOGITIGNORE,
|
||||
PLATFORM_ESP32,
|
||||
PLATFORM_ESP8266,
|
||||
PLATFORM_RP2040,
|
||||
@ -88,9 +90,9 @@ def choose_prompt(options, purpose: str = None):
|
||||
def choose_upload_log_host(
|
||||
default, check_default, show_ota, show_mqtt, show_api, purpose: str = None
|
||||
):
|
||||
options = []
|
||||
for port in get_serial_ports():
|
||||
options.append((f"{port.path} ({port.description})", port.path))
|
||||
options = [
|
||||
(f"{port.path} ({port.description})", port.path) for port in get_serial_ports()
|
||||
]
|
||||
if default == "SERIAL":
|
||||
return choose_prompt(options, purpose=purpose)
|
||||
if (show_ota and "ota" in CORE.config) or (show_api and "api" in CORE.config):
|
||||
@ -118,9 +120,7 @@ def mqtt_logging_enabled(mqtt_config):
|
||||
return False
|
||||
if CONF_TOPIC not in log_topic:
|
||||
return False
|
||||
if log_topic.get(CONF_LEVEL, None) == "NONE":
|
||||
return False
|
||||
return True
|
||||
return log_topic.get(CONF_LEVEL, None) != "NONE"
|
||||
|
||||
|
||||
def get_port_type(port):
|
||||
@ -209,6 +209,9 @@ def wrap_to_code(name, comp):
|
||||
|
||||
|
||||
def write_cpp(config):
|
||||
if not get_bool_env(ENV_NOGITIGNORE):
|
||||
writer.write_gitignore()
|
||||
|
||||
generate_cpp_contents(config)
|
||||
return write_cpp_file()
|
||||
|
||||
@ -225,10 +228,13 @@ def generate_cpp_contents(config):
|
||||
|
||||
|
||||
def write_cpp_file():
|
||||
writer.write_platformio_project()
|
||||
|
||||
code_s = indent(CORE.cpp_main_section)
|
||||
writer.write_cpp(code_s)
|
||||
|
||||
from esphome.build_gen import platformio
|
||||
|
||||
platformio.write_project()
|
||||
|
||||
return 0
|
||||
|
||||
|
||||
@ -330,7 +336,7 @@ def check_permissions(port):
|
||||
raise EsphomeError(
|
||||
"You do not have read or write permission on the selected serial port. "
|
||||
"To resolve this issue, you can add your user to the dialout group "
|
||||
f"by running the following command: sudo usermod -a -G dialout {os.getlogin()}. "
|
||||
f"by running the following command: sudo usermod -a -G dialout {getpass.getuser()}. "
|
||||
"You will need to log out & back in or reboot to activate the new group access."
|
||||
)
|
||||
|
||||
|
0
esphome/build_gen/__init__.py
Normal file
0
esphome/build_gen/__init__.py
Normal file
102
esphome/build_gen/platformio.py
Normal file
102
esphome/build_gen/platformio.py
Normal file
@ -0,0 +1,102 @@
|
||||
import os
|
||||
|
||||
from esphome.const import __version__
|
||||
from esphome.core import CORE
|
||||
from esphome.helpers import mkdir_p, read_file, write_file_if_changed
|
||||
from esphome.writer import find_begin_end, update_storage_json
|
||||
|
||||
INI_AUTO_GENERATE_BEGIN = "; ========== AUTO GENERATED CODE BEGIN ==========="
|
||||
INI_AUTO_GENERATE_END = "; =========== AUTO GENERATED CODE END ============"
|
||||
|
||||
INI_BASE_FORMAT = (
|
||||
"""; Auto generated code by esphome
|
||||
|
||||
[common]
|
||||
lib_deps =
|
||||
build_flags =
|
||||
upload_flags =
|
||||
|
||||
""",
|
||||
"""
|
||||
|
||||
""",
|
||||
)
|
||||
|
||||
|
||||
def format_ini(data: dict[str, str | list[str]]) -> str:
|
||||
content = ""
|
||||
for key, value in sorted(data.items()):
|
||||
if isinstance(value, list):
|
||||
content += f"{key} =\n"
|
||||
for x in value:
|
||||
content += f" {x}\n"
|
||||
else:
|
||||
content += f"{key} = {value}\n"
|
||||
return content
|
||||
|
||||
|
||||
def get_ini_content():
|
||||
CORE.add_platformio_option(
|
||||
"lib_deps",
|
||||
[x.as_lib_dep for x in CORE.platformio_libraries.values()]
|
||||
+ ["${common.lib_deps}"],
|
||||
)
|
||||
# Sort to avoid changing build flags order
|
||||
CORE.add_platformio_option("build_flags", sorted(CORE.build_flags))
|
||||
|
||||
# Sort to avoid changing build unflags order
|
||||
CORE.add_platformio_option("build_unflags", sorted(CORE.build_unflags))
|
||||
|
||||
# Add extra script for C++ flags
|
||||
CORE.add_platformio_option("extra_scripts", [f"pre:{CXX_FLAGS_FILE_NAME}"])
|
||||
|
||||
content = "[platformio]\n"
|
||||
content += f"description = ESPHome {__version__}\n"
|
||||
|
||||
content += f"[env:{CORE.name}]\n"
|
||||
content += format_ini(CORE.platformio_options)
|
||||
|
||||
return content
|
||||
|
||||
|
||||
def write_ini(content):
|
||||
update_storage_json()
|
||||
path = CORE.relative_build_path("platformio.ini")
|
||||
|
||||
if os.path.isfile(path):
|
||||
text = read_file(path)
|
||||
content_format = find_begin_end(
|
||||
text, INI_AUTO_GENERATE_BEGIN, INI_AUTO_GENERATE_END
|
||||
)
|
||||
else:
|
||||
content_format = INI_BASE_FORMAT
|
||||
full_file = f"{content_format[0] + INI_AUTO_GENERATE_BEGIN}\n{content}"
|
||||
full_file += INI_AUTO_GENERATE_END + content_format[1]
|
||||
write_file_if_changed(path, full_file)
|
||||
|
||||
|
||||
def write_project():
|
||||
mkdir_p(CORE.build_path)
|
||||
|
||||
content = get_ini_content()
|
||||
write_ini(content)
|
||||
|
||||
# Write extra script for C++ specific flags
|
||||
write_cxx_flags_script()
|
||||
|
||||
|
||||
CXX_FLAGS_FILE_NAME = "cxx_flags.py"
|
||||
CXX_FLAGS_FILE_CONTENTS = """# Auto-generated ESPHome script for C++ specific compiler flags
|
||||
Import("env")
|
||||
|
||||
# Add C++ specific flags
|
||||
"""
|
||||
|
||||
|
||||
def write_cxx_flags_script() -> None:
|
||||
path = CORE.relative_build_path(CXX_FLAGS_FILE_NAME)
|
||||
contents = CXX_FLAGS_FILE_CONTENTS
|
||||
if not CORE.is_host:
|
||||
contents += 'env.Append(CXXFLAGS=["-Wno-volatile"])'
|
||||
contents += "\n"
|
||||
write_file_if_changed(path, contents)
|
@ -7,7 +7,6 @@ namespace a4988 {
|
||||
static const char *const TAG = "a4988.stepper";
|
||||
|
||||
void A4988::setup() {
|
||||
ESP_LOGCONFIG(TAG, "Running setup");
|
||||
if (this->sleep_pin_ != nullptr) {
|
||||
this->sleep_pin_->setup();
|
||||
this->sleep_pin_->digital_write(false);
|
||||
|
@ -7,8 +7,6 @@ namespace absolute_humidity {
|
||||
static const char *const TAG = "absolute_humidity.sensor";
|
||||
|
||||
void AbsoluteHumidityComponent::setup() {
|
||||
ESP_LOGCONFIG(TAG, "Running setup for '%s'", this->get_name().c_str());
|
||||
|
||||
ESP_LOGD(TAG, " Added callback for temperature '%s'", this->temperature_sensor_->get_name().c_str());
|
||||
this->temperature_sensor_->add_on_state_callback([this](float state) { this->temperature_callback_(state); });
|
||||
if (this->temperature_sensor_->has_state()) {
|
||||
|
@ -37,7 +37,6 @@ const LogString *adc_unit_to_str(adc_unit_t unit) {
|
||||
}
|
||||
|
||||
void ADCSensor::setup() {
|
||||
ESP_LOGCONFIG(TAG, "Running setup for '%s'", this->get_name().c_str());
|
||||
// Check if another sensor already initialized this ADC unit
|
||||
if (ADCSensor::shared_adc_handles[this->adc_unit_] == nullptr) {
|
||||
adc_oneshot_unit_init_cfg_t init_config = {}; // Zero initialize
|
||||
|
@ -17,7 +17,6 @@ namespace adc {
|
||||
static const char *const TAG = "adc.esp8266";
|
||||
|
||||
void ADCSensor::setup() {
|
||||
ESP_LOGCONFIG(TAG, "Running setup for '%s'", this->get_name().c_str());
|
||||
#ifndef USE_ADC_SENSOR_VCC
|
||||
this->pin_->setup();
|
||||
#endif
|
||||
|
@ -9,7 +9,6 @@ namespace adc {
|
||||
static const char *const TAG = "adc.libretiny";
|
||||
|
||||
void ADCSensor::setup() {
|
||||
ESP_LOGCONFIG(TAG, "Running setup for '%s'", this->get_name().c_str());
|
||||
#ifndef USE_ADC_SENSOR_VCC
|
||||
this->pin_->setup();
|
||||
#endif // !USE_ADC_SENSOR_VCC
|
||||
|
@ -14,7 +14,6 @@ namespace adc {
|
||||
static const char *const TAG = "adc.rp2040";
|
||||
|
||||
void ADCSensor::setup() {
|
||||
ESP_LOGCONFIG(TAG, "Running setup for '%s'", this->get_name().c_str());
|
||||
static bool initialized = false;
|
||||
if (!initialized) {
|
||||
adc_init();
|
||||
|
@ -8,10 +8,7 @@ static const char *const TAG = "adc128s102";
|
||||
|
||||
float ADC128S102::get_setup_priority() const { return setup_priority::HARDWARE; }
|
||||
|
||||
void ADC128S102::setup() {
|
||||
ESP_LOGCONFIG(TAG, "Running setup");
|
||||
this->spi_setup();
|
||||
}
|
||||
void ADC128S102::setup() { this->spi_setup(); }
|
||||
|
||||
void ADC128S102::dump_config() {
|
||||
ESP_LOGCONFIG(TAG, "ADC128S102:");
|
||||
|
@ -10,7 +10,6 @@ static const uint8_t ADS1115_REGISTER_CONVERSION = 0x00;
|
||||
static const uint8_t ADS1115_REGISTER_CONFIG = 0x01;
|
||||
|
||||
void ADS1115Component::setup() {
|
||||
ESP_LOGCONFIG(TAG, "Running setup");
|
||||
uint16_t value;
|
||||
if (!this->read_byte_16(ADS1115_REGISTER_CONVERSION, &value)) {
|
||||
this->mark_failed();
|
||||
|
@ -9,7 +9,6 @@ static const char *const TAG = "ads1118";
|
||||
static const uint8_t ADS1118_DATA_RATE_860_SPS = 0b111;
|
||||
|
||||
void ADS1118::setup() {
|
||||
ESP_LOGCONFIG(TAG, "Running setup");
|
||||
this->spi_setup();
|
||||
|
||||
this->config_ = 0;
|
||||
|
@ -24,8 +24,6 @@ static const uint16_t ZP_CURRENT = 0x0000;
|
||||
static const uint16_t ZP_DEFAULT = 0xFFFF;
|
||||
|
||||
void AGS10Component::setup() {
|
||||
ESP_LOGCONFIG(TAG, "Running setup");
|
||||
|
||||
auto version = this->read_version_();
|
||||
if (version) {
|
||||
ESP_LOGD(TAG, "AGS10 Sensor Version: 0x%02X", *version);
|
||||
@ -45,8 +43,6 @@ void AGS10Component::setup() {
|
||||
} else {
|
||||
ESP_LOGE(TAG, "AGS10 Sensor Resistance: unknown");
|
||||
}
|
||||
|
||||
ESP_LOGD(TAG, "Sensor initialized");
|
||||
}
|
||||
|
||||
void AGS10Component::update() {
|
||||
|
@ -38,8 +38,6 @@ static const uint8_t AHT10_STATUS_BUSY = 0x80;
|
||||
static const float AHT10_DIVISOR = 1048576.0f; // 2^20, used for temperature and humidity calculations
|
||||
|
||||
void AHT10Component::setup() {
|
||||
ESP_LOGCONFIG(TAG, "Running setup");
|
||||
|
||||
if (this->write(AHT10_SOFTRESET_CMD, sizeof(AHT10_SOFTRESET_CMD)) != i2c::ERROR_OK) {
|
||||
ESP_LOGE(TAG, "Reset failed");
|
||||
}
|
||||
@ -80,8 +78,6 @@ void AHT10Component::setup() {
|
||||
this->mark_failed();
|
||||
return;
|
||||
}
|
||||
|
||||
ESP_LOGV(TAG, "Initialization complete");
|
||||
}
|
||||
|
||||
void AHT10Component::restart_read_() {
|
||||
|
@ -17,8 +17,6 @@ static const char *const TAG = "aic3204";
|
||||
}
|
||||
|
||||
void AIC3204::setup() {
|
||||
ESP_LOGCONFIG(TAG, "Running setup");
|
||||
|
||||
// Set register page to 0
|
||||
ERROR_CHECK(this->write_byte(AIC3204_PAGE_CTRL, 0x00), "Set page 0 failed");
|
||||
// Initiate SW reset (PLL is powered off as part of reset)
|
||||
|
@ -90,8 +90,6 @@ bool AM2315C::convert_(uint8_t *data, float &humidity, float &temperature) {
|
||||
}
|
||||
|
||||
void AM2315C::setup() {
|
||||
ESP_LOGCONFIG(TAG, "Running setup");
|
||||
|
||||
// get status
|
||||
uint8_t status = 0;
|
||||
if (this->read(&status, 1) != i2c::ERROR_OK) {
|
||||
|
@ -34,7 +34,6 @@ void AM2320Component::update() {
|
||||
this->status_clear_warning();
|
||||
}
|
||||
void AM2320Component::setup() {
|
||||
ESP_LOGCONFIG(TAG, "Running setup");
|
||||
uint8_t data[8];
|
||||
data[0] = 0;
|
||||
data[1] = 4;
|
||||
|
@ -54,8 +54,6 @@ enum { // APDS9306 registers
|
||||
}
|
||||
|
||||
void APDS9306::setup() {
|
||||
ESP_LOGCONFIG(TAG, "Running setup");
|
||||
|
||||
uint8_t id;
|
||||
if (!this->read_byte(APDS9306_PART_ID, &id)) { // Part ID register
|
||||
this->error_code_ = COMMUNICATION_FAILED;
|
||||
@ -86,8 +84,6 @@ void APDS9306::setup() {
|
||||
|
||||
// Set to active mode
|
||||
APDS9306_WRITE_BYTE(APDS9306_MAIN_CTRL, 0x02);
|
||||
|
||||
ESP_LOGCONFIG(TAG, "APDS9306 setup complete");
|
||||
}
|
||||
|
||||
void APDS9306::dump_config() {
|
||||
|
@ -15,7 +15,6 @@ static const char *const TAG = "apds9960";
|
||||
#define APDS9960_WRITE_BYTE(reg, value) APDS9960_ERROR_CHECK(this->write_byte(reg, value));
|
||||
|
||||
void APDS9960::setup() {
|
||||
ESP_LOGCONFIG(TAG, "Running setup");
|
||||
uint8_t id;
|
||||
if (!this->read_byte(0x92, &id)) { // ID register
|
||||
this->error_code_ = COMMUNICATION_FAILED;
|
||||
|
@ -53,6 +53,8 @@ SERVICE_ARG_NATIVE_TYPES = {
|
||||
CONF_ENCRYPTION = "encryption"
|
||||
CONF_BATCH_DELAY = "batch_delay"
|
||||
CONF_CUSTOM_SERVICES = "custom_services"
|
||||
CONF_HOMEASSISTANT_SERVICES = "homeassistant_services"
|
||||
CONF_HOMEASSISTANT_STATES = "homeassistant_states"
|
||||
|
||||
|
||||
def validate_encryption_key(value):
|
||||
@ -118,6 +120,8 @@ CONFIG_SCHEMA = cv.All(
|
||||
cv.Range(max=cv.TimePeriod(milliseconds=65535)),
|
||||
),
|
||||
cv.Optional(CONF_CUSTOM_SERVICES, default=False): cv.boolean,
|
||||
cv.Optional(CONF_HOMEASSISTANT_SERVICES, default=False): cv.boolean,
|
||||
cv.Optional(CONF_HOMEASSISTANT_STATES, default=False): cv.boolean,
|
||||
cv.Optional(CONF_ON_CLIENT_CONNECTED): automation.validate_automation(
|
||||
single=True
|
||||
),
|
||||
@ -146,6 +150,12 @@ async def to_code(config):
|
||||
if config.get(CONF_ACTIONS) or config[CONF_CUSTOM_SERVICES]:
|
||||
cg.add_define("USE_API_SERVICES")
|
||||
|
||||
if config[CONF_HOMEASSISTANT_SERVICES]:
|
||||
cg.add_define("USE_API_HOMEASSISTANT_SERVICES")
|
||||
|
||||
if config[CONF_HOMEASSISTANT_STATES]:
|
||||
cg.add_define("USE_API_HOMEASSISTANT_STATES")
|
||||
|
||||
if actions := config.get(CONF_ACTIONS, []):
|
||||
for conf in actions:
|
||||
template_args = []
|
||||
@ -235,6 +245,7 @@ HOMEASSISTANT_ACTION_ACTION_SCHEMA = cv.All(
|
||||
HOMEASSISTANT_ACTION_ACTION_SCHEMA,
|
||||
)
|
||||
async def homeassistant_service_to_code(config, action_id, template_arg, args):
|
||||
cg.add_define("USE_API_HOMEASSISTANT_SERVICES")
|
||||
serv = await cg.get_variable(config[CONF_ID])
|
||||
var = cg.new_Pvariable(action_id, template_arg, serv, False)
|
||||
templ = await cg.templatable(config[CONF_ACTION], args, None)
|
||||
@ -278,6 +289,7 @@ HOMEASSISTANT_EVENT_ACTION_SCHEMA = cv.Schema(
|
||||
HOMEASSISTANT_EVENT_ACTION_SCHEMA,
|
||||
)
|
||||
async def homeassistant_event_to_code(config, action_id, template_arg, args):
|
||||
cg.add_define("USE_API_HOMEASSISTANT_SERVICES")
|
||||
serv = await cg.get_variable(config[CONF_ID])
|
||||
var = cg.new_Pvariable(action_id, template_arg, serv, True)
|
||||
templ = await cg.templatable(config[CONF_EVENT], args, None)
|
||||
@ -323,9 +335,10 @@ async def api_connected_to_code(config, condition_id, template_arg, args):
|
||||
|
||||
|
||||
def FILTER_SOURCE_FILES() -> list[str]:
|
||||
"""Filter out api_pb2_dump.cpp when proto message dumping is not enabled
|
||||
and user_services.cpp when no services are defined."""
|
||||
files_to_filter = []
|
||||
"""Filter out api_pb2_dump.cpp when proto message dumping is not enabled,
|
||||
user_services.cpp when no services are defined, and protocol-specific
|
||||
implementations based on encryption configuration."""
|
||||
files_to_filter: list[str] = []
|
||||
|
||||
# api_pb2_dump.cpp is only needed when HAS_PROTO_MESSAGE_DUMP is defined
|
||||
# This is a particularly large file that still needs to be opened and read
|
||||
@ -341,4 +354,16 @@ def FILTER_SOURCE_FILES() -> list[str]:
|
||||
if config and not config.get(CONF_ACTIONS) and not config[CONF_CUSTOM_SERVICES]:
|
||||
files_to_filter.append("user_services.cpp")
|
||||
|
||||
# Filter protocol-specific implementations based on encryption configuration
|
||||
encryption_config = config.get(CONF_ENCRYPTION) if config else None
|
||||
|
||||
# If encryption is not configured at all, we only need plaintext
|
||||
if encryption_config is None:
|
||||
files_to_filter.append("api_frame_helper_noise.cpp")
|
||||
# If encryption is configured with a key, we only need noise
|
||||
elif encryption_config.get(CONF_KEY):
|
||||
files_to_filter.append("api_frame_helper_plaintext.cpp")
|
||||
# If encryption is configured but no key is provided, we need both
|
||||
# (this allows a plaintext client to provide a noise key)
|
||||
|
||||
return files_to_filter
|
||||
|
@ -203,7 +203,7 @@ message DeviceInfoResponse {
|
||||
option (id) = 10;
|
||||
option (source) = SOURCE_SERVER;
|
||||
|
||||
bool uses_password = 1;
|
||||
bool uses_password = 1 [(field_ifdef) = "USE_API_PASSWORD"];
|
||||
|
||||
// The name of the node, given by "App.set_name()"
|
||||
string name = 2;
|
||||
@ -230,14 +230,16 @@ message DeviceInfoResponse {
|
||||
|
||||
uint32 webserver_port = 10 [(field_ifdef) = "USE_WEBSERVER"];
|
||||
|
||||
uint32 legacy_bluetooth_proxy_version = 11 [(field_ifdef) = "USE_BLUETOOTH_PROXY"];
|
||||
// Deprecated in API version 1.9
|
||||
uint32 legacy_bluetooth_proxy_version = 11 [deprecated=true, (field_ifdef) = "USE_BLUETOOTH_PROXY"];
|
||||
uint32 bluetooth_proxy_feature_flags = 15 [(field_ifdef) = "USE_BLUETOOTH_PROXY"];
|
||||
|
||||
string manufacturer = 12;
|
||||
|
||||
string friendly_name = 13;
|
||||
|
||||
uint32 legacy_voice_assistant_version = 14 [(field_ifdef) = "USE_VOICE_ASSISTANT"];
|
||||
// Deprecated in API version 1.10
|
||||
uint32 legacy_voice_assistant_version = 14 [deprecated=true, (field_ifdef) = "USE_VOICE_ASSISTANT"];
|
||||
uint32 voice_assistant_feature_flags = 17 [(field_ifdef) = "USE_VOICE_ASSISTANT"];
|
||||
|
||||
string suggested_area = 16 [(field_ifdef) = "USE_AREAS"];
|
||||
@ -337,7 +339,9 @@ message ListEntitiesCoverResponse {
|
||||
uint32 device_id = 13 [(field_ifdef) = "USE_DEVICES"];
|
||||
}
|
||||
|
||||
// Deprecated in API version 1.1
|
||||
enum LegacyCoverState {
|
||||
option deprecated = true;
|
||||
LEGACY_COVER_STATE_OPEN = 0;
|
||||
LEGACY_COVER_STATE_CLOSED = 1;
|
||||
}
|
||||
@ -356,7 +360,8 @@ message CoverStateResponse {
|
||||
fixed32 key = 1;
|
||||
// legacy: state has been removed in 1.13
|
||||
// clients/servers must still send/accept it until the next protocol change
|
||||
LegacyCoverState legacy_state = 2;
|
||||
// Deprecated in API version 1.1
|
||||
LegacyCoverState legacy_state = 2 [deprecated=true];
|
||||
|
||||
float position = 3;
|
||||
float tilt = 4;
|
||||
@ -364,7 +369,9 @@ message CoverStateResponse {
|
||||
uint32 device_id = 6 [(field_ifdef) = "USE_DEVICES"];
|
||||
}
|
||||
|
||||
// Deprecated in API version 1.1
|
||||
enum LegacyCoverCommand {
|
||||
option deprecated = true;
|
||||
LEGACY_COVER_COMMAND_OPEN = 0;
|
||||
LEGACY_COVER_COMMAND_CLOSE = 1;
|
||||
LEGACY_COVER_COMMAND_STOP = 2;
|
||||
@ -380,8 +387,10 @@ message CoverCommandRequest {
|
||||
|
||||
// legacy: command has been removed in 1.13
|
||||
// clients/servers must still send/accept it until the next protocol change
|
||||
bool has_legacy_command = 2;
|
||||
LegacyCoverCommand legacy_command = 3;
|
||||
// Deprecated in API version 1.1
|
||||
bool has_legacy_command = 2 [deprecated=true];
|
||||
// Deprecated in API version 1.1
|
||||
LegacyCoverCommand legacy_command = 3 [deprecated=true];
|
||||
|
||||
bool has_position = 4;
|
||||
float position = 5;
|
||||
@ -413,7 +422,9 @@ message ListEntitiesFanResponse {
|
||||
repeated string supported_preset_modes = 12;
|
||||
uint32 device_id = 13 [(field_ifdef) = "USE_DEVICES"];
|
||||
}
|
||||
// Deprecated in API version 1.6 - only used in deprecated fields
|
||||
enum FanSpeed {
|
||||
option deprecated = true;
|
||||
FAN_SPEED_LOW = 0;
|
||||
FAN_SPEED_MEDIUM = 1;
|
||||
FAN_SPEED_HIGH = 2;
|
||||
@ -432,7 +443,8 @@ message FanStateResponse {
|
||||
fixed32 key = 1;
|
||||
bool state = 2;
|
||||
bool oscillating = 3;
|
||||
FanSpeed speed = 4 [deprecated = true];
|
||||
// Deprecated in API version 1.6
|
||||
FanSpeed speed = 4 [deprecated=true];
|
||||
FanDirection direction = 5;
|
||||
int32 speed_level = 6;
|
||||
string preset_mode = 7;
|
||||
@ -448,8 +460,10 @@ message FanCommandRequest {
|
||||
fixed32 key = 1;
|
||||
bool has_state = 2;
|
||||
bool state = 3;
|
||||
bool has_speed = 4 [deprecated = true];
|
||||
FanSpeed speed = 5 [deprecated = true];
|
||||
// Deprecated in API version 1.6
|
||||
bool has_speed = 4 [deprecated=true];
|
||||
// Deprecated in API version 1.6
|
||||
FanSpeed speed = 5 [deprecated=true];
|
||||
bool has_oscillating = 6;
|
||||
bool oscillating = 7;
|
||||
bool has_direction = 8;
|
||||
@ -488,9 +502,13 @@ message ListEntitiesLightResponse {
|
||||
|
||||
repeated ColorMode supported_color_modes = 12;
|
||||
// next four supports_* are for legacy clients, newer clients should use color modes
|
||||
// Deprecated in API version 1.6
|
||||
bool legacy_supports_brightness = 5 [deprecated=true];
|
||||
// Deprecated in API version 1.6
|
||||
bool legacy_supports_rgb = 6 [deprecated=true];
|
||||
// Deprecated in API version 1.6
|
||||
bool legacy_supports_white_value = 7 [deprecated=true];
|
||||
// Deprecated in API version 1.6
|
||||
bool legacy_supports_color_temperature = 8 [deprecated=true];
|
||||
float min_mireds = 9;
|
||||
float max_mireds = 10;
|
||||
@ -567,7 +585,9 @@ enum SensorStateClass {
|
||||
STATE_CLASS_TOTAL = 3;
|
||||
}
|
||||
|
||||
// Deprecated in API version 1.5
|
||||
enum SensorLastResetType {
|
||||
option deprecated = true;
|
||||
LAST_RESET_NONE = 0;
|
||||
LAST_RESET_NEVER = 1;
|
||||
LAST_RESET_AUTO = 2;
|
||||
@ -591,7 +611,8 @@ message ListEntitiesSensorResponse {
|
||||
string device_class = 9;
|
||||
SensorStateClass state_class = 10;
|
||||
// Last reset type removed in 2021.9.0
|
||||
SensorLastResetType legacy_last_reset_type = 11;
|
||||
// Deprecated in API version 1.5
|
||||
SensorLastResetType legacy_last_reset_type = 11 [deprecated=true];
|
||||
bool disabled_by_default = 12;
|
||||
EntityCategory entity_category = 13;
|
||||
uint32 device_id = 14 [(field_ifdef) = "USE_DEVICES"];
|
||||
@ -711,7 +732,6 @@ message SubscribeLogsResponse {
|
||||
|
||||
LogLevel level = 1;
|
||||
bytes message = 3;
|
||||
bool send_failed = 4;
|
||||
}
|
||||
|
||||
// ==================== NOISE ENCRYPTION ====================
|
||||
@ -735,17 +755,19 @@ message NoiseEncryptionSetKeyResponse {
|
||||
message SubscribeHomeassistantServicesRequest {
|
||||
option (id) = 34;
|
||||
option (source) = SOURCE_CLIENT;
|
||||
option (ifdef) = "USE_API_HOMEASSISTANT_SERVICES";
|
||||
}
|
||||
|
||||
message HomeassistantServiceMap {
|
||||
string key = 1;
|
||||
string value = 2;
|
||||
string value = 2 [(no_zero_copy) = true];
|
||||
}
|
||||
|
||||
message HomeassistantServiceResponse {
|
||||
option (id) = 35;
|
||||
option (source) = SOURCE_SERVER;
|
||||
option (no_delay) = true;
|
||||
option (ifdef) = "USE_API_HOMEASSISTANT_SERVICES";
|
||||
|
||||
string service = 1;
|
||||
repeated HomeassistantServiceMap data = 2;
|
||||
@ -761,11 +783,13 @@ message HomeassistantServiceResponse {
|
||||
message SubscribeHomeAssistantStatesRequest {
|
||||
option (id) = 38;
|
||||
option (source) = SOURCE_CLIENT;
|
||||
option (ifdef) = "USE_API_HOMEASSISTANT_STATES";
|
||||
}
|
||||
|
||||
message SubscribeHomeAssistantStateResponse {
|
||||
option (id) = 39;
|
||||
option (source) = SOURCE_SERVER;
|
||||
option (ifdef) = "USE_API_HOMEASSISTANT_STATES";
|
||||
string entity_id = 1;
|
||||
string attribute = 2;
|
||||
bool once = 3;
|
||||
@ -775,6 +799,7 @@ message HomeAssistantStateResponse {
|
||||
option (id) = 40;
|
||||
option (source) = SOURCE_CLIENT;
|
||||
option (no_delay) = true;
|
||||
option (ifdef) = "USE_API_HOMEASSISTANT_STATES";
|
||||
|
||||
string entity_id = 1;
|
||||
string state = 2;
|
||||
@ -947,7 +972,8 @@ message ListEntitiesClimateResponse {
|
||||
float visual_target_temperature_step = 10;
|
||||
// for older peer versions - in new system this
|
||||
// is if CLIMATE_PRESET_AWAY exists is supported_presets
|
||||
bool legacy_supports_away = 11;
|
||||
// Deprecated in API version 1.5
|
||||
bool legacy_supports_away = 11 [deprecated=true];
|
||||
bool supports_action = 12;
|
||||
repeated ClimateFanMode supported_fan_modes = 13;
|
||||
repeated ClimateSwingMode supported_swing_modes = 14;
|
||||
@ -978,7 +1004,8 @@ message ClimateStateResponse {
|
||||
float target_temperature_low = 5;
|
||||
float target_temperature_high = 6;
|
||||
// For older peers, equal to preset == CLIMATE_PRESET_AWAY
|
||||
bool unused_legacy_away = 7;
|
||||
// Deprecated in API version 1.5
|
||||
bool unused_legacy_away = 7 [deprecated=true];
|
||||
ClimateAction action = 8;
|
||||
ClimateFanMode fan_mode = 9;
|
||||
ClimateSwingMode swing_mode = 10;
|
||||
@ -1006,8 +1033,10 @@ message ClimateCommandRequest {
|
||||
bool has_target_temperature_high = 8;
|
||||
float target_temperature_high = 9;
|
||||
// legacy, for older peers, newer ones should use CLIMATE_PRESET_AWAY in preset
|
||||
bool unused_has_legacy_away = 10;
|
||||
bool unused_legacy_away = 11;
|
||||
// Deprecated in API version 1.5
|
||||
bool unused_has_legacy_away = 10 [deprecated=true];
|
||||
// Deprecated in API version 1.5
|
||||
bool unused_legacy_away = 11 [deprecated=true];
|
||||
bool has_fan_mode = 12;
|
||||
ClimateFanMode fan_mode = 13;
|
||||
bool has_swing_mode = 14;
|
||||
@ -1354,12 +1383,17 @@ message SubscribeBluetoothLEAdvertisementsRequest {
|
||||
uint32 flags = 1;
|
||||
}
|
||||
|
||||
// Deprecated - only used by deprecated BluetoothLEAdvertisementResponse
|
||||
message BluetoothServiceData {
|
||||
option deprecated = true;
|
||||
string uuid = 1;
|
||||
repeated uint32 legacy_data = 2 [deprecated = true]; // Removed in api version 1.7
|
||||
// Deprecated in API version 1.7
|
||||
repeated uint32 legacy_data = 2 [deprecated=true]; // Removed in api version 1.7
|
||||
bytes data = 3; // Added in api version 1.7
|
||||
}
|
||||
// Removed in ESPHome 2025.8.0 - use BluetoothLERawAdvertisementsResponse instead
|
||||
message BluetoothLEAdvertisementResponse {
|
||||
option deprecated = true;
|
||||
option (id) = 67;
|
||||
option (source) = SOURCE_SERVER;
|
||||
option (ifdef) = "USE_BLUETOOTH_PROXY";
|
||||
@ -1381,7 +1415,7 @@ message BluetoothLERawAdvertisement {
|
||||
sint32 rssi = 2;
|
||||
uint32 address_type = 3;
|
||||
|
||||
bytes data = 4;
|
||||
bytes data = 4 [(fixed_array_size) = 62];
|
||||
}
|
||||
|
||||
message BluetoothLERawAdvertisementsResponse {
|
||||
@ -1434,19 +1468,19 @@ message BluetoothGATTGetServicesRequest {
|
||||
}
|
||||
|
||||
message BluetoothGATTDescriptor {
|
||||
repeated uint64 uuid = 1;
|
||||
repeated uint64 uuid = 1 [(fixed_array_size) = 2];
|
||||
uint32 handle = 2;
|
||||
}
|
||||
|
||||
message BluetoothGATTCharacteristic {
|
||||
repeated uint64 uuid = 1;
|
||||
repeated uint64 uuid = 1 [(fixed_array_size) = 2];
|
||||
uint32 handle = 2;
|
||||
uint32 properties = 3;
|
||||
repeated BluetoothGATTDescriptor descriptors = 4;
|
||||
}
|
||||
|
||||
message BluetoothGATTService {
|
||||
repeated uint64 uuid = 1;
|
||||
repeated uint64 uuid = 1 [(fixed_array_size) = 2];
|
||||
uint32 handle = 2;
|
||||
repeated BluetoothGATTCharacteristic characteristics = 3;
|
||||
}
|
||||
@ -1457,7 +1491,7 @@ message BluetoothGATTGetServicesResponse {
|
||||
option (ifdef) = "USE_BLUETOOTH_PROXY";
|
||||
|
||||
uint64 address = 1;
|
||||
repeated BluetoothGATTService services = 2;
|
||||
repeated BluetoothGATTService services = 2 [(fixed_array_size) = 1];
|
||||
}
|
||||
|
||||
message BluetoothGATTGetServicesDoneResponse {
|
||||
|
@ -1,5 +1,11 @@
|
||||
#include "api_connection.h"
|
||||
#ifdef USE_API
|
||||
#ifdef USE_API_NOISE
|
||||
#include "api_frame_helper_noise.h"
|
||||
#endif
|
||||
#ifdef USE_API_PLAINTEXT
|
||||
#include "api_frame_helper_plaintext.h"
|
||||
#endif
|
||||
#include <cerrno>
|
||||
#include <cinttypes>
|
||||
#include <utility>
|
||||
@ -25,8 +31,7 @@
|
||||
#include "esphome/components/voice_assistant/voice_assistant.h"
|
||||
#endif
|
||||
|
||||
namespace esphome {
|
||||
namespace api {
|
||||
namespace esphome::api {
|
||||
|
||||
// Read a maximum of 5 messages per loop iteration to prevent starving other components.
|
||||
// This is a balance between API responsiveness and allowing other components to run.
|
||||
@ -79,14 +84,16 @@ APIConnection::APIConnection(std::unique_ptr<socket::Socket> sock, APIServer *pa
|
||||
#if defined(USE_API_PLAINTEXT) && defined(USE_API_NOISE)
|
||||
auto noise_ctx = parent->get_noise_ctx();
|
||||
if (noise_ctx->has_psk()) {
|
||||
this->helper_ = std::unique_ptr<APIFrameHelper>{new APINoiseFrameHelper(std::move(sock), noise_ctx)};
|
||||
this->helper_ =
|
||||
std::unique_ptr<APIFrameHelper>{new APINoiseFrameHelper(std::move(sock), noise_ctx, &this->client_info_)};
|
||||
} else {
|
||||
this->helper_ = std::unique_ptr<APIFrameHelper>{new APIPlaintextFrameHelper(std::move(sock))};
|
||||
this->helper_ = std::unique_ptr<APIFrameHelper>{new APIPlaintextFrameHelper(std::move(sock), &this->client_info_)};
|
||||
}
|
||||
#elif defined(USE_API_PLAINTEXT)
|
||||
this->helper_ = std::unique_ptr<APIFrameHelper>{new APIPlaintextFrameHelper(std::move(sock))};
|
||||
this->helper_ = std::unique_ptr<APIFrameHelper>{new APIPlaintextFrameHelper(std::move(sock), &this->client_info_)};
|
||||
#elif defined(USE_API_NOISE)
|
||||
this->helper_ = std::unique_ptr<APIFrameHelper>{new APINoiseFrameHelper(std::move(sock), parent->get_noise_ctx())};
|
||||
this->helper_ = std::unique_ptr<APIFrameHelper>{
|
||||
new APINoiseFrameHelper(std::move(sock), parent->get_noise_ctx(), &this->client_info_)};
|
||||
#else
|
||||
#error "No frame helper defined"
|
||||
#endif
|
||||
@ -109,9 +116,8 @@ void APIConnection::start() {
|
||||
errno);
|
||||
return;
|
||||
}
|
||||
this->client_info_ = helper_->getpeername();
|
||||
this->client_peername_ = this->client_info_;
|
||||
this->helper_->set_log_info(this->client_info_);
|
||||
this->client_info_.peername = helper_->getpeername();
|
||||
this->client_info_.name = this->client_info_.peername;
|
||||
}
|
||||
|
||||
APIConnection::~APIConnection() {
|
||||
@ -218,24 +224,16 @@ void APIConnection::loop() {
|
||||
if (this->image_reader_ && this->image_reader_->available() && this->helper_->can_write_without_blocking()) {
|
||||
uint32_t to_send = std::min((size_t) MAX_BATCH_PACKET_SIZE, this->image_reader_->available());
|
||||
bool done = this->image_reader_->available() == to_send;
|
||||
uint32_t msg_size = 0;
|
||||
ProtoSize::add_fixed_field<4>(msg_size, 1, true);
|
||||
// partial message size calculated manually since its a special case
|
||||
// 1 for the data field, varint for the data size, and the data itself
|
||||
msg_size += 1 + ProtoSize::varint(to_send) + to_send;
|
||||
ProtoSize::add_bool_field(msg_size, 1, done);
|
||||
|
||||
auto buffer = this->create_buffer(msg_size);
|
||||
// fixed32 key = 1;
|
||||
buffer.encode_fixed32(1, camera::Camera::instance()->get_object_id_hash());
|
||||
// bytes data = 2;
|
||||
buffer.encode_bytes(2, this->image_reader_->peek_data_buffer(), to_send);
|
||||
// bool done = 3;
|
||||
buffer.encode_bool(3, done);
|
||||
CameraImageResponse msg;
|
||||
msg.key = camera::Camera::instance()->get_object_id_hash();
|
||||
msg.set_data(this->image_reader_->peek_data_buffer(), to_send);
|
||||
msg.done = done;
|
||||
#ifdef USE_DEVICES
|
||||
msg.device_id = camera::Camera::instance()->get_device_id();
|
||||
#endif
|
||||
|
||||
bool success = this->send_buffer(buffer, CameraImageResponse::MESSAGE_TYPE);
|
||||
|
||||
if (success) {
|
||||
if (this->send_message_(msg, CameraImageResponse::MESSAGE_TYPE)) {
|
||||
this->image_reader_->consume_data(to_send);
|
||||
if (done) {
|
||||
this->image_reader_->return_image();
|
||||
@ -244,31 +242,21 @@ void APIConnection::loop() {
|
||||
}
|
||||
#endif
|
||||
|
||||
#ifdef USE_API_HOMEASSISTANT_STATES
|
||||
if (state_subs_at_ >= 0) {
|
||||
const auto &subs = this->parent_->get_state_subs();
|
||||
if (state_subs_at_ < static_cast<int>(subs.size())) {
|
||||
auto &it = subs[state_subs_at_];
|
||||
SubscribeHomeAssistantStateResponse resp;
|
||||
resp.entity_id = it.entity_id;
|
||||
resp.attribute = it.attribute.value();
|
||||
resp.once = it.once;
|
||||
if (this->send_message(resp, SubscribeHomeAssistantStateResponse::MESSAGE_TYPE)) {
|
||||
state_subs_at_++;
|
||||
}
|
||||
} else {
|
||||
state_subs_at_ = -1;
|
||||
}
|
||||
this->process_state_subscriptions_();
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
DisconnectResponse APIConnection::disconnect(const DisconnectRequest &msg) {
|
||||
bool APIConnection::send_disconnect_response(const DisconnectRequest &msg) {
|
||||
// remote initiated disconnect_client
|
||||
// don't close yet, we still need to send the disconnect response
|
||||
// close will happen on next loop
|
||||
ESP_LOGD(TAG, "%s disconnected", this->get_client_combined_info().c_str());
|
||||
this->flags_.next_close = true;
|
||||
DisconnectResponse resp;
|
||||
return resp;
|
||||
return this->send_message(resp, DisconnectResponse::MESSAGE_TYPE);
|
||||
}
|
||||
void APIConnection::on_disconnect_response(const DisconnectResponse &value) {
|
||||
this->helper_->close();
|
||||
@ -345,7 +333,7 @@ uint16_t APIConnection::try_send_binary_sensor_info(EntityBase *entity, APIConne
|
||||
bool is_single) {
|
||||
auto *binary_sensor = static_cast<binary_sensor::BinarySensor *>(entity);
|
||||
ListEntitiesBinarySensorResponse msg;
|
||||
msg.device_class = binary_sensor->get_device_class();
|
||||
msg.set_device_class(binary_sensor->get_device_class_ref());
|
||||
msg.is_status_binary_sensor = binary_sensor->is_status_binary_sensor();
|
||||
return fill_and_encode_entity_info(binary_sensor, msg, ListEntitiesBinarySensorResponse::MESSAGE_TYPE, conn,
|
||||
remaining_size, is_single);
|
||||
@ -362,8 +350,6 @@ uint16_t APIConnection::try_send_cover_state(EntityBase *entity, APIConnection *
|
||||
auto *cover = static_cast<cover::Cover *>(entity);
|
||||
CoverStateResponse msg;
|
||||
auto traits = cover->get_traits();
|
||||
msg.legacy_state =
|
||||
(cover->position == cover::COVER_OPEN) ? enums::LEGACY_COVER_STATE_OPEN : enums::LEGACY_COVER_STATE_CLOSED;
|
||||
msg.position = cover->position;
|
||||
if (traits.get_supports_tilt())
|
||||
msg.tilt = cover->tilt;
|
||||
@ -379,25 +365,12 @@ uint16_t APIConnection::try_send_cover_info(EntityBase *entity, APIConnection *c
|
||||
msg.supports_position = traits.get_supports_position();
|
||||
msg.supports_tilt = traits.get_supports_tilt();
|
||||
msg.supports_stop = traits.get_supports_stop();
|
||||
msg.device_class = cover->get_device_class();
|
||||
msg.set_device_class(cover->get_device_class_ref());
|
||||
return fill_and_encode_entity_info(cover, msg, ListEntitiesCoverResponse::MESSAGE_TYPE, conn, remaining_size,
|
||||
is_single);
|
||||
}
|
||||
void APIConnection::cover_command(const CoverCommandRequest &msg) {
|
||||
ENTITY_COMMAND_MAKE_CALL(cover::Cover, cover, cover)
|
||||
if (msg.has_legacy_command) {
|
||||
switch (msg.legacy_command) {
|
||||
case enums::LEGACY_COVER_COMMAND_OPEN:
|
||||
call.set_command_open();
|
||||
break;
|
||||
case enums::LEGACY_COVER_COMMAND_CLOSE:
|
||||
call.set_command_close();
|
||||
break;
|
||||
case enums::LEGACY_COVER_COMMAND_STOP:
|
||||
call.set_command_stop();
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (msg.has_position)
|
||||
call.set_position(msg.position);
|
||||
if (msg.has_tilt)
|
||||
@ -427,7 +400,7 @@ uint16_t APIConnection::try_send_fan_state(EntityBase *entity, APIConnection *co
|
||||
if (traits.supports_direction())
|
||||
msg.direction = static_cast<enums::FanDirection>(fan->direction);
|
||||
if (traits.supports_preset_modes())
|
||||
msg.preset_mode = fan->preset_mode;
|
||||
msg.set_preset_mode(StringRef(fan->preset_mode));
|
||||
return fill_and_encode_entity_state(fan, msg, FanStateResponse::MESSAGE_TYPE, conn, remaining_size, is_single);
|
||||
}
|
||||
uint16_t APIConnection::try_send_fan_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size,
|
||||
@ -484,8 +457,11 @@ uint16_t APIConnection::try_send_light_state(EntityBase *entity, APIConnection *
|
||||
resp.color_temperature = values.get_color_temperature();
|
||||
resp.cold_white = values.get_cold_white();
|
||||
resp.warm_white = values.get_warm_white();
|
||||
if (light->supports_effects())
|
||||
resp.effect = light->get_effect_name();
|
||||
if (light->supports_effects()) {
|
||||
// get_effect_name() returns temporary std::string - must store it
|
||||
std::string effect_name = light->get_effect_name();
|
||||
resp.set_effect(StringRef(effect_name));
|
||||
}
|
||||
return fill_and_encode_entity_state(light, resp, LightStateResponse::MESSAGE_TYPE, conn, remaining_size, is_single);
|
||||
}
|
||||
uint16_t APIConnection::try_send_light_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size,
|
||||
@ -495,14 +471,8 @@ uint16_t APIConnection::try_send_light_info(EntityBase *entity, APIConnection *c
|
||||
auto traits = light->get_traits();
|
||||
for (auto mode : traits.get_supported_color_modes())
|
||||
msg.supported_color_modes.push_back(static_cast<enums::ColorMode>(mode));
|
||||
msg.legacy_supports_brightness = traits.supports_color_capability(light::ColorCapability::BRIGHTNESS);
|
||||
msg.legacy_supports_rgb = traits.supports_color_capability(light::ColorCapability::RGB);
|
||||
msg.legacy_supports_white_value =
|
||||
msg.legacy_supports_rgb && (traits.supports_color_capability(light::ColorCapability::WHITE) ||
|
||||
traits.supports_color_capability(light::ColorCapability::COLD_WARM_WHITE));
|
||||
msg.legacy_supports_color_temperature = traits.supports_color_capability(light::ColorCapability::COLOR_TEMPERATURE) ||
|
||||
traits.supports_color_capability(light::ColorCapability::COLD_WARM_WHITE);
|
||||
if (msg.legacy_supports_color_temperature) {
|
||||
if (traits.supports_color_capability(light::ColorCapability::COLOR_TEMPERATURE) ||
|
||||
traits.supports_color_capability(light::ColorCapability::COLD_WARM_WHITE)) {
|
||||
msg.min_mireds = traits.get_min_mireds();
|
||||
msg.max_mireds = traits.get_max_mireds();
|
||||
}
|
||||
@ -567,10 +537,10 @@ uint16_t APIConnection::try_send_sensor_info(EntityBase *entity, APIConnection *
|
||||
bool is_single) {
|
||||
auto *sensor = static_cast<sensor::Sensor *>(entity);
|
||||
ListEntitiesSensorResponse msg;
|
||||
msg.unit_of_measurement = sensor->get_unit_of_measurement();
|
||||
msg.set_unit_of_measurement(sensor->get_unit_of_measurement_ref());
|
||||
msg.accuracy_decimals = sensor->get_accuracy_decimals();
|
||||
msg.force_update = sensor->get_force_update();
|
||||
msg.device_class = sensor->get_device_class();
|
||||
msg.set_device_class(sensor->get_device_class_ref());
|
||||
msg.state_class = static_cast<enums::SensorStateClass>(sensor->get_state_class());
|
||||
return fill_and_encode_entity_info(sensor, msg, ListEntitiesSensorResponse::MESSAGE_TYPE, conn, remaining_size,
|
||||
is_single);
|
||||
@ -597,7 +567,7 @@ uint16_t APIConnection::try_send_switch_info(EntityBase *entity, APIConnection *
|
||||
auto *a_switch = static_cast<switch_::Switch *>(entity);
|
||||
ListEntitiesSwitchResponse msg;
|
||||
msg.assumed_state = a_switch->assumed_state();
|
||||
msg.device_class = a_switch->get_device_class();
|
||||
msg.set_device_class(a_switch->get_device_class_ref());
|
||||
return fill_and_encode_entity_info(a_switch, msg, ListEntitiesSwitchResponse::MESSAGE_TYPE, conn, remaining_size,
|
||||
is_single);
|
||||
}
|
||||
@ -622,7 +592,7 @@ uint16_t APIConnection::try_send_text_sensor_state(EntityBase *entity, APIConnec
|
||||
bool is_single) {
|
||||
auto *text_sensor = static_cast<text_sensor::TextSensor *>(entity);
|
||||
TextSensorStateResponse resp;
|
||||
resp.state = text_sensor->state;
|
||||
resp.set_state(StringRef(text_sensor->state));
|
||||
resp.missing_state = !text_sensor->has_state();
|
||||
return fill_and_encode_entity_state(text_sensor, resp, TextSensorStateResponse::MESSAGE_TYPE, conn, remaining_size,
|
||||
is_single);
|
||||
@ -631,7 +601,7 @@ uint16_t APIConnection::try_send_text_sensor_info(EntityBase *entity, APIConnect
|
||||
bool is_single) {
|
||||
auto *text_sensor = static_cast<text_sensor::TextSensor *>(entity);
|
||||
ListEntitiesTextSensorResponse msg;
|
||||
msg.device_class = text_sensor->get_device_class();
|
||||
msg.set_device_class(text_sensor->get_device_class_ref());
|
||||
return fill_and_encode_entity_info(text_sensor, msg, ListEntitiesTextSensorResponse::MESSAGE_TYPE, conn,
|
||||
remaining_size, is_single);
|
||||
}
|
||||
@ -659,13 +629,15 @@ uint16_t APIConnection::try_send_climate_state(EntityBase *entity, APIConnection
|
||||
}
|
||||
if (traits.get_supports_fan_modes() && climate->fan_mode.has_value())
|
||||
resp.fan_mode = static_cast<enums::ClimateFanMode>(climate->fan_mode.value());
|
||||
if (!traits.get_supported_custom_fan_modes().empty() && climate->custom_fan_mode.has_value())
|
||||
resp.custom_fan_mode = climate->custom_fan_mode.value();
|
||||
if (!traits.get_supported_custom_fan_modes().empty() && climate->custom_fan_mode.has_value()) {
|
||||
resp.set_custom_fan_mode(StringRef(climate->custom_fan_mode.value()));
|
||||
}
|
||||
if (traits.get_supports_presets() && climate->preset.has_value()) {
|
||||
resp.preset = static_cast<enums::ClimatePreset>(climate->preset.value());
|
||||
}
|
||||
if (!traits.get_supported_custom_presets().empty() && climate->custom_preset.has_value())
|
||||
resp.custom_preset = climate->custom_preset.value();
|
||||
if (!traits.get_supported_custom_presets().empty() && climate->custom_preset.has_value()) {
|
||||
resp.set_custom_preset(StringRef(climate->custom_preset.value()));
|
||||
}
|
||||
if (traits.get_supports_swing_modes())
|
||||
resp.swing_mode = static_cast<enums::ClimateSwingMode>(climate->swing_mode);
|
||||
if (traits.get_supports_current_humidity())
|
||||
@ -692,7 +664,6 @@ uint16_t APIConnection::try_send_climate_info(EntityBase *entity, APIConnection
|
||||
msg.visual_current_temperature_step = traits.get_visual_current_temperature_step();
|
||||
msg.visual_min_humidity = traits.get_visual_min_humidity();
|
||||
msg.visual_max_humidity = traits.get_visual_max_humidity();
|
||||
msg.legacy_supports_away = traits.supports_preset(climate::CLIMATE_PRESET_AWAY);
|
||||
msg.supports_action = traits.get_supports_action();
|
||||
for (auto fan_mode : traits.get_supported_fan_modes())
|
||||
msg.supported_fan_modes.push_back(static_cast<enums::ClimateFanMode>(fan_mode));
|
||||
@ -752,9 +723,9 @@ uint16_t APIConnection::try_send_number_info(EntityBase *entity, APIConnection *
|
||||
bool is_single) {
|
||||
auto *number = static_cast<number::Number *>(entity);
|
||||
ListEntitiesNumberResponse msg;
|
||||
msg.unit_of_measurement = number->traits.get_unit_of_measurement();
|
||||
msg.set_unit_of_measurement(number->traits.get_unit_of_measurement_ref());
|
||||
msg.mode = static_cast<enums::NumberMode>(number->traits.get_mode());
|
||||
msg.device_class = number->traits.get_device_class();
|
||||
msg.set_device_class(number->traits.get_device_class_ref());
|
||||
msg.min_value = number->traits.get_min_value();
|
||||
msg.max_value = number->traits.get_max_value();
|
||||
msg.step = number->traits.get_step();
|
||||
@ -867,7 +838,7 @@ uint16_t APIConnection::try_send_text_state(EntityBase *entity, APIConnection *c
|
||||
bool is_single) {
|
||||
auto *text = static_cast<text::Text *>(entity);
|
||||
TextStateResponse resp;
|
||||
resp.state = text->state;
|
||||
resp.set_state(StringRef(text->state));
|
||||
resp.missing_state = !text->has_state();
|
||||
return fill_and_encode_entity_state(text, resp, TextStateResponse::MESSAGE_TYPE, conn, remaining_size, is_single);
|
||||
}
|
||||
@ -879,7 +850,7 @@ uint16_t APIConnection::try_send_text_info(EntityBase *entity, APIConnection *co
|
||||
msg.mode = static_cast<enums::TextMode>(text->traits.get_mode());
|
||||
msg.min_length = text->traits.get_min_length();
|
||||
msg.max_length = text->traits.get_max_length();
|
||||
msg.pattern = text->traits.get_pattern();
|
||||
msg.set_pattern(text->traits.get_pattern_ref());
|
||||
return fill_and_encode_entity_info(text, msg, ListEntitiesTextResponse::MESSAGE_TYPE, conn, remaining_size,
|
||||
is_single);
|
||||
}
|
||||
@ -900,7 +871,7 @@ uint16_t APIConnection::try_send_select_state(EntityBase *entity, APIConnection
|
||||
bool is_single) {
|
||||
auto *select = static_cast<select::Select *>(entity);
|
||||
SelectStateResponse resp;
|
||||
resp.state = select->state;
|
||||
resp.set_state(StringRef(select->state));
|
||||
resp.missing_state = !select->has_state();
|
||||
return fill_and_encode_entity_state(select, resp, SelectStateResponse::MESSAGE_TYPE, conn, remaining_size, is_single);
|
||||
}
|
||||
@ -926,7 +897,7 @@ uint16_t APIConnection::try_send_button_info(EntityBase *entity, APIConnection *
|
||||
bool is_single) {
|
||||
auto *button = static_cast<button::Button *>(entity);
|
||||
ListEntitiesButtonResponse msg;
|
||||
msg.device_class = button->get_device_class();
|
||||
msg.set_device_class(button->get_device_class_ref());
|
||||
return fill_and_encode_entity_info(button, msg, ListEntitiesButtonResponse::MESSAGE_TYPE, conn, remaining_size,
|
||||
is_single);
|
||||
}
|
||||
@ -995,7 +966,7 @@ uint16_t APIConnection::try_send_valve_info(EntityBase *entity, APIConnection *c
|
||||
auto *valve = static_cast<valve::Valve *>(entity);
|
||||
ListEntitiesValveResponse msg;
|
||||
auto traits = valve->get_traits();
|
||||
msg.device_class = valve->get_device_class();
|
||||
msg.set_device_class(valve->get_device_class_ref());
|
||||
msg.assumed_state = traits.get_is_assumed_state();
|
||||
msg.supports_position = traits.get_supports_position();
|
||||
msg.supports_stop = traits.get_supports_stop();
|
||||
@ -1037,13 +1008,13 @@ uint16_t APIConnection::try_send_media_player_info(EntityBase *entity, APIConnec
|
||||
auto traits = media_player->get_traits();
|
||||
msg.supports_pause = traits.get_supports_pause();
|
||||
for (auto &supported_format : traits.get_supported_formats()) {
|
||||
MediaPlayerSupportedFormat media_format;
|
||||
media_format.format = supported_format.format;
|
||||
msg.supported_formats.emplace_back();
|
||||
auto &media_format = msg.supported_formats.back();
|
||||
media_format.set_format(StringRef(supported_format.format));
|
||||
media_format.sample_rate = supported_format.sample_rate;
|
||||
media_format.num_channels = supported_format.num_channels;
|
||||
media_format.purpose = static_cast<enums::MediaPlayerFormatPurpose>(supported_format.purpose);
|
||||
media_format.sample_bytes = supported_format.sample_bytes;
|
||||
msg.supported_formats.push_back(media_format);
|
||||
}
|
||||
return fill_and_encode_entity_info(media_player, msg, ListEntitiesMediaPlayerResponse::MESSAGE_TYPE, conn,
|
||||
remaining_size, is_single);
|
||||
@ -1106,6 +1077,12 @@ void APIConnection::on_get_time_response(const GetTimeResponse &value) {
|
||||
}
|
||||
#endif
|
||||
|
||||
bool APIConnection::send_get_time_response(const GetTimeRequest &msg) {
|
||||
GetTimeResponse resp;
|
||||
resp.epoch_seconds = ::time(nullptr);
|
||||
return this->send_message(resp, GetTimeResponse::MESSAGE_TYPE);
|
||||
}
|
||||
|
||||
#ifdef USE_BLUETOOTH_PROXY
|
||||
void APIConnection::subscribe_bluetooth_le_advertisements(const SubscribeBluetoothLEAdvertisementsRequest &msg) {
|
||||
bluetooth_proxy::global_bluetooth_proxy->subscribe_api_connection(this, msg.flags);
|
||||
@ -1113,21 +1090,6 @@ void APIConnection::subscribe_bluetooth_le_advertisements(const SubscribeBluetoo
|
||||
void APIConnection::unsubscribe_bluetooth_le_advertisements(const UnsubscribeBluetoothLEAdvertisementsRequest &msg) {
|
||||
bluetooth_proxy::global_bluetooth_proxy->unsubscribe_api_connection(this);
|
||||
}
|
||||
bool APIConnection::send_bluetooth_le_advertisement(const BluetoothLEAdvertisementResponse &msg) {
|
||||
if (this->client_api_version_major_ < 1 || this->client_api_version_minor_ < 7) {
|
||||
BluetoothLEAdvertisementResponse resp = msg;
|
||||
for (auto &service : resp.service_data) {
|
||||
service.legacy_data.assign(service.data.begin(), service.data.end());
|
||||
service.data.clear();
|
||||
}
|
||||
for (auto &manufacturer_data : resp.manufacturer_data) {
|
||||
manufacturer_data.legacy_data.assign(manufacturer_data.data.begin(), manufacturer_data.data.end());
|
||||
manufacturer_data.data.clear();
|
||||
}
|
||||
return this->send_message(resp, BluetoothLEAdvertisementResponse::MESSAGE_TYPE);
|
||||
}
|
||||
return this->send_message(msg, BluetoothLEAdvertisementResponse::MESSAGE_TYPE);
|
||||
}
|
||||
void APIConnection::bluetooth_device_request(const BluetoothDeviceRequest &msg) {
|
||||
bluetooth_proxy::global_bluetooth_proxy->bluetooth_device_request(msg);
|
||||
}
|
||||
@ -1151,12 +1113,12 @@ void APIConnection::bluetooth_gatt_notify(const BluetoothGATTNotifyRequest &msg)
|
||||
bluetooth_proxy::global_bluetooth_proxy->bluetooth_gatt_notify(msg);
|
||||
}
|
||||
|
||||
BluetoothConnectionsFreeResponse APIConnection::subscribe_bluetooth_connections_free(
|
||||
bool APIConnection::send_subscribe_bluetooth_connections_free_response(
|
||||
const SubscribeBluetoothConnectionsFreeRequest &msg) {
|
||||
BluetoothConnectionsFreeResponse resp;
|
||||
resp.free = bluetooth_proxy::global_bluetooth_proxy->get_bluetooth_connections_free();
|
||||
resp.limit = bluetooth_proxy::global_bluetooth_proxy->get_bluetooth_connections_limit();
|
||||
return resp;
|
||||
return this->send_message(resp, BluetoothConnectionsFreeResponse::MESSAGE_TYPE);
|
||||
}
|
||||
|
||||
void APIConnection::bluetooth_scanner_set_mode(const BluetoothScannerSetModeRequest &msg) {
|
||||
@ -1217,28 +1179,27 @@ void APIConnection::on_voice_assistant_announce_request(const VoiceAssistantAnno
|
||||
}
|
||||
}
|
||||
|
||||
VoiceAssistantConfigurationResponse APIConnection::voice_assistant_get_configuration(
|
||||
const VoiceAssistantConfigurationRequest &msg) {
|
||||
bool APIConnection::send_voice_assistant_get_configuration_response(const VoiceAssistantConfigurationRequest &msg) {
|
||||
VoiceAssistantConfigurationResponse resp;
|
||||
if (!this->check_voice_assistant_api_connection_()) {
|
||||
return resp;
|
||||
return this->send_message(resp, VoiceAssistantConfigurationResponse::MESSAGE_TYPE);
|
||||
}
|
||||
|
||||
auto &config = voice_assistant::global_voice_assistant->get_configuration();
|
||||
for (auto &wake_word : config.available_wake_words) {
|
||||
VoiceAssistantWakeWord resp_wake_word;
|
||||
resp_wake_word.id = wake_word.id;
|
||||
resp_wake_word.wake_word = wake_word.wake_word;
|
||||
resp.available_wake_words.emplace_back();
|
||||
auto &resp_wake_word = resp.available_wake_words.back();
|
||||
resp_wake_word.set_id(StringRef(wake_word.id));
|
||||
resp_wake_word.set_wake_word(StringRef(wake_word.wake_word));
|
||||
for (const auto &lang : wake_word.trained_languages) {
|
||||
resp_wake_word.trained_languages.push_back(lang);
|
||||
}
|
||||
resp.available_wake_words.push_back(std::move(resp_wake_word));
|
||||
}
|
||||
for (auto &wake_word_id : config.active_wake_words) {
|
||||
resp.active_wake_words.push_back(wake_word_id);
|
||||
}
|
||||
resp.max_active_wake_words = config.max_active_wake_words;
|
||||
return resp;
|
||||
return this->send_message(resp, VoiceAssistantConfigurationResponse::MESSAGE_TYPE);
|
||||
}
|
||||
|
||||
void APIConnection::voice_assistant_set_configuration(const VoiceAssistantSetConfiguration &msg) {
|
||||
@ -1311,7 +1272,7 @@ void APIConnection::send_event(event::Event *event, const std::string &event_typ
|
||||
uint16_t APIConnection::try_send_event_response(event::Event *event, const std::string &event_type, APIConnection *conn,
|
||||
uint32_t remaining_size, bool is_single) {
|
||||
EventResponse resp;
|
||||
resp.event_type = event_type;
|
||||
resp.set_event_type(StringRef(event_type));
|
||||
return fill_and_encode_entity_state(event, resp, EventResponse::MESSAGE_TYPE, conn, remaining_size, is_single);
|
||||
}
|
||||
|
||||
@ -1319,7 +1280,7 @@ uint16_t APIConnection::try_send_event_info(EntityBase *entity, APIConnection *c
|
||||
bool is_single) {
|
||||
auto *event = static_cast<event::Event *>(entity);
|
||||
ListEntitiesEventResponse msg;
|
||||
msg.device_class = event->get_device_class();
|
||||
msg.set_device_class(event->get_device_class_ref());
|
||||
for (const auto &event_type : event->get_event_types())
|
||||
msg.event_types.push_back(event_type);
|
||||
return fill_and_encode_entity_info(event, msg, ListEntitiesEventResponse::MESSAGE_TYPE, conn, remaining_size,
|
||||
@ -1343,11 +1304,11 @@ uint16_t APIConnection::try_send_update_state(EntityBase *entity, APIConnection
|
||||
resp.has_progress = true;
|
||||
resp.progress = update->update_info.progress;
|
||||
}
|
||||
resp.current_version = update->update_info.current_version;
|
||||
resp.latest_version = update->update_info.latest_version;
|
||||
resp.title = update->update_info.title;
|
||||
resp.release_summary = update->update_info.summary;
|
||||
resp.release_url = update->update_info.release_url;
|
||||
resp.set_current_version(StringRef(update->update_info.current_version));
|
||||
resp.set_latest_version(StringRef(update->update_info.latest_version));
|
||||
resp.set_title(StringRef(update->update_info.title));
|
||||
resp.set_release_summary(StringRef(update->update_info.summary));
|
||||
resp.set_release_url(StringRef(update->update_info.release_url));
|
||||
}
|
||||
return fill_and_encode_entity_state(update, resp, UpdateStateResponse::MESSAGE_TYPE, conn, remaining_size, is_single);
|
||||
}
|
||||
@ -1355,7 +1316,7 @@ uint16_t APIConnection::try_send_update_info(EntityBase *entity, APIConnection *
|
||||
bool is_single) {
|
||||
auto *update = static_cast<update::UpdateEntity *>(entity);
|
||||
ListEntitiesUpdateResponse msg;
|
||||
msg.device_class = update->get_device_class();
|
||||
msg.set_device_class(update->get_device_class_ref());
|
||||
return fill_and_encode_entity_info(update, msg, ListEntitiesUpdateResponse::MESSAGE_TYPE, conn, remaining_size,
|
||||
is_single);
|
||||
}
|
||||
@ -1380,26 +1341,10 @@ void APIConnection::update_command(const UpdateCommandRequest &msg) {
|
||||
#endif
|
||||
|
||||
bool APIConnection::try_send_log_message(int level, const char *tag, const char *line, size_t message_len) {
|
||||
// Pre-calculate message size to avoid reallocations
|
||||
uint32_t msg_size = 0;
|
||||
|
||||
// Add size for level field (field ID 1, varint type)
|
||||
// 1 byte for field tag + size of the level varint
|
||||
msg_size += 1 + api::ProtoSize::varint(static_cast<uint32_t>(level));
|
||||
|
||||
// Add size for string field (field ID 3, string type)
|
||||
// 1 byte for field tag + size of length varint + string length
|
||||
msg_size += 1 + api::ProtoSize::varint(static_cast<uint32_t>(message_len)) + message_len;
|
||||
|
||||
// Create a pre-sized buffer
|
||||
auto buffer = this->create_buffer(msg_size);
|
||||
|
||||
// Encode the message (SubscribeLogsResponse)
|
||||
buffer.encode_uint32(1, static_cast<uint32_t>(level)); // LogLevel level = 1
|
||||
buffer.encode_string(3, line, message_len); // string message = 3
|
||||
|
||||
// SubscribeLogsResponse - 29
|
||||
return this->send_buffer(buffer, SubscribeLogsResponse::MESSAGE_TYPE);
|
||||
SubscribeLogsResponse msg;
|
||||
msg.level = static_cast<enums::LogLevel>(level);
|
||||
msg.set_message(reinterpret_cast<const uint8_t *>(line), message_len);
|
||||
return this->send_message_(msg, SubscribeLogsResponse::MESSAGE_TYPE);
|
||||
}
|
||||
|
||||
void APIConnection::complete_authentication_() {
|
||||
@ -1411,7 +1356,7 @@ void APIConnection::complete_authentication_() {
|
||||
this->flags_.connection_state = static_cast<uint8_t>(ConnectionState::AUTHENTICATED);
|
||||
ESP_LOGD(TAG, "%s connected", this->get_client_combined_info().c_str());
|
||||
#ifdef USE_API_CLIENT_CONNECTED_TRIGGER
|
||||
this->parent_->get_client_connected_trigger()->trigger(this->client_info_, this->client_peername_);
|
||||
this->parent_->get_client_connected_trigger()->trigger(this->client_info_.name, this->client_info_.peername);
|
||||
#endif
|
||||
#ifdef USE_HOMEASSISTANT_TIME
|
||||
if (homeassistant::global_homeassistant_time != nullptr) {
|
||||
@ -1420,20 +1365,21 @@ void APIConnection::complete_authentication_() {
|
||||
#endif
|
||||
}
|
||||
|
||||
HelloResponse APIConnection::hello(const HelloRequest &msg) {
|
||||
this->client_info_ = msg.client_info;
|
||||
this->client_peername_ = this->helper_->getpeername();
|
||||
this->helper_->set_log_info(this->get_client_combined_info());
|
||||
bool APIConnection::send_hello_response(const HelloRequest &msg) {
|
||||
this->client_info_.name = msg.client_info;
|
||||
this->client_info_.peername = this->helper_->getpeername();
|
||||
this->client_api_version_major_ = msg.api_version_major;
|
||||
this->client_api_version_minor_ = msg.api_version_minor;
|
||||
ESP_LOGV(TAG, "Hello from client: '%s' | %s | API Version %" PRIu32 ".%" PRIu32, this->client_info_.c_str(),
|
||||
this->client_peername_.c_str(), this->client_api_version_major_, this->client_api_version_minor_);
|
||||
ESP_LOGV(TAG, "Hello from client: '%s' | %s | API Version %" PRIu32 ".%" PRIu32, this->client_info_.name.c_str(),
|
||||
this->client_info_.peername.c_str(), this->client_api_version_major_, this->client_api_version_minor_);
|
||||
|
||||
HelloResponse resp;
|
||||
resp.api_version_major = 1;
|
||||
resp.api_version_minor = 10;
|
||||
resp.server_info = App.get_name() + " (esphome v" ESPHOME_VERSION ")";
|
||||
resp.name = App.get_name();
|
||||
// Temporary string for concatenation - will be valid during send_message call
|
||||
std::string server_info = App.get_name() + " (esphome v" ESPHOME_VERSION ")";
|
||||
resp.set_server_info(StringRef(server_info));
|
||||
resp.set_name(StringRef(App.get_name()));
|
||||
|
||||
#ifdef USE_API_PASSWORD
|
||||
// Password required - wait for authentication
|
||||
@ -1443,9 +1389,9 @@ HelloResponse APIConnection::hello(const HelloRequest &msg) {
|
||||
this->complete_authentication_();
|
||||
#endif
|
||||
|
||||
return resp;
|
||||
return this->send_message(resp, HelloResponse::MESSAGE_TYPE);
|
||||
}
|
||||
ConnectResponse APIConnection::connect(const ConnectRequest &msg) {
|
||||
bool APIConnection::send_connect_response(const ConnectRequest &msg) {
|
||||
bool correct = true;
|
||||
#ifdef USE_API_PASSWORD
|
||||
correct = this->parent_->check_password(msg.password);
|
||||
@ -1457,54 +1403,73 @@ ConnectResponse APIConnection::connect(const ConnectRequest &msg) {
|
||||
if (correct) {
|
||||
this->complete_authentication_();
|
||||
}
|
||||
return resp;
|
||||
return this->send_message(resp, ConnectResponse::MESSAGE_TYPE);
|
||||
}
|
||||
DeviceInfoResponse APIConnection::device_info(const DeviceInfoRequest &msg) {
|
||||
|
||||
bool APIConnection::send_ping_response(const PingRequest &msg) {
|
||||
PingResponse resp;
|
||||
return this->send_message(resp, PingResponse::MESSAGE_TYPE);
|
||||
}
|
||||
|
||||
bool APIConnection::send_device_info_response(const DeviceInfoRequest &msg) {
|
||||
DeviceInfoResponse resp{};
|
||||
#ifdef USE_API_PASSWORD
|
||||
resp.uses_password = true;
|
||||
#else
|
||||
resp.uses_password = false;
|
||||
#endif
|
||||
resp.name = App.get_name();
|
||||
resp.friendly_name = App.get_friendly_name();
|
||||
resp.set_name(StringRef(App.get_name()));
|
||||
resp.set_friendly_name(StringRef(App.get_friendly_name()));
|
||||
#ifdef USE_AREAS
|
||||
resp.suggested_area = App.get_area();
|
||||
resp.set_suggested_area(StringRef(App.get_area()));
|
||||
#endif
|
||||
resp.mac_address = get_mac_address_pretty();
|
||||
resp.esphome_version = ESPHOME_VERSION;
|
||||
resp.compilation_time = App.get_compilation_time();
|
||||
// mac_address must store temporary string - will be valid during send_message call
|
||||
std::string mac_address = get_mac_address_pretty();
|
||||
resp.set_mac_address(StringRef(mac_address));
|
||||
|
||||
// Compile-time StringRef constants
|
||||
static constexpr auto ESPHOME_VERSION_REF = StringRef::from_lit(ESPHOME_VERSION);
|
||||
resp.set_esphome_version(ESPHOME_VERSION_REF);
|
||||
|
||||
// get_compilation_time() returns temporary std::string - must store it
|
||||
std::string compilation_time = App.get_compilation_time();
|
||||
resp.set_compilation_time(StringRef(compilation_time));
|
||||
|
||||
// Compile-time StringRef constants for manufacturers
|
||||
#if defined(USE_ESP8266) || defined(USE_ESP32)
|
||||
resp.manufacturer = "Espressif";
|
||||
static constexpr auto MANUFACTURER = StringRef::from_lit("Espressif");
|
||||
#elif defined(USE_RP2040)
|
||||
resp.manufacturer = "Raspberry Pi";
|
||||
static constexpr auto MANUFACTURER = StringRef::from_lit("Raspberry Pi");
|
||||
#elif defined(USE_BK72XX)
|
||||
resp.manufacturer = "Beken";
|
||||
static constexpr auto MANUFACTURER = StringRef::from_lit("Beken");
|
||||
#elif defined(USE_LN882X)
|
||||
resp.manufacturer = "Lightning";
|
||||
static constexpr auto MANUFACTURER = StringRef::from_lit("Lightning");
|
||||
#elif defined(USE_RTL87XX)
|
||||
resp.manufacturer = "Realtek";
|
||||
static constexpr auto MANUFACTURER = StringRef::from_lit("Realtek");
|
||||
#elif defined(USE_HOST)
|
||||
resp.manufacturer = "Host";
|
||||
static constexpr auto MANUFACTURER = StringRef::from_lit("Host");
|
||||
#endif
|
||||
resp.model = ESPHOME_BOARD;
|
||||
resp.set_manufacturer(MANUFACTURER);
|
||||
|
||||
static constexpr auto MODEL = StringRef::from_lit(ESPHOME_BOARD);
|
||||
resp.set_model(MODEL);
|
||||
#ifdef USE_DEEP_SLEEP
|
||||
resp.has_deep_sleep = deep_sleep::global_has_deep_sleep;
|
||||
#endif
|
||||
#ifdef ESPHOME_PROJECT_NAME
|
||||
resp.project_name = ESPHOME_PROJECT_NAME;
|
||||
resp.project_version = ESPHOME_PROJECT_VERSION;
|
||||
static constexpr auto PROJECT_NAME = StringRef::from_lit(ESPHOME_PROJECT_NAME);
|
||||
static constexpr auto PROJECT_VERSION = StringRef::from_lit(ESPHOME_PROJECT_VERSION);
|
||||
resp.set_project_name(PROJECT_NAME);
|
||||
resp.set_project_version(PROJECT_VERSION);
|
||||
#endif
|
||||
#ifdef USE_WEBSERVER
|
||||
resp.webserver_port = USE_WEBSERVER_PORT;
|
||||
#endif
|
||||
#ifdef USE_BLUETOOTH_PROXY
|
||||
resp.legacy_bluetooth_proxy_version = bluetooth_proxy::global_bluetooth_proxy->get_legacy_version();
|
||||
resp.bluetooth_proxy_feature_flags = bluetooth_proxy::global_bluetooth_proxy->get_feature_flags();
|
||||
resp.bluetooth_mac_address = bluetooth_proxy::global_bluetooth_proxy->get_bluetooth_mac_address_pretty();
|
||||
// bt_mac must store temporary string - will be valid during send_message call
|
||||
std::string bluetooth_mac = bluetooth_proxy::global_bluetooth_proxy->get_bluetooth_mac_address_pretty();
|
||||
resp.set_bluetooth_mac_address(StringRef(bluetooth_mac));
|
||||
#endif
|
||||
#ifdef USE_VOICE_ASSISTANT
|
||||
resp.legacy_voice_assistant_version = voice_assistant::global_voice_assistant->get_legacy_version();
|
||||
resp.voice_assistant_feature_flags = voice_assistant::global_voice_assistant->get_feature_flags();
|
||||
#endif
|
||||
#ifdef USE_API_NOISE
|
||||
@ -1512,23 +1477,26 @@ DeviceInfoResponse APIConnection::device_info(const DeviceInfoRequest &msg) {
|
||||
#endif
|
||||
#ifdef USE_DEVICES
|
||||
for (auto const &device : App.get_devices()) {
|
||||
DeviceInfo device_info;
|
||||
resp.devices.emplace_back();
|
||||
auto &device_info = resp.devices.back();
|
||||
device_info.device_id = device->get_device_id();
|
||||
device_info.name = device->get_name();
|
||||
device_info.set_name(StringRef(device->get_name()));
|
||||
device_info.area_id = device->get_area_id();
|
||||
resp.devices.push_back(device_info);
|
||||
}
|
||||
#endif
|
||||
#ifdef USE_AREAS
|
||||
for (auto const &area : App.get_areas()) {
|
||||
AreaInfo area_info;
|
||||
resp.areas.emplace_back();
|
||||
auto &area_info = resp.areas.back();
|
||||
area_info.area_id = area->get_area_id();
|
||||
area_info.name = area->get_name();
|
||||
resp.areas.push_back(area_info);
|
||||
area_info.set_name(StringRef(area->get_name()));
|
||||
}
|
||||
#endif
|
||||
return resp;
|
||||
|
||||
return this->send_message(resp, DeviceInfoResponse::MESSAGE_TYPE);
|
||||
}
|
||||
|
||||
#ifdef USE_API_HOMEASSISTANT_STATES
|
||||
void APIConnection::on_home_assistant_state_response(const HomeAssistantStateResponse &msg) {
|
||||
for (auto &it : this->parent_->get_state_subs()) {
|
||||
if (it.entity_id == msg.entity_id && it.attribute.value() == msg.attribute) {
|
||||
@ -1536,6 +1504,7 @@ void APIConnection::on_home_assistant_state_response(const HomeAssistantStateRes
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
#ifdef USE_API_SERVICES
|
||||
void APIConnection::execute_service(const ExecuteServiceRequest &msg) {
|
||||
bool found = false;
|
||||
@ -1550,28 +1519,27 @@ void APIConnection::execute_service(const ExecuteServiceRequest &msg) {
|
||||
}
|
||||
#endif
|
||||
#ifdef USE_API_NOISE
|
||||
NoiseEncryptionSetKeyResponse APIConnection::noise_encryption_set_key(const NoiseEncryptionSetKeyRequest &msg) {
|
||||
psk_t psk{};
|
||||
bool APIConnection::send_noise_encryption_set_key_response(const NoiseEncryptionSetKeyRequest &msg) {
|
||||
NoiseEncryptionSetKeyResponse resp;
|
||||
resp.success = false;
|
||||
|
||||
psk_t psk{};
|
||||
if (base64_decode(msg.key, psk.data(), msg.key.size()) != psk.size()) {
|
||||
ESP_LOGW(TAG, "Invalid encryption key length");
|
||||
resp.success = false;
|
||||
return resp;
|
||||
}
|
||||
|
||||
if (!this->parent_->save_noise_psk(psk, true)) {
|
||||
} else if (!this->parent_->save_noise_psk(psk, true)) {
|
||||
ESP_LOGW(TAG, "Failed to save encryption key");
|
||||
resp.success = false;
|
||||
return resp;
|
||||
} else {
|
||||
resp.success = true;
|
||||
}
|
||||
|
||||
resp.success = true;
|
||||
return resp;
|
||||
return this->send_message(resp, NoiseEncryptionSetKeyResponse::MESSAGE_TYPE);
|
||||
}
|
||||
#endif
|
||||
#ifdef USE_API_HOMEASSISTANT_STATES
|
||||
void APIConnection::subscribe_home_assistant_states(const SubscribeHomeAssistantStatesRequest &msg) {
|
||||
state_subs_at_ = 0;
|
||||
}
|
||||
#endif
|
||||
bool APIConnection::try_to_clear_buffer(bool log_out_of_space) {
|
||||
if (this->flags_.remove)
|
||||
return false;
|
||||
@ -1609,10 +1577,12 @@ bool APIConnection::send_buffer(ProtoWriteBuffer buffer, uint8_t message_type) {
|
||||
// Do not set last_traffic_ on send
|
||||
return true;
|
||||
}
|
||||
#ifdef USE_API_PASSWORD
|
||||
void APIConnection::on_unauthenticated_access() {
|
||||
this->on_fatal_error();
|
||||
ESP_LOGD(TAG, "%s access without authentication", this->get_client_combined_info().c_str());
|
||||
}
|
||||
#endif
|
||||
void APIConnection::on_no_setup_connection() {
|
||||
this->on_fatal_error();
|
||||
ESP_LOGD(TAG, "%s access without full connection", this->get_client_combined_info().c_str());
|
||||
@ -1671,6 +1641,10 @@ ProtoWriteBuffer APIConnection::allocate_batch_message_buffer(uint16_t size) {
|
||||
}
|
||||
|
||||
void APIConnection::process_batch_() {
|
||||
// Ensure PacketInfo remains trivially destructible for our placement new approach
|
||||
static_assert(std::is_trivially_destructible<PacketInfo>::value,
|
||||
"PacketInfo must remain trivially destructible with this placement-new approach");
|
||||
|
||||
if (this->deferred_batch_.empty()) {
|
||||
this->flags_.batch_scheduled = false;
|
||||
return;
|
||||
@ -1708,9 +1682,12 @@ void APIConnection::process_batch_() {
|
||||
return;
|
||||
}
|
||||
|
||||
// Pre-allocate storage for packet info
|
||||
std::vector<PacketInfo> packet_info;
|
||||
packet_info.reserve(num_items);
|
||||
size_t packets_to_process = std::min(num_items, MAX_PACKETS_PER_BATCH);
|
||||
|
||||
// Stack-allocated array for packet info
|
||||
alignas(PacketInfo) char packet_info_storage[MAX_PACKETS_PER_BATCH * sizeof(PacketInfo)];
|
||||
PacketInfo *packet_info = reinterpret_cast<PacketInfo *>(packet_info_storage);
|
||||
size_t packet_count = 0;
|
||||
|
||||
// Cache these values to avoid repeated virtual calls
|
||||
const uint8_t header_padding = this->helper_->frame_header_padding();
|
||||
@ -1742,8 +1719,8 @@ void APIConnection::process_batch_() {
|
||||
// The actual message data follows after the header padding
|
||||
uint32_t current_offset = 0;
|
||||
|
||||
// Process items and encode directly to buffer
|
||||
for (size_t i = 0; i < this->deferred_batch_.size(); i++) {
|
||||
// Process items and encode directly to buffer (up to our limit)
|
||||
for (size_t i = 0; i < packets_to_process; i++) {
|
||||
const auto &item = this->deferred_batch_[i];
|
||||
// Try to encode message
|
||||
// The creator will calculate overhead to determine if the message fits
|
||||
@ -1757,7 +1734,11 @@ void APIConnection::process_batch_() {
|
||||
// Message was encoded successfully
|
||||
// payload_size is header_padding + actual payload size + footer_size
|
||||
uint16_t proto_payload_size = payload_size - header_padding - footer_size;
|
||||
packet_info.emplace_back(item.message_type, current_offset, proto_payload_size);
|
||||
// Use placement new to construct PacketInfo in pre-allocated stack array
|
||||
// This avoids default-constructing all MAX_PACKETS_PER_BATCH elements
|
||||
// Explicit destruction is not needed because PacketInfo is trivially destructible,
|
||||
// as ensured by the static_assert in its definition.
|
||||
new (&packet_info[packet_count++]) PacketInfo(item.message_type, current_offset, proto_payload_size);
|
||||
|
||||
// Update tracking variables
|
||||
items_processed++;
|
||||
@ -1783,8 +1764,8 @@ void APIConnection::process_batch_() {
|
||||
}
|
||||
|
||||
// Send all collected packets
|
||||
APIError err =
|
||||
this->helper_->write_protobuf_packets(ProtoWriteBuffer{&this->parent_->get_shared_buffer_ref()}, packet_info);
|
||||
APIError err = this->helper_->write_protobuf_packets(ProtoWriteBuffer{&this->parent_->get_shared_buffer_ref()},
|
||||
std::span<const PacketInfo>(packet_info, packet_count));
|
||||
if (err != APIError::OK && err != APIError::WOULD_BLOCK) {
|
||||
on_fatal_error();
|
||||
ESP_LOGW(TAG, "%s: Batch write failed %s errno=%d", this->get_client_combined_info().c_str(), api_error_to_str(err),
|
||||
@ -1844,6 +1825,27 @@ uint16_t APIConnection::try_send_ping_request(EntityBase *entity, APIConnection
|
||||
return encode_message_to_buffer(req, PingRequest::MESSAGE_TYPE, conn, remaining_size, is_single);
|
||||
}
|
||||
|
||||
} // namespace api
|
||||
} // namespace esphome
|
||||
#ifdef USE_API_HOMEASSISTANT_STATES
|
||||
void APIConnection::process_state_subscriptions_() {
|
||||
const auto &subs = this->parent_->get_state_subs();
|
||||
if (this->state_subs_at_ >= static_cast<int>(subs.size())) {
|
||||
this->state_subs_at_ = -1;
|
||||
return;
|
||||
}
|
||||
|
||||
const auto &it = subs[this->state_subs_at_];
|
||||
SubscribeHomeAssistantStateResponse resp;
|
||||
resp.set_entity_id(StringRef(it.entity_id));
|
||||
|
||||
// Avoid string copy by directly using the optional's value if it exists
|
||||
resp.set_attribute(it.attribute.has_value() ? StringRef(it.attribute.value()) : StringRef(""));
|
||||
|
||||
resp.once = it.once;
|
||||
if (this->send_message(resp, SubscribeHomeAssistantStateResponse::MESSAGE_TYPE)) {
|
||||
this->state_subs_at_++;
|
||||
}
|
||||
}
|
||||
#endif // USE_API_HOMEASSISTANT_STATES
|
||||
|
||||
} // namespace esphome::api
|
||||
#endif
|
||||
|
@ -13,13 +13,36 @@
|
||||
#include <vector>
|
||||
#include <functional>
|
||||
|
||||
namespace esphome {
|
||||
namespace api {
|
||||
namespace esphome::api {
|
||||
|
||||
// Client information structure
|
||||
struct ClientInfo {
|
||||
std::string name; // Client name from Hello message
|
||||
std::string peername; // IP:port from socket
|
||||
|
||||
std::string get_combined_info() const {
|
||||
if (name == peername) {
|
||||
// Before Hello message, both are the same
|
||||
return name;
|
||||
}
|
||||
return name + " (" + peername + ")";
|
||||
}
|
||||
};
|
||||
|
||||
// Keepalive timeout in milliseconds
|
||||
static constexpr uint32_t KEEPALIVE_TIMEOUT_MS = 60000;
|
||||
// Maximum number of entities to process in a single batch during initial state/info sending
|
||||
static constexpr size_t MAX_INITIAL_PER_BATCH = 20;
|
||||
// This was increased from 20 to 24 after removing the unique_id field from entity info messages,
|
||||
// which reduced message sizes allowing more entities per batch without exceeding packet limits
|
||||
static constexpr size_t MAX_INITIAL_PER_BATCH = 24;
|
||||
// Maximum number of packets to process in a single batch (platform-dependent)
|
||||
// This limit exists to prevent stack overflow from the PacketInfo array in process_batch_
|
||||
// Each PacketInfo is 8 bytes, so 64 * 8 = 512 bytes, 32 * 8 = 256 bytes
|
||||
#if defined(USE_ESP32) || defined(USE_HOST)
|
||||
static constexpr size_t MAX_PACKETS_PER_BATCH = 64; // ESP32 has 8KB+ stack, HOST has plenty
|
||||
#else
|
||||
static constexpr size_t MAX_PACKETS_PER_BATCH = 32; // ESP8266/RP2040/etc have smaller stacks
|
||||
#endif
|
||||
|
||||
class APIConnection : public APIServerConnection {
|
||||
public:
|
||||
@ -108,15 +131,16 @@ class APIConnection : public APIServerConnection {
|
||||
void media_player_command(const MediaPlayerCommandRequest &msg) override;
|
||||
#endif
|
||||
bool try_send_log_message(int level, const char *tag, const char *line, size_t message_len);
|
||||
#ifdef USE_API_HOMEASSISTANT_SERVICES
|
||||
void send_homeassistant_service_call(const HomeassistantServiceResponse &call) {
|
||||
if (!this->flags_.service_call_subscription)
|
||||
return;
|
||||
this->send_message(call, HomeassistantServiceResponse::MESSAGE_TYPE);
|
||||
}
|
||||
#endif
|
||||
#ifdef USE_BLUETOOTH_PROXY
|
||||
void subscribe_bluetooth_le_advertisements(const SubscribeBluetoothLEAdvertisementsRequest &msg) override;
|
||||
void unsubscribe_bluetooth_le_advertisements(const UnsubscribeBluetoothLEAdvertisementsRequest &msg) override;
|
||||
bool send_bluetooth_le_advertisement(const BluetoothLEAdvertisementResponse &msg);
|
||||
|
||||
void bluetooth_device_request(const BluetoothDeviceRequest &msg) override;
|
||||
void bluetooth_gatt_read(const BluetoothGATTReadRequest &msg) override;
|
||||
@ -125,8 +149,7 @@ class APIConnection : public APIServerConnection {
|
||||
void bluetooth_gatt_write_descriptor(const BluetoothGATTWriteDescriptorRequest &msg) override;
|
||||
void bluetooth_gatt_get_services(const BluetoothGATTGetServicesRequest &msg) override;
|
||||
void bluetooth_gatt_notify(const BluetoothGATTNotifyRequest &msg) override;
|
||||
BluetoothConnectionsFreeResponse subscribe_bluetooth_connections_free(
|
||||
const SubscribeBluetoothConnectionsFreeRequest &msg) override;
|
||||
bool send_subscribe_bluetooth_connections_free_response(const SubscribeBluetoothConnectionsFreeRequest &msg) override;
|
||||
void bluetooth_scanner_set_mode(const BluetoothScannerSetModeRequest &msg) override;
|
||||
|
||||
#endif
|
||||
@ -144,8 +167,7 @@ class APIConnection : public APIServerConnection {
|
||||
void on_voice_assistant_audio(const VoiceAssistantAudio &msg) override;
|
||||
void on_voice_assistant_timer_event_response(const VoiceAssistantTimerEventResponse &msg) override;
|
||||
void on_voice_assistant_announce_request(const VoiceAssistantAnnounceRequest &msg) override;
|
||||
VoiceAssistantConfigurationResponse voice_assistant_get_configuration(
|
||||
const VoiceAssistantConfigurationRequest &msg) override;
|
||||
bool send_voice_assistant_get_configuration_response(const VoiceAssistantConfigurationRequest &msg) override;
|
||||
void voice_assistant_set_configuration(const VoiceAssistantSetConfiguration &msg) override;
|
||||
#endif
|
||||
|
||||
@ -168,15 +190,17 @@ class APIConnection : public APIServerConnection {
|
||||
// we initiated ping
|
||||
this->flags_.sent_ping = false;
|
||||
}
|
||||
#ifdef USE_API_HOMEASSISTANT_STATES
|
||||
void on_home_assistant_state_response(const HomeAssistantStateResponse &msg) override;
|
||||
#endif
|
||||
#ifdef USE_HOMEASSISTANT_TIME
|
||||
void on_get_time_response(const GetTimeResponse &value) override;
|
||||
#endif
|
||||
HelloResponse hello(const HelloRequest &msg) override;
|
||||
ConnectResponse connect(const ConnectRequest &msg) override;
|
||||
DisconnectResponse disconnect(const DisconnectRequest &msg) override;
|
||||
PingResponse ping(const PingRequest &msg) override { return {}; }
|
||||
DeviceInfoResponse device_info(const DeviceInfoRequest &msg) override;
|
||||
bool send_hello_response(const HelloRequest &msg) override;
|
||||
bool send_connect_response(const ConnectRequest &msg) override;
|
||||
bool send_disconnect_response(const DisconnectRequest &msg) override;
|
||||
bool send_ping_response(const PingRequest &msg) override;
|
||||
bool send_device_info_response(const DeviceInfoRequest &msg) override;
|
||||
void list_entities(const ListEntitiesRequest &msg) override { this->list_entities_iterator_.begin(); }
|
||||
void subscribe_states(const SubscribeStatesRequest &msg) override {
|
||||
this->flags_.state_subscription = true;
|
||||
@ -187,19 +211,20 @@ class APIConnection : public APIServerConnection {
|
||||
if (msg.dump_config)
|
||||
App.schedule_dump_config();
|
||||
}
|
||||
#ifdef USE_API_HOMEASSISTANT_SERVICES
|
||||
void subscribe_homeassistant_services(const SubscribeHomeassistantServicesRequest &msg) override {
|
||||
this->flags_.service_call_subscription = true;
|
||||
}
|
||||
#endif
|
||||
#ifdef USE_API_HOMEASSISTANT_STATES
|
||||
void subscribe_home_assistant_states(const SubscribeHomeAssistantStatesRequest &msg) override;
|
||||
GetTimeResponse get_time(const GetTimeRequest &msg) override {
|
||||
// TODO
|
||||
return {};
|
||||
}
|
||||
#endif
|
||||
bool send_get_time_response(const GetTimeRequest &msg) override;
|
||||
#ifdef USE_API_SERVICES
|
||||
void execute_service(const ExecuteServiceRequest &msg) override;
|
||||
#endif
|
||||
#ifdef USE_API_NOISE
|
||||
NoiseEncryptionSetKeyResponse noise_encryption_set_key(const NoiseEncryptionSetKeyRequest &msg) override;
|
||||
bool send_noise_encryption_set_key_response(const NoiseEncryptionSetKeyRequest &msg) override;
|
||||
#endif
|
||||
|
||||
bool is_authenticated() override {
|
||||
@ -211,7 +236,9 @@ class APIConnection : public APIServerConnection {
|
||||
}
|
||||
uint8_t get_log_subscription_level() const { return this->flags_.log_subscription; }
|
||||
void on_fatal_error() override;
|
||||
#ifdef USE_API_PASSWORD
|
||||
void on_unauthenticated_access() override;
|
||||
#endif
|
||||
void on_no_setup_connection() override;
|
||||
ProtoWriteBuffer create_buffer(uint32_t reserve_size) override {
|
||||
// FIXME: ensure no recursive writes can happen
|
||||
@ -261,13 +288,7 @@ class APIConnection : public APIServerConnection {
|
||||
bool try_to_clear_buffer(bool log_out_of_space);
|
||||
bool send_buffer(ProtoWriteBuffer buffer, uint8_t message_type) override;
|
||||
|
||||
std::string get_client_combined_info() const {
|
||||
if (this->client_info_ == this->client_peername_) {
|
||||
// Before Hello message, both are the same (just IP:port)
|
||||
return this->client_info_;
|
||||
}
|
||||
return this->client_info_ + " (" + this->client_peername_ + ")";
|
||||
}
|
||||
std::string get_client_combined_info() const { return this->client_info_.get_combined_info(); }
|
||||
|
||||
// Buffer allocator methods for batch processing
|
||||
ProtoWriteBuffer allocate_single_message_buffer(uint16_t size);
|
||||
@ -277,6 +298,10 @@ class APIConnection : public APIServerConnection {
|
||||
// Helper function to handle authentication completion
|
||||
void complete_authentication_();
|
||||
|
||||
#ifdef USE_API_HOMEASSISTANT_STATES
|
||||
void process_state_subscriptions_();
|
||||
#endif
|
||||
|
||||
// Non-template helper to encode any ProtoMessage
|
||||
static uint16_t encode_message_to_buffer(ProtoMessage &msg, uint8_t message_type, APIConnection *conn,
|
||||
uint32_t remaining_size, bool is_single);
|
||||
@ -296,13 +321,18 @@ class APIConnection : public APIServerConnection {
|
||||
APIConnection *conn, uint32_t remaining_size, bool is_single) {
|
||||
// Set common fields that are shared by all entity types
|
||||
msg.key = entity->get_object_id_hash();
|
||||
msg.object_id = entity->get_object_id();
|
||||
// IMPORTANT: get_object_id() may return a temporary std::string
|
||||
std::string object_id = entity->get_object_id();
|
||||
msg.set_object_id(StringRef(object_id));
|
||||
|
||||
if (entity->has_own_name())
|
||||
msg.name = entity->get_name();
|
||||
if (entity->has_own_name()) {
|
||||
msg.set_name(entity->get_name());
|
||||
}
|
||||
|
||||
// Set common EntityBase properties
|
||||
msg.icon = entity->get_icon();
|
||||
#ifdef USE_ENTITY_ICON
|
||||
msg.set_icon(entity->get_icon_ref());
|
||||
#endif
|
||||
msg.disabled_by_default = entity->is_disabled_by_default();
|
||||
msg.entity_category = static_cast<enums::EntityCategory>(entity->get_entity_category());
|
||||
#ifdef USE_DEVICES
|
||||
@ -471,13 +501,14 @@ class APIConnection : public APIServerConnection {
|
||||
std::unique_ptr<camera::CameraImageReader> image_reader_;
|
||||
#endif
|
||||
|
||||
// Group 3: Strings (12 bytes each on 32-bit, 4-byte aligned)
|
||||
std::string client_info_;
|
||||
std::string client_peername_;
|
||||
// Group 3: Client info struct (24 bytes on 32-bit: 2 strings × 12 bytes each)
|
||||
ClientInfo client_info_;
|
||||
|
||||
// Group 4: 4-byte types
|
||||
uint32_t last_traffic_;
|
||||
#ifdef USE_API_HOMEASSISTANT_STATES
|
||||
int state_subs_at_ = -1;
|
||||
#endif
|
||||
|
||||
// Function pointer type for message encoding
|
||||
using MessageCreatorPtr = uint16_t (*)(EntityBase *, APIConnection *, uint32_t remaining_size, bool is_single);
|
||||
@ -707,6 +738,5 @@ class APIConnection : public APIServerConnection {
|
||||
}
|
||||
};
|
||||
|
||||
} // namespace api
|
||||
} // namespace esphome
|
||||
} // namespace esphome::api
|
||||
#endif
|
||||
|
File diff suppressed because it is too large
Load Diff
@ -8,16 +8,17 @@
|
||||
|
||||
#include "esphome/core/defines.h"
|
||||
#ifdef USE_API
|
||||
#ifdef USE_API_NOISE
|
||||
#include "noise/protocol.h"
|
||||
#endif
|
||||
|
||||
#include "api_noise_context.h"
|
||||
#include "esphome/components/socket/socket.h"
|
||||
#include "esphome/core/application.h"
|
||||
#include "esphome/core/log.h"
|
||||
|
||||
namespace esphome {
|
||||
namespace api {
|
||||
namespace esphome::api {
|
||||
|
||||
// uncomment to log raw packets
|
||||
//#define HELPER_LOG_PACKETS
|
||||
|
||||
// Forward declaration
|
||||
struct ClientInfo;
|
||||
|
||||
class ProtoWriteBuffer;
|
||||
|
||||
@ -40,7 +41,6 @@ struct PacketInfo {
|
||||
enum class APIError : uint16_t {
|
||||
OK = 0,
|
||||
WOULD_BLOCK = 1001,
|
||||
BAD_HANDSHAKE_PACKET_LEN = 1002,
|
||||
BAD_INDICATOR = 1003,
|
||||
BAD_DATA_PACKET = 1004,
|
||||
TCP_NODELAY_FAILED = 1005,
|
||||
@ -51,16 +51,19 @@ enum class APIError : uint16_t {
|
||||
BAD_ARG = 1010,
|
||||
SOCKET_READ_FAILED = 1011,
|
||||
SOCKET_WRITE_FAILED = 1012,
|
||||
OUT_OF_MEMORY = 1018,
|
||||
CONNECTION_CLOSED = 1022,
|
||||
#ifdef USE_API_NOISE
|
||||
BAD_HANDSHAKE_PACKET_LEN = 1002,
|
||||
HANDSHAKESTATE_READ_FAILED = 1013,
|
||||
HANDSHAKESTATE_WRITE_FAILED = 1014,
|
||||
HANDSHAKESTATE_BAD_STATE = 1015,
|
||||
CIPHERSTATE_DECRYPT_FAILED = 1016,
|
||||
CIPHERSTATE_ENCRYPT_FAILED = 1017,
|
||||
OUT_OF_MEMORY = 1018,
|
||||
HANDSHAKESTATE_SETUP_FAILED = 1019,
|
||||
HANDSHAKESTATE_SPLIT_FAILED = 1020,
|
||||
BAD_HANDSHAKE_ERROR_BYTE = 1021,
|
||||
CONNECTION_CLOSED = 1022,
|
||||
#endif
|
||||
};
|
||||
|
||||
const char *api_error_to_str(APIError err);
|
||||
@ -68,7 +71,8 @@ const char *api_error_to_str(APIError err);
|
||||
class APIFrameHelper {
|
||||
public:
|
||||
APIFrameHelper() = default;
|
||||
explicit APIFrameHelper(std::unique_ptr<socket::Socket> socket) : socket_owned_(std::move(socket)) {
|
||||
explicit APIFrameHelper(std::unique_ptr<socket::Socket> socket, const ClientInfo *client_info)
|
||||
: socket_owned_(std::move(socket)), client_info_(client_info) {
|
||||
socket_ = socket_owned_.get();
|
||||
}
|
||||
virtual ~APIFrameHelper() = default;
|
||||
@ -94,8 +98,6 @@ class APIFrameHelper {
|
||||
}
|
||||
return APIError::OK;
|
||||
}
|
||||
// Give this helper a name for logging
|
||||
void set_log_info(std::string info) { info_ = std::move(info); }
|
||||
virtual APIError write_protobuf_packet(uint8_t type, ProtoWriteBuffer buffer) = 0;
|
||||
// Write multiple protobuf packets in a single operation
|
||||
// packets contains (message_type, offset, length) for each message in the buffer
|
||||
@ -109,29 +111,28 @@ class APIFrameHelper {
|
||||
bool is_socket_ready() const { return socket_ != nullptr && socket_->ready(); }
|
||||
|
||||
protected:
|
||||
// Struct for holding parsed frame data
|
||||
struct ParsedFrame {
|
||||
std::vector<uint8_t> msg;
|
||||
};
|
||||
|
||||
// Buffer containing data to be sent
|
||||
struct SendBuffer {
|
||||
std::vector<uint8_t> data;
|
||||
uint16_t offset{0}; // Current offset within the buffer (uint16_t to reduce memory usage)
|
||||
std::unique_ptr<uint8_t[]> data;
|
||||
uint16_t size{0}; // Total size of the buffer
|
||||
uint16_t offset{0}; // Current offset within the buffer
|
||||
|
||||
// Using uint16_t reduces memory usage since ESPHome API messages are limited to UINT16_MAX (65535) bytes
|
||||
uint16_t remaining() const { return static_cast<uint16_t>(data.size()) - offset; }
|
||||
const uint8_t *current_data() const { return data.data() + offset; }
|
||||
uint16_t remaining() const { return size - offset; }
|
||||
const uint8_t *current_data() const { return data.get() + offset; }
|
||||
};
|
||||
|
||||
// Common implementation for writing raw data to socket
|
||||
APIError write_raw_(const struct iovec *iov, int iovcnt);
|
||||
APIError write_raw_(const struct iovec *iov, int iovcnt, uint16_t total_write_len);
|
||||
|
||||
// Try to send data from the tx buffer
|
||||
APIError try_send_tx_buf_();
|
||||
|
||||
// Helper method to buffer data from IOVs
|
||||
void buffer_data_from_iov_(const struct iovec *iov, int iovcnt, uint16_t total_write_len);
|
||||
void buffer_data_from_iov_(const struct iovec *iov, int iovcnt, uint16_t total_write_len, uint16_t offset);
|
||||
|
||||
// Common socket write error handling
|
||||
APIError handle_socket_write_error_();
|
||||
template<typename StateEnum>
|
||||
APIError write_raw_(const struct iovec *iov, int iovcnt, socket::Socket *socket, std::vector<uint8_t> &tx_buf,
|
||||
const std::string &info, StateEnum &state, StateEnum failed_state);
|
||||
@ -161,10 +162,13 @@ class APIFrameHelper {
|
||||
|
||||
// Containers (size varies, but typically 12+ bytes on 32-bit)
|
||||
std::deque<SendBuffer> tx_buf_;
|
||||
std::string info_;
|
||||
std::vector<struct iovec> reusable_iovs_;
|
||||
std::vector<uint8_t> rx_buf_;
|
||||
|
||||
// Pointer to client info (4 bytes on 32-bit)
|
||||
// Note: The pointed-to ClientInfo object must outlive this APIFrameHelper instance.
|
||||
const ClientInfo *client_info_{nullptr};
|
||||
|
||||
// Group smaller types together
|
||||
uint16_t rx_buf_len_ = 0;
|
||||
State state_{State::INITIALIZE};
|
||||
@ -179,105 +183,6 @@ class APIFrameHelper {
|
||||
APIError handle_socket_read_result_(ssize_t received);
|
||||
};
|
||||
|
||||
#ifdef USE_API_NOISE
|
||||
class APINoiseFrameHelper : public APIFrameHelper {
|
||||
public:
|
||||
APINoiseFrameHelper(std::unique_ptr<socket::Socket> socket, std::shared_ptr<APINoiseContext> ctx)
|
||||
: APIFrameHelper(std::move(socket)), ctx_(std::move(ctx)) {
|
||||
// Noise header structure:
|
||||
// Pos 0: indicator (0x01)
|
||||
// Pos 1-2: encrypted payload size (16-bit big-endian)
|
||||
// Pos 3-6: encrypted type (16-bit) + data_len (16-bit)
|
||||
// Pos 7+: actual payload data
|
||||
frame_header_padding_ = 7;
|
||||
}
|
||||
~APINoiseFrameHelper() override;
|
||||
APIError init() override;
|
||||
APIError loop() override;
|
||||
APIError read_packet(ReadPacketBuffer *buffer) override;
|
||||
APIError write_protobuf_packet(uint8_t type, ProtoWriteBuffer buffer) override;
|
||||
APIError write_protobuf_packets(ProtoWriteBuffer buffer, std::span<const PacketInfo> packets) override;
|
||||
// Get the frame header padding required by this protocol
|
||||
uint8_t frame_header_padding() override { return frame_header_padding_; }
|
||||
// Get the frame footer size required by this protocol
|
||||
uint8_t frame_footer_size() override { return frame_footer_size_; }
|
||||
} // namespace esphome::api
|
||||
|
||||
protected:
|
||||
APIError state_action_();
|
||||
APIError try_read_frame_(ParsedFrame *frame);
|
||||
APIError write_frame_(const uint8_t *data, uint16_t len);
|
||||
APIError init_handshake_();
|
||||
APIError check_handshake_finished_();
|
||||
void send_explicit_handshake_reject_(const std::string &reason);
|
||||
|
||||
// Pointers first (4 bytes each)
|
||||
NoiseHandshakeState *handshake_{nullptr};
|
||||
NoiseCipherState *send_cipher_{nullptr};
|
||||
NoiseCipherState *recv_cipher_{nullptr};
|
||||
|
||||
// Shared pointer (8 bytes on 32-bit = 4 bytes control block pointer + 4 bytes object pointer)
|
||||
std::shared_ptr<APINoiseContext> ctx_;
|
||||
|
||||
// Vector (12 bytes on 32-bit)
|
||||
std::vector<uint8_t> prologue_;
|
||||
|
||||
// NoiseProtocolId (size depends on implementation)
|
||||
NoiseProtocolId nid_;
|
||||
|
||||
// Group small types together
|
||||
// Fixed-size header buffer for noise protocol:
|
||||
// 1 byte for indicator + 2 bytes for message size (16-bit value, not varint)
|
||||
// Note: Maximum message size is UINT16_MAX (65535), with a limit of 128 bytes during handshake phase
|
||||
uint8_t rx_header_buf_[3];
|
||||
uint8_t rx_header_buf_len_ = 0;
|
||||
// 4 bytes total, no padding
|
||||
};
|
||||
#endif // USE_API_NOISE
|
||||
|
||||
#ifdef USE_API_PLAINTEXT
|
||||
class APIPlaintextFrameHelper : public APIFrameHelper {
|
||||
public:
|
||||
APIPlaintextFrameHelper(std::unique_ptr<socket::Socket> socket) : APIFrameHelper(std::move(socket)) {
|
||||
// Plaintext header structure (worst case):
|
||||
// Pos 0: indicator (0x00)
|
||||
// Pos 1-3: payload size varint (up to 3 bytes)
|
||||
// Pos 4-5: message type varint (up to 2 bytes)
|
||||
// Pos 6+: actual payload data
|
||||
frame_header_padding_ = 6;
|
||||
}
|
||||
~APIPlaintextFrameHelper() override = default;
|
||||
APIError init() override;
|
||||
APIError loop() override;
|
||||
APIError read_packet(ReadPacketBuffer *buffer) override;
|
||||
APIError write_protobuf_packet(uint8_t type, ProtoWriteBuffer buffer) override;
|
||||
APIError write_protobuf_packets(ProtoWriteBuffer buffer, std::span<const PacketInfo> packets) override;
|
||||
uint8_t frame_header_padding() override { return frame_header_padding_; }
|
||||
// Get the frame footer size required by this protocol
|
||||
uint8_t frame_footer_size() override { return frame_footer_size_; }
|
||||
|
||||
protected:
|
||||
APIError try_read_frame_(ParsedFrame *frame);
|
||||
|
||||
// Group 2-byte aligned types
|
||||
uint16_t rx_header_parsed_type_ = 0;
|
||||
uint16_t rx_header_parsed_len_ = 0;
|
||||
|
||||
// Group 1-byte types together
|
||||
// Fixed-size header buffer for plaintext protocol:
|
||||
// We now store the indicator byte + the two varints.
|
||||
// To match noise protocol's maximum message size (UINT16_MAX = 65535), we need:
|
||||
// 1 byte for indicator + 3 bytes for message size varint (supports up to 2097151) + 2 bytes for message type varint
|
||||
//
|
||||
// While varints could theoretically be up to 10 bytes each for 64-bit values,
|
||||
// attempting to process messages with headers that large would likely crash the
|
||||
// ESP32 due to memory constraints.
|
||||
uint8_t rx_header_buf_[6]; // 1 byte indicator + 5 bytes for varints (3 for size + 2 for type)
|
||||
uint8_t rx_header_buf_pos_ = 0;
|
||||
bool rx_header_parsed_ = false;
|
||||
// 8 bytes total, no padding needed
|
||||
};
|
||||
#endif
|
||||
|
||||
} // namespace api
|
||||
} // namespace esphome
|
||||
#endif
|
||||
#endif // USE_API
|
||||
|
583
esphome/components/api/api_frame_helper_noise.cpp
Normal file
583
esphome/components/api/api_frame_helper_noise.cpp
Normal file
@ -0,0 +1,583 @@
|
||||
#include "api_frame_helper_noise.h"
|
||||
#ifdef USE_API
|
||||
#ifdef USE_API_NOISE
|
||||
#include "api_connection.h" // For ClientInfo struct
|
||||
#include "esphome/core/application.h"
|
||||
#include "esphome/core/hal.h"
|
||||
#include "esphome/core/helpers.h"
|
||||
#include "esphome/core/log.h"
|
||||
#include "proto.h"
|
||||
#include <cstring>
|
||||
#include <cinttypes>
|
||||
|
||||
namespace esphome::api {
|
||||
|
||||
static const char *const TAG = "api.noise";
|
||||
static const char *const PROLOGUE_INIT = "NoiseAPIInit";
|
||||
static constexpr size_t PROLOGUE_INIT_LEN = 12; // strlen("NoiseAPIInit")
|
||||
|
||||
#define HELPER_LOG(msg, ...) ESP_LOGVV(TAG, "%s: " msg, this->client_info_->get_combined_info().c_str(), ##__VA_ARGS__)
|
||||
|
||||
#ifdef HELPER_LOG_PACKETS
|
||||
#define LOG_PACKET_RECEIVED(buffer) ESP_LOGVV(TAG, "Received frame: %s", format_hex_pretty(buffer).c_str())
|
||||
#define LOG_PACKET_SENDING(data, len) ESP_LOGVV(TAG, "Sending raw: %s", format_hex_pretty(data, len).c_str())
|
||||
#else
|
||||
#define LOG_PACKET_RECEIVED(buffer) ((void) 0)
|
||||
#define LOG_PACKET_SENDING(data, len) ((void) 0)
|
||||
#endif
|
||||
|
||||
/// Convert a noise error code to a readable error
|
||||
std::string noise_err_to_str(int err) {
|
||||
if (err == NOISE_ERROR_NO_MEMORY)
|
||||
return "NO_MEMORY";
|
||||
if (err == NOISE_ERROR_UNKNOWN_ID)
|
||||
return "UNKNOWN_ID";
|
||||
if (err == NOISE_ERROR_UNKNOWN_NAME)
|
||||
return "UNKNOWN_NAME";
|
||||
if (err == NOISE_ERROR_MAC_FAILURE)
|
||||
return "MAC_FAILURE";
|
||||
if (err == NOISE_ERROR_NOT_APPLICABLE)
|
||||
return "NOT_APPLICABLE";
|
||||
if (err == NOISE_ERROR_SYSTEM)
|
||||
return "SYSTEM";
|
||||
if (err == NOISE_ERROR_REMOTE_KEY_REQUIRED)
|
||||
return "REMOTE_KEY_REQUIRED";
|
||||
if (err == NOISE_ERROR_LOCAL_KEY_REQUIRED)
|
||||
return "LOCAL_KEY_REQUIRED";
|
||||
if (err == NOISE_ERROR_PSK_REQUIRED)
|
||||
return "PSK_REQUIRED";
|
||||
if (err == NOISE_ERROR_INVALID_LENGTH)
|
||||
return "INVALID_LENGTH";
|
||||
if (err == NOISE_ERROR_INVALID_PARAM)
|
||||
return "INVALID_PARAM";
|
||||
if (err == NOISE_ERROR_INVALID_STATE)
|
||||
return "INVALID_STATE";
|
||||
if (err == NOISE_ERROR_INVALID_NONCE)
|
||||
return "INVALID_NONCE";
|
||||
if (err == NOISE_ERROR_INVALID_PRIVATE_KEY)
|
||||
return "INVALID_PRIVATE_KEY";
|
||||
if (err == NOISE_ERROR_INVALID_PUBLIC_KEY)
|
||||
return "INVALID_PUBLIC_KEY";
|
||||
if (err == NOISE_ERROR_INVALID_FORMAT)
|
||||
return "INVALID_FORMAT";
|
||||
if (err == NOISE_ERROR_INVALID_SIGNATURE)
|
||||
return "INVALID_SIGNATURE";
|
||||
return to_string(err);
|
||||
}
|
||||
|
||||
/// Initialize the frame helper, returns OK if successful.
|
||||
APIError APINoiseFrameHelper::init() {
|
||||
APIError err = init_common_();
|
||||
if (err != APIError::OK) {
|
||||
return err;
|
||||
}
|
||||
|
||||
// init prologue
|
||||
size_t old_size = prologue_.size();
|
||||
prologue_.resize(old_size + PROLOGUE_INIT_LEN);
|
||||
std::memcpy(prologue_.data() + old_size, PROLOGUE_INIT, PROLOGUE_INIT_LEN);
|
||||
|
||||
state_ = State::CLIENT_HELLO;
|
||||
return APIError::OK;
|
||||
}
|
||||
// Helper for handling handshake frame errors
|
||||
APIError APINoiseFrameHelper::handle_handshake_frame_error_(APIError aerr) {
|
||||
if (aerr == APIError::BAD_INDICATOR) {
|
||||
send_explicit_handshake_reject_("Bad indicator byte");
|
||||
} else if (aerr == APIError::BAD_HANDSHAKE_PACKET_LEN) {
|
||||
send_explicit_handshake_reject_("Bad handshake packet len");
|
||||
}
|
||||
return aerr;
|
||||
}
|
||||
|
||||
// Helper for handling noise library errors
|
||||
APIError APINoiseFrameHelper::handle_noise_error_(int err, const char *func_name, APIError api_err) {
|
||||
if (err != 0) {
|
||||
state_ = State::FAILED;
|
||||
HELPER_LOG("%s failed: %s", func_name, noise_err_to_str(err).c_str());
|
||||
return api_err;
|
||||
}
|
||||
return APIError::OK;
|
||||
}
|
||||
|
||||
/// Run through handshake messages (if in that phase)
|
||||
APIError APINoiseFrameHelper::loop() {
|
||||
// During handshake phase, process as many actions as possible until we can't progress
|
||||
// socket_->ready() stays true until next main loop, but state_action() will return
|
||||
// WOULD_BLOCK when no more data is available to read
|
||||
while (state_ != State::DATA && this->socket_->ready()) {
|
||||
APIError err = state_action_();
|
||||
if (err == APIError::WOULD_BLOCK) {
|
||||
break;
|
||||
}
|
||||
if (err != APIError::OK) {
|
||||
return err;
|
||||
}
|
||||
}
|
||||
|
||||
// Use base class implementation for buffer sending
|
||||
return APIFrameHelper::loop();
|
||||
}
|
||||
|
||||
/** Read a packet into the rx_buf_. If successful, stores frame data in the frame parameter
|
||||
*
|
||||
* @param frame: The struct to hold the frame information in.
|
||||
* msg_start: points to the start of the payload - this pointer is only valid until the next
|
||||
* try_receive_raw_ call
|
||||
*
|
||||
* @return 0 if a full packet is in rx_buf_
|
||||
* @return -1 if error, check errno.
|
||||
*
|
||||
* errno EWOULDBLOCK: Packet could not be read without blocking. Try again later.
|
||||
* errno ENOMEM: Not enough memory for reading packet.
|
||||
* errno API_ERROR_BAD_INDICATOR: Bad indicator byte at start of frame.
|
||||
* errno API_ERROR_HANDSHAKE_PACKET_LEN: Packet too big for this phase.
|
||||
*/
|
||||
APIError APINoiseFrameHelper::try_read_frame_(std::vector<uint8_t> *frame) {
|
||||
if (frame == nullptr) {
|
||||
HELPER_LOG("Bad argument for try_read_frame_");
|
||||
return APIError::BAD_ARG;
|
||||
}
|
||||
|
||||
// read header
|
||||
if (rx_header_buf_len_ < 3) {
|
||||
// no header information yet
|
||||
uint8_t to_read = 3 - rx_header_buf_len_;
|
||||
ssize_t received = this->socket_->read(&rx_header_buf_[rx_header_buf_len_], to_read);
|
||||
APIError err = handle_socket_read_result_(received);
|
||||
if (err != APIError::OK) {
|
||||
return err;
|
||||
}
|
||||
rx_header_buf_len_ += static_cast<uint8_t>(received);
|
||||
if (static_cast<uint8_t>(received) != to_read) {
|
||||
// not a full read
|
||||
return APIError::WOULD_BLOCK;
|
||||
}
|
||||
|
||||
if (rx_header_buf_[0] != 0x01) {
|
||||
state_ = State::FAILED;
|
||||
HELPER_LOG("Bad indicator byte %u", rx_header_buf_[0]);
|
||||
return APIError::BAD_INDICATOR;
|
||||
}
|
||||
// header reading done
|
||||
}
|
||||
|
||||
// read body
|
||||
uint16_t msg_size = (((uint16_t) rx_header_buf_[1]) << 8) | rx_header_buf_[2];
|
||||
|
||||
if (state_ != State::DATA && msg_size > 128) {
|
||||
// for handshake message only permit up to 128 bytes
|
||||
state_ = State::FAILED;
|
||||
HELPER_LOG("Bad packet len for handshake: %d", msg_size);
|
||||
return APIError::BAD_HANDSHAKE_PACKET_LEN;
|
||||
}
|
||||
|
||||
// reserve space for body
|
||||
if (rx_buf_.size() != msg_size) {
|
||||
rx_buf_.resize(msg_size);
|
||||
}
|
||||
|
||||
if (rx_buf_len_ < msg_size) {
|
||||
// more data to read
|
||||
uint16_t to_read = msg_size - rx_buf_len_;
|
||||
ssize_t received = this->socket_->read(&rx_buf_[rx_buf_len_], to_read);
|
||||
APIError err = handle_socket_read_result_(received);
|
||||
if (err != APIError::OK) {
|
||||
return err;
|
||||
}
|
||||
rx_buf_len_ += static_cast<uint16_t>(received);
|
||||
if (static_cast<uint16_t>(received) != to_read) {
|
||||
// not all read
|
||||
return APIError::WOULD_BLOCK;
|
||||
}
|
||||
}
|
||||
|
||||
LOG_PACKET_RECEIVED(rx_buf_);
|
||||
*frame = std::move(rx_buf_);
|
||||
// consume msg
|
||||
rx_buf_ = {};
|
||||
rx_buf_len_ = 0;
|
||||
rx_header_buf_len_ = 0;
|
||||
return APIError::OK;
|
||||
}
|
||||
|
||||
/** To be called from read/write methods.
|
||||
*
|
||||
* This method runs through the internal handshake methods, if in that state.
|
||||
*
|
||||
* If the handshake is still active when this method returns and a read/write can't take place at
|
||||
* the moment, returns WOULD_BLOCK.
|
||||
* If an error occurred, returns that error. Only returns OK if the transport is ready for data
|
||||
* traffic.
|
||||
*/
|
||||
APIError APINoiseFrameHelper::state_action_() {
|
||||
int err;
|
||||
APIError aerr;
|
||||
if (state_ == State::INITIALIZE) {
|
||||
HELPER_LOG("Bad state for method: %d", (int) state_);
|
||||
return APIError::BAD_STATE;
|
||||
}
|
||||
if (state_ == State::CLIENT_HELLO) {
|
||||
// waiting for client hello
|
||||
std::vector<uint8_t> frame;
|
||||
aerr = try_read_frame_(&frame);
|
||||
if (aerr != APIError::OK) {
|
||||
return handle_handshake_frame_error_(aerr);
|
||||
}
|
||||
// ignore contents, may be used in future for flags
|
||||
// Resize for: existing prologue + 2 size bytes + frame data
|
||||
size_t old_size = prologue_.size();
|
||||
prologue_.resize(old_size + 2 + frame.size());
|
||||
prologue_[old_size] = (uint8_t) (frame.size() >> 8);
|
||||
prologue_[old_size + 1] = (uint8_t) frame.size();
|
||||
std::memcpy(prologue_.data() + old_size + 2, frame.data(), frame.size());
|
||||
|
||||
state_ = State::SERVER_HELLO;
|
||||
}
|
||||
if (state_ == State::SERVER_HELLO) {
|
||||
// send server hello
|
||||
const std::string &name = App.get_name();
|
||||
const std::string &mac = get_mac_address();
|
||||
|
||||
std::vector<uint8_t> msg;
|
||||
// Calculate positions and sizes
|
||||
size_t name_len = name.size() + 1; // including null terminator
|
||||
size_t mac_len = mac.size() + 1; // including null terminator
|
||||
size_t name_offset = 1;
|
||||
size_t mac_offset = name_offset + name_len;
|
||||
size_t total_size = 1 + name_len + mac_len;
|
||||
|
||||
msg.resize(total_size);
|
||||
|
||||
// chosen proto
|
||||
msg[0] = 0x01;
|
||||
|
||||
// node name, terminated by null byte
|
||||
std::memcpy(msg.data() + name_offset, name.c_str(), name_len);
|
||||
// node mac, terminated by null byte
|
||||
std::memcpy(msg.data() + mac_offset, mac.c_str(), mac_len);
|
||||
|
||||
aerr = write_frame_(msg.data(), msg.size());
|
||||
if (aerr != APIError::OK)
|
||||
return aerr;
|
||||
|
||||
// start handshake
|
||||
aerr = init_handshake_();
|
||||
if (aerr != APIError::OK)
|
||||
return aerr;
|
||||
|
||||
state_ = State::HANDSHAKE;
|
||||
}
|
||||
if (state_ == State::HANDSHAKE) {
|
||||
int action = noise_handshakestate_get_action(handshake_);
|
||||
if (action == NOISE_ACTION_READ_MESSAGE) {
|
||||
// waiting for handshake msg
|
||||
std::vector<uint8_t> frame;
|
||||
aerr = try_read_frame_(&frame);
|
||||
if (aerr != APIError::OK) {
|
||||
return handle_handshake_frame_error_(aerr);
|
||||
}
|
||||
|
||||
if (frame.empty()) {
|
||||
send_explicit_handshake_reject_("Empty handshake message");
|
||||
return APIError::BAD_HANDSHAKE_ERROR_BYTE;
|
||||
} else if (frame[0] != 0x00) {
|
||||
HELPER_LOG("Bad handshake error byte: %u", frame[0]);
|
||||
send_explicit_handshake_reject_("Bad handshake error byte");
|
||||
return APIError::BAD_HANDSHAKE_ERROR_BYTE;
|
||||
}
|
||||
|
||||
NoiseBuffer mbuf;
|
||||
noise_buffer_init(mbuf);
|
||||
noise_buffer_set_input(mbuf, frame.data() + 1, frame.size() - 1);
|
||||
err = noise_handshakestate_read_message(handshake_, &mbuf, nullptr);
|
||||
if (err != 0) {
|
||||
// Special handling for MAC failure
|
||||
send_explicit_handshake_reject_(err == NOISE_ERROR_MAC_FAILURE ? "Handshake MAC failure" : "Handshake error");
|
||||
return handle_noise_error_(err, "noise_handshakestate_read_message", APIError::HANDSHAKESTATE_READ_FAILED);
|
||||
}
|
||||
|
||||
aerr = check_handshake_finished_();
|
||||
if (aerr != APIError::OK)
|
||||
return aerr;
|
||||
} else if (action == NOISE_ACTION_WRITE_MESSAGE) {
|
||||
uint8_t buffer[65];
|
||||
NoiseBuffer mbuf;
|
||||
noise_buffer_init(mbuf);
|
||||
noise_buffer_set_output(mbuf, buffer + 1, sizeof(buffer) - 1);
|
||||
|
||||
err = noise_handshakestate_write_message(handshake_, &mbuf, nullptr);
|
||||
APIError aerr_write =
|
||||
handle_noise_error_(err, "noise_handshakestate_write_message", APIError::HANDSHAKESTATE_WRITE_FAILED);
|
||||
if (aerr_write != APIError::OK)
|
||||
return aerr_write;
|
||||
buffer[0] = 0x00; // success
|
||||
|
||||
aerr = write_frame_(buffer, mbuf.size + 1);
|
||||
if (aerr != APIError::OK)
|
||||
return aerr;
|
||||
aerr = check_handshake_finished_();
|
||||
if (aerr != APIError::OK)
|
||||
return aerr;
|
||||
} else {
|
||||
// bad state for action
|
||||
state_ = State::FAILED;
|
||||
HELPER_LOG("Bad action for handshake: %d", action);
|
||||
return APIError::HANDSHAKESTATE_BAD_STATE;
|
||||
}
|
||||
}
|
||||
if (state_ == State::CLOSED || state_ == State::FAILED) {
|
||||
return APIError::BAD_STATE;
|
||||
}
|
||||
return APIError::OK;
|
||||
}
|
||||
void APINoiseFrameHelper::send_explicit_handshake_reject_(const std::string &reason) {
|
||||
std::vector<uint8_t> data;
|
||||
data.resize(reason.length() + 1);
|
||||
data[0] = 0x01; // failure
|
||||
|
||||
// Copy error message in bulk
|
||||
if (!reason.empty()) {
|
||||
std::memcpy(data.data() + 1, reason.c_str(), reason.length());
|
||||
}
|
||||
|
||||
// temporarily remove failed state
|
||||
auto orig_state = state_;
|
||||
state_ = State::EXPLICIT_REJECT;
|
||||
write_frame_(data.data(), data.size());
|
||||
state_ = orig_state;
|
||||
}
|
||||
APIError APINoiseFrameHelper::read_packet(ReadPacketBuffer *buffer) {
|
||||
int err;
|
||||
APIError aerr;
|
||||
aerr = state_action_();
|
||||
if (aerr != APIError::OK) {
|
||||
return aerr;
|
||||
}
|
||||
|
||||
if (state_ != State::DATA) {
|
||||
return APIError::WOULD_BLOCK;
|
||||
}
|
||||
|
||||
std::vector<uint8_t> frame;
|
||||
aerr = try_read_frame_(&frame);
|
||||
if (aerr != APIError::OK)
|
||||
return aerr;
|
||||
|
||||
NoiseBuffer mbuf;
|
||||
noise_buffer_init(mbuf);
|
||||
noise_buffer_set_inout(mbuf, frame.data(), frame.size(), frame.size());
|
||||
err = noise_cipherstate_decrypt(recv_cipher_, &mbuf);
|
||||
APIError decrypt_err = handle_noise_error_(err, "noise_cipherstate_decrypt", APIError::CIPHERSTATE_DECRYPT_FAILED);
|
||||
if (decrypt_err != APIError::OK)
|
||||
return decrypt_err;
|
||||
|
||||
uint16_t msg_size = mbuf.size;
|
||||
uint8_t *msg_data = frame.data();
|
||||
if (msg_size < 4) {
|
||||
state_ = State::FAILED;
|
||||
HELPER_LOG("Bad data packet: size %d too short", msg_size);
|
||||
return APIError::BAD_DATA_PACKET;
|
||||
}
|
||||
|
||||
uint16_t type = (((uint16_t) msg_data[0]) << 8) | msg_data[1];
|
||||
uint16_t data_len = (((uint16_t) msg_data[2]) << 8) | msg_data[3];
|
||||
if (data_len > msg_size - 4) {
|
||||
state_ = State::FAILED;
|
||||
HELPER_LOG("Bad data packet: data_len %u greater than msg_size %u", data_len, msg_size);
|
||||
return APIError::BAD_DATA_PACKET;
|
||||
}
|
||||
|
||||
buffer->container = std::move(frame);
|
||||
buffer->data_offset = 4;
|
||||
buffer->data_len = data_len;
|
||||
buffer->type = type;
|
||||
return APIError::OK;
|
||||
}
|
||||
APIError APINoiseFrameHelper::write_protobuf_packet(uint8_t type, ProtoWriteBuffer buffer) {
|
||||
// Resize to include MAC space (required for Noise encryption)
|
||||
buffer.get_buffer()->resize(buffer.get_buffer()->size() + frame_footer_size_);
|
||||
PacketInfo packet{type, 0,
|
||||
static_cast<uint16_t>(buffer.get_buffer()->size() - frame_header_padding_ - frame_footer_size_)};
|
||||
return write_protobuf_packets(buffer, std::span<const PacketInfo>(&packet, 1));
|
||||
}
|
||||
|
||||
APIError APINoiseFrameHelper::write_protobuf_packets(ProtoWriteBuffer buffer, std::span<const PacketInfo> packets) {
|
||||
APIError aerr = state_action_();
|
||||
if (aerr != APIError::OK) {
|
||||
return aerr;
|
||||
}
|
||||
|
||||
if (state_ != State::DATA) {
|
||||
return APIError::WOULD_BLOCK;
|
||||
}
|
||||
|
||||
if (packets.empty()) {
|
||||
return APIError::OK;
|
||||
}
|
||||
|
||||
std::vector<uint8_t> *raw_buffer = buffer.get_buffer();
|
||||
uint8_t *buffer_data = raw_buffer->data(); // Cache buffer pointer
|
||||
|
||||
this->reusable_iovs_.clear();
|
||||
this->reusable_iovs_.reserve(packets.size());
|
||||
uint16_t total_write_len = 0;
|
||||
|
||||
// We need to encrypt each packet in place
|
||||
for (const auto &packet : packets) {
|
||||
// The buffer already has padding at offset
|
||||
uint8_t *buf_start = buffer_data + packet.offset;
|
||||
|
||||
// Write noise header
|
||||
buf_start[0] = 0x01; // indicator
|
||||
// buf_start[1], buf_start[2] to be set after encryption
|
||||
|
||||
// Write message header (to be encrypted)
|
||||
const uint8_t msg_offset = 3;
|
||||
buf_start[msg_offset] = static_cast<uint8_t>(packet.message_type >> 8); // type high byte
|
||||
buf_start[msg_offset + 1] = static_cast<uint8_t>(packet.message_type); // type low byte
|
||||
buf_start[msg_offset + 2] = static_cast<uint8_t>(packet.payload_size >> 8); // data_len high byte
|
||||
buf_start[msg_offset + 3] = static_cast<uint8_t>(packet.payload_size); // data_len low byte
|
||||
// payload data is already in the buffer starting at offset + 7
|
||||
|
||||
// Make sure we have space for MAC
|
||||
// The buffer should already have been sized appropriately
|
||||
|
||||
// Encrypt the message in place
|
||||
NoiseBuffer mbuf;
|
||||
noise_buffer_init(mbuf);
|
||||
noise_buffer_set_inout(mbuf, buf_start + msg_offset, 4 + packet.payload_size,
|
||||
4 + packet.payload_size + frame_footer_size_);
|
||||
|
||||
int err = noise_cipherstate_encrypt(send_cipher_, &mbuf);
|
||||
APIError aerr = handle_noise_error_(err, "noise_cipherstate_encrypt", APIError::CIPHERSTATE_ENCRYPT_FAILED);
|
||||
if (aerr != APIError::OK)
|
||||
return aerr;
|
||||
|
||||
// Fill in the encrypted size
|
||||
buf_start[1] = static_cast<uint8_t>(mbuf.size >> 8);
|
||||
buf_start[2] = static_cast<uint8_t>(mbuf.size);
|
||||
|
||||
// Add iovec for this encrypted packet
|
||||
size_t packet_len = static_cast<size_t>(3 + mbuf.size); // indicator + size + encrypted data
|
||||
this->reusable_iovs_.push_back({buf_start, packet_len});
|
||||
total_write_len += packet_len;
|
||||
}
|
||||
|
||||
// Send all encrypted packets in one writev call
|
||||
return this->write_raw_(this->reusable_iovs_.data(), this->reusable_iovs_.size(), total_write_len);
|
||||
}
|
||||
|
||||
APIError APINoiseFrameHelper::write_frame_(const uint8_t *data, uint16_t len) {
|
||||
uint8_t header[3];
|
||||
header[0] = 0x01; // indicator
|
||||
header[1] = (uint8_t) (len >> 8);
|
||||
header[2] = (uint8_t) len;
|
||||
|
||||
struct iovec iov[2];
|
||||
iov[0].iov_base = header;
|
||||
iov[0].iov_len = 3;
|
||||
if (len == 0) {
|
||||
return this->write_raw_(iov, 1, 3); // Just header
|
||||
}
|
||||
iov[1].iov_base = const_cast<uint8_t *>(data);
|
||||
iov[1].iov_len = len;
|
||||
|
||||
return this->write_raw_(iov, 2, 3 + len); // Header + data
|
||||
}
|
||||
|
||||
/** Initiate the data structures for the handshake.
|
||||
*
|
||||
* @return 0 on success, -1 on error (check errno)
|
||||
*/
|
||||
APIError APINoiseFrameHelper::init_handshake_() {
|
||||
int err;
|
||||
memset(&nid_, 0, sizeof(nid_));
|
||||
// const char *proto = "Noise_NNpsk0_25519_ChaChaPoly_SHA256";
|
||||
// err = noise_protocol_name_to_id(&nid_, proto, strlen(proto));
|
||||
nid_.pattern_id = NOISE_PATTERN_NN;
|
||||
nid_.cipher_id = NOISE_CIPHER_CHACHAPOLY;
|
||||
nid_.dh_id = NOISE_DH_CURVE25519;
|
||||
nid_.prefix_id = NOISE_PREFIX_STANDARD;
|
||||
nid_.hybrid_id = NOISE_DH_NONE;
|
||||
nid_.hash_id = NOISE_HASH_SHA256;
|
||||
nid_.modifier_ids[0] = NOISE_MODIFIER_PSK0;
|
||||
|
||||
err = noise_handshakestate_new_by_id(&handshake_, &nid_, NOISE_ROLE_RESPONDER);
|
||||
APIError aerr = handle_noise_error_(err, "noise_handshakestate_new_by_id", APIError::HANDSHAKESTATE_SETUP_FAILED);
|
||||
if (aerr != APIError::OK)
|
||||
return aerr;
|
||||
|
||||
const auto &psk = ctx_->get_psk();
|
||||
err = noise_handshakestate_set_pre_shared_key(handshake_, psk.data(), psk.size());
|
||||
aerr = handle_noise_error_(err, "noise_handshakestate_set_pre_shared_key", APIError::HANDSHAKESTATE_SETUP_FAILED);
|
||||
if (aerr != APIError::OK)
|
||||
return aerr;
|
||||
|
||||
err = noise_handshakestate_set_prologue(handshake_, prologue_.data(), prologue_.size());
|
||||
aerr = handle_noise_error_(err, "noise_handshakestate_set_prologue", APIError::HANDSHAKESTATE_SETUP_FAILED);
|
||||
if (aerr != APIError::OK)
|
||||
return aerr;
|
||||
// set_prologue copies it into handshakestate, so we can get rid of it now
|
||||
prologue_ = {};
|
||||
|
||||
err = noise_handshakestate_start(handshake_);
|
||||
aerr = handle_noise_error_(err, "noise_handshakestate_start", APIError::HANDSHAKESTATE_SETUP_FAILED);
|
||||
if (aerr != APIError::OK)
|
||||
return aerr;
|
||||
return APIError::OK;
|
||||
}
|
||||
|
||||
APIError APINoiseFrameHelper::check_handshake_finished_() {
|
||||
assert(state_ == State::HANDSHAKE);
|
||||
|
||||
int action = noise_handshakestate_get_action(handshake_);
|
||||
if (action == NOISE_ACTION_READ_MESSAGE || action == NOISE_ACTION_WRITE_MESSAGE)
|
||||
return APIError::OK;
|
||||
if (action != NOISE_ACTION_SPLIT) {
|
||||
state_ = State::FAILED;
|
||||
HELPER_LOG("Bad action for handshake: %d", action);
|
||||
return APIError::HANDSHAKESTATE_BAD_STATE;
|
||||
}
|
||||
int err = noise_handshakestate_split(handshake_, &send_cipher_, &recv_cipher_);
|
||||
APIError aerr = handle_noise_error_(err, "noise_handshakestate_split", APIError::HANDSHAKESTATE_SPLIT_FAILED);
|
||||
if (aerr != APIError::OK)
|
||||
return aerr;
|
||||
|
||||
frame_footer_size_ = noise_cipherstate_get_mac_length(send_cipher_);
|
||||
|
||||
HELPER_LOG("Handshake complete!");
|
||||
noise_handshakestate_free(handshake_);
|
||||
handshake_ = nullptr;
|
||||
state_ = State::DATA;
|
||||
return APIError::OK;
|
||||
}
|
||||
|
||||
APINoiseFrameHelper::~APINoiseFrameHelper() {
|
||||
if (handshake_ != nullptr) {
|
||||
noise_handshakestate_free(handshake_);
|
||||
handshake_ = nullptr;
|
||||
}
|
||||
if (send_cipher_ != nullptr) {
|
||||
noise_cipherstate_free(send_cipher_);
|
||||
send_cipher_ = nullptr;
|
||||
}
|
||||
if (recv_cipher_ != nullptr) {
|
||||
noise_cipherstate_free(recv_cipher_);
|
||||
recv_cipher_ = nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
extern "C" {
|
||||
// declare how noise generates random bytes (here with a good HWRNG based on the RF system)
|
||||
void noise_rand_bytes(void *output, size_t len) {
|
||||
if (!esphome::random_bytes(reinterpret_cast<uint8_t *>(output), len)) {
|
||||
ESP_LOGE(TAG, "Acquiring random bytes failed; rebooting");
|
||||
arch_restart();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace esphome::api
|
||||
#endif // USE_API_NOISE
|
||||
#endif // USE_API
|
68
esphome/components/api/api_frame_helper_noise.h
Normal file
68
esphome/components/api/api_frame_helper_noise.h
Normal file
@ -0,0 +1,68 @@
|
||||
#pragma once
|
||||
#include "api_frame_helper.h"
|
||||
#ifdef USE_API
|
||||
#ifdef USE_API_NOISE
|
||||
#include "noise/protocol.h"
|
||||
#include "api_noise_context.h"
|
||||
|
||||
namespace esphome::api {
|
||||
|
||||
class APINoiseFrameHelper : public APIFrameHelper {
|
||||
public:
|
||||
APINoiseFrameHelper(std::unique_ptr<socket::Socket> socket, std::shared_ptr<APINoiseContext> ctx,
|
||||
const ClientInfo *client_info)
|
||||
: APIFrameHelper(std::move(socket), client_info), ctx_(std::move(ctx)) {
|
||||
// Noise header structure:
|
||||
// Pos 0: indicator (0x01)
|
||||
// Pos 1-2: encrypted payload size (16-bit big-endian)
|
||||
// Pos 3-6: encrypted type (16-bit) + data_len (16-bit)
|
||||
// Pos 7+: actual payload data
|
||||
frame_header_padding_ = 7;
|
||||
}
|
||||
~APINoiseFrameHelper() override;
|
||||
APIError init() override;
|
||||
APIError loop() override;
|
||||
APIError read_packet(ReadPacketBuffer *buffer) override;
|
||||
APIError write_protobuf_packet(uint8_t type, ProtoWriteBuffer buffer) override;
|
||||
APIError write_protobuf_packets(ProtoWriteBuffer buffer, std::span<const PacketInfo> packets) override;
|
||||
// Get the frame header padding required by this protocol
|
||||
uint8_t frame_header_padding() override { return frame_header_padding_; }
|
||||
// Get the frame footer size required by this protocol
|
||||
uint8_t frame_footer_size() override { return frame_footer_size_; }
|
||||
|
||||
protected:
|
||||
APIError state_action_();
|
||||
APIError try_read_frame_(std::vector<uint8_t> *frame);
|
||||
APIError write_frame_(const uint8_t *data, uint16_t len);
|
||||
APIError init_handshake_();
|
||||
APIError check_handshake_finished_();
|
||||
void send_explicit_handshake_reject_(const std::string &reason);
|
||||
APIError handle_handshake_frame_error_(APIError aerr);
|
||||
APIError handle_noise_error_(int err, const char *func_name, APIError api_err);
|
||||
|
||||
// Pointers first (4 bytes each)
|
||||
NoiseHandshakeState *handshake_{nullptr};
|
||||
NoiseCipherState *send_cipher_{nullptr};
|
||||
NoiseCipherState *recv_cipher_{nullptr};
|
||||
|
||||
// Shared pointer (8 bytes on 32-bit = 4 bytes control block pointer + 4 bytes object pointer)
|
||||
std::shared_ptr<APINoiseContext> ctx_;
|
||||
|
||||
// Vector (12 bytes on 32-bit)
|
||||
std::vector<uint8_t> prologue_;
|
||||
|
||||
// NoiseProtocolId (size depends on implementation)
|
||||
NoiseProtocolId nid_;
|
||||
|
||||
// Group small types together
|
||||
// Fixed-size header buffer for noise protocol:
|
||||
// 1 byte for indicator + 2 bytes for message size (16-bit value, not varint)
|
||||
// Note: Maximum message size is UINT16_MAX (65535), with a limit of 128 bytes during handshake phase
|
||||
uint8_t rx_header_buf_[3];
|
||||
uint8_t rx_header_buf_len_ = 0;
|
||||
// 4 bytes total, no padding
|
||||
};
|
||||
|
||||
} // namespace esphome::api
|
||||
#endif // USE_API_NOISE
|
||||
#endif // USE_API
|
290
esphome/components/api/api_frame_helper_plaintext.cpp
Normal file
290
esphome/components/api/api_frame_helper_plaintext.cpp
Normal file
@ -0,0 +1,290 @@
|
||||
#include "api_frame_helper_plaintext.h"
|
||||
#ifdef USE_API
|
||||
#ifdef USE_API_PLAINTEXT
|
||||
#include "api_connection.h" // For ClientInfo struct
|
||||
#include "esphome/core/application.h"
|
||||
#include "esphome/core/hal.h"
|
||||
#include "esphome/core/helpers.h"
|
||||
#include "esphome/core/log.h"
|
||||
#include "proto.h"
|
||||
#include <cstring>
|
||||
#include <cinttypes>
|
||||
|
||||
namespace esphome::api {
|
||||
|
||||
static const char *const TAG = "api.plaintext";
|
||||
|
||||
#define HELPER_LOG(msg, ...) ESP_LOGVV(TAG, "%s: " msg, this->client_info_->get_combined_info().c_str(), ##__VA_ARGS__)
|
||||
|
||||
#ifdef HELPER_LOG_PACKETS
|
||||
#define LOG_PACKET_RECEIVED(buffer) ESP_LOGVV(TAG, "Received frame: %s", format_hex_pretty(buffer).c_str())
|
||||
#define LOG_PACKET_SENDING(data, len) ESP_LOGVV(TAG, "Sending raw: %s", format_hex_pretty(data, len).c_str())
|
||||
#else
|
||||
#define LOG_PACKET_RECEIVED(buffer) ((void) 0)
|
||||
#define LOG_PACKET_SENDING(data, len) ((void) 0)
|
||||
#endif
|
||||
|
||||
/// Initialize the frame helper, returns OK if successful.
|
||||
APIError APIPlaintextFrameHelper::init() {
|
||||
APIError err = init_common_();
|
||||
if (err != APIError::OK) {
|
||||
return err;
|
||||
}
|
||||
|
||||
state_ = State::DATA;
|
||||
return APIError::OK;
|
||||
}
|
||||
APIError APIPlaintextFrameHelper::loop() {
|
||||
if (state_ != State::DATA) {
|
||||
return APIError::BAD_STATE;
|
||||
}
|
||||
// Use base class implementation for buffer sending
|
||||
return APIFrameHelper::loop();
|
||||
}
|
||||
|
||||
/** Read a packet into the rx_buf_. If successful, stores frame data in the frame parameter
|
||||
*
|
||||
* @param frame: The struct to hold the frame information in.
|
||||
* msg: store the parsed frame in that struct
|
||||
*
|
||||
* @return See APIError
|
||||
*
|
||||
* error API_ERROR_BAD_INDICATOR: Bad indicator byte at start of frame.
|
||||
*/
|
||||
APIError APIPlaintextFrameHelper::try_read_frame_(std::vector<uint8_t> *frame) {
|
||||
if (frame == nullptr) {
|
||||
HELPER_LOG("Bad argument for try_read_frame_");
|
||||
return APIError::BAD_ARG;
|
||||
}
|
||||
|
||||
// read header
|
||||
while (!rx_header_parsed_) {
|
||||
// Now that we know when the socket is ready, we can read up to 3 bytes
|
||||
// into the rx_header_buf_ before we have to switch back to reading
|
||||
// one byte at a time to ensure we don't read past the message and
|
||||
// into the next one.
|
||||
|
||||
// Read directly into rx_header_buf_ at the current position
|
||||
// Try to get to at least 3 bytes total (indicator + 2 varint bytes), then read one byte at a time
|
||||
ssize_t received =
|
||||
this->socket_->read(&rx_header_buf_[rx_header_buf_pos_], rx_header_buf_pos_ < 3 ? 3 - rx_header_buf_pos_ : 1);
|
||||
APIError err = handle_socket_read_result_(received);
|
||||
if (err != APIError::OK) {
|
||||
return err;
|
||||
}
|
||||
|
||||
// If this was the first read, validate the indicator byte
|
||||
if (rx_header_buf_pos_ == 0 && received > 0) {
|
||||
if (rx_header_buf_[0] != 0x00) {
|
||||
state_ = State::FAILED;
|
||||
HELPER_LOG("Bad indicator byte %u", rx_header_buf_[0]);
|
||||
return APIError::BAD_INDICATOR;
|
||||
}
|
||||
}
|
||||
|
||||
rx_header_buf_pos_ += received;
|
||||
|
||||
// Check for buffer overflow
|
||||
if (rx_header_buf_pos_ >= sizeof(rx_header_buf_)) {
|
||||
state_ = State::FAILED;
|
||||
HELPER_LOG("Header buffer overflow");
|
||||
return APIError::BAD_DATA_PACKET;
|
||||
}
|
||||
|
||||
// Need at least 3 bytes total (indicator + 2 varint bytes) before trying to parse
|
||||
if (rx_header_buf_pos_ < 3) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// At this point, we have at least 3 bytes total:
|
||||
// - Validated indicator byte (0x00) stored at position 0
|
||||
// - At least 2 bytes in the buffer for the varints
|
||||
// Buffer layout:
|
||||
// [0]: indicator byte (0x00)
|
||||
// [1-3]: Message size varint (variable length)
|
||||
// - 2 bytes would only allow up to 16383, which is less than noise's UINT16_MAX (65535)
|
||||
// - 3 bytes allows up to 2097151, ensuring we support at least as much as noise
|
||||
// [2-5]: Message type varint (variable length)
|
||||
// We now attempt to parse both varints. If either is incomplete,
|
||||
// we'll continue reading more bytes.
|
||||
|
||||
// Skip indicator byte at position 0
|
||||
uint8_t varint_pos = 1;
|
||||
uint32_t consumed = 0;
|
||||
|
||||
auto msg_size_varint = ProtoVarInt::parse(&rx_header_buf_[varint_pos], rx_header_buf_pos_ - varint_pos, &consumed);
|
||||
if (!msg_size_varint.has_value()) {
|
||||
// not enough data there yet
|
||||
continue;
|
||||
}
|
||||
|
||||
if (msg_size_varint->as_uint32() > std::numeric_limits<uint16_t>::max()) {
|
||||
state_ = State::FAILED;
|
||||
HELPER_LOG("Bad packet: message size %" PRIu32 " exceeds maximum %u", msg_size_varint->as_uint32(),
|
||||
std::numeric_limits<uint16_t>::max());
|
||||
return APIError::BAD_DATA_PACKET;
|
||||
}
|
||||
rx_header_parsed_len_ = msg_size_varint->as_uint16();
|
||||
|
||||
// Move to next varint position
|
||||
varint_pos += consumed;
|
||||
|
||||
auto msg_type_varint = ProtoVarInt::parse(&rx_header_buf_[varint_pos], rx_header_buf_pos_ - varint_pos, &consumed);
|
||||
if (!msg_type_varint.has_value()) {
|
||||
// not enough data there yet
|
||||
continue;
|
||||
}
|
||||
if (msg_type_varint->as_uint32() > std::numeric_limits<uint16_t>::max()) {
|
||||
state_ = State::FAILED;
|
||||
HELPER_LOG("Bad packet: message type %" PRIu32 " exceeds maximum %u", msg_type_varint->as_uint32(),
|
||||
std::numeric_limits<uint16_t>::max());
|
||||
return APIError::BAD_DATA_PACKET;
|
||||
}
|
||||
rx_header_parsed_type_ = msg_type_varint->as_uint16();
|
||||
rx_header_parsed_ = true;
|
||||
}
|
||||
// header reading done
|
||||
|
||||
// reserve space for body
|
||||
if (rx_buf_.size() != rx_header_parsed_len_) {
|
||||
rx_buf_.resize(rx_header_parsed_len_);
|
||||
}
|
||||
|
||||
if (rx_buf_len_ < rx_header_parsed_len_) {
|
||||
// more data to read
|
||||
uint16_t to_read = rx_header_parsed_len_ - rx_buf_len_;
|
||||
ssize_t received = this->socket_->read(&rx_buf_[rx_buf_len_], to_read);
|
||||
APIError err = handle_socket_read_result_(received);
|
||||
if (err != APIError::OK) {
|
||||
return err;
|
||||
}
|
||||
rx_buf_len_ += static_cast<uint16_t>(received);
|
||||
if (static_cast<uint16_t>(received) != to_read) {
|
||||
// not all read
|
||||
return APIError::WOULD_BLOCK;
|
||||
}
|
||||
}
|
||||
|
||||
LOG_PACKET_RECEIVED(rx_buf_);
|
||||
*frame = std::move(rx_buf_);
|
||||
// consume msg
|
||||
rx_buf_ = {};
|
||||
rx_buf_len_ = 0;
|
||||
rx_header_buf_pos_ = 0;
|
||||
rx_header_parsed_ = false;
|
||||
return APIError::OK;
|
||||
}
|
||||
APIError APIPlaintextFrameHelper::read_packet(ReadPacketBuffer *buffer) {
|
||||
APIError aerr;
|
||||
|
||||
if (state_ != State::DATA) {
|
||||
return APIError::WOULD_BLOCK;
|
||||
}
|
||||
|
||||
std::vector<uint8_t> frame;
|
||||
aerr = try_read_frame_(&frame);
|
||||
if (aerr != APIError::OK) {
|
||||
if (aerr == APIError::BAD_INDICATOR) {
|
||||
// Make sure to tell the remote that we don't
|
||||
// understand the indicator byte so it knows
|
||||
// we do not support it.
|
||||
struct iovec iov[1];
|
||||
// The \x00 first byte is the marker for plaintext.
|
||||
//
|
||||
// The remote will know how to handle the indicator byte,
|
||||
// but it likely won't understand the rest of the message.
|
||||
//
|
||||
// We must send at least 3 bytes to be read, so we add
|
||||
// a message after the indicator byte to ensures its long
|
||||
// enough and can aid in debugging.
|
||||
const char msg[] = "\x00"
|
||||
"Bad indicator byte";
|
||||
iov[0].iov_base = (void *) msg;
|
||||
iov[0].iov_len = 19;
|
||||
this->write_raw_(iov, 1, 19);
|
||||
}
|
||||
return aerr;
|
||||
}
|
||||
|
||||
buffer->container = std::move(frame);
|
||||
buffer->data_offset = 0;
|
||||
buffer->data_len = rx_header_parsed_len_;
|
||||
buffer->type = rx_header_parsed_type_;
|
||||
return APIError::OK;
|
||||
}
|
||||
APIError APIPlaintextFrameHelper::write_protobuf_packet(uint8_t type, ProtoWriteBuffer buffer) {
|
||||
PacketInfo packet{type, 0, static_cast<uint16_t>(buffer.get_buffer()->size() - frame_header_padding_)};
|
||||
return write_protobuf_packets(buffer, std::span<const PacketInfo>(&packet, 1));
|
||||
}
|
||||
|
||||
APIError APIPlaintextFrameHelper::write_protobuf_packets(ProtoWriteBuffer buffer, std::span<const PacketInfo> packets) {
|
||||
if (state_ != State::DATA) {
|
||||
return APIError::BAD_STATE;
|
||||
}
|
||||
|
||||
if (packets.empty()) {
|
||||
return APIError::OK;
|
||||
}
|
||||
|
||||
std::vector<uint8_t> *raw_buffer = buffer.get_buffer();
|
||||
uint8_t *buffer_data = raw_buffer->data(); // Cache buffer pointer
|
||||
|
||||
this->reusable_iovs_.clear();
|
||||
this->reusable_iovs_.reserve(packets.size());
|
||||
uint16_t total_write_len = 0;
|
||||
|
||||
for (const auto &packet : packets) {
|
||||
// Calculate varint sizes for header layout
|
||||
uint8_t size_varint_len = api::ProtoSize::varint(static_cast<uint32_t>(packet.payload_size));
|
||||
uint8_t type_varint_len = api::ProtoSize::varint(static_cast<uint32_t>(packet.message_type));
|
||||
uint8_t total_header_len = 1 + size_varint_len + type_varint_len;
|
||||
|
||||
// Calculate where to start writing the header
|
||||
// The header starts at the latest possible position to minimize unused padding
|
||||
//
|
||||
// Example 1 (small values): total_header_len = 3, header_offset = 6 - 3 = 3
|
||||
// [0-2] - Unused padding
|
||||
// [3] - 0x00 indicator byte
|
||||
// [4] - Payload size varint (1 byte, for sizes 0-127)
|
||||
// [5] - Message type varint (1 byte, for types 0-127)
|
||||
// [6...] - Actual payload data
|
||||
//
|
||||
// Example 2 (medium values): total_header_len = 4, header_offset = 6 - 4 = 2
|
||||
// [0-1] - Unused padding
|
||||
// [2] - 0x00 indicator byte
|
||||
// [3-4] - Payload size varint (2 bytes, for sizes 128-16383)
|
||||
// [5] - Message type varint (1 byte, for types 0-127)
|
||||
// [6...] - Actual payload data
|
||||
//
|
||||
// Example 3 (large values): total_header_len = 6, header_offset = 6 - 6 = 0
|
||||
// [0] - 0x00 indicator byte
|
||||
// [1-3] - Payload size varint (3 bytes, for sizes 16384-2097151)
|
||||
// [4-5] - Message type varint (2 bytes, for types 128-32767)
|
||||
// [6...] - Actual payload data
|
||||
//
|
||||
// The message starts at offset + frame_header_padding_
|
||||
// So we write the header starting at offset + frame_header_padding_ - total_header_len
|
||||
uint8_t *buf_start = buffer_data + packet.offset;
|
||||
uint32_t header_offset = frame_header_padding_ - total_header_len;
|
||||
|
||||
// Write the plaintext header
|
||||
buf_start[header_offset] = 0x00; // indicator
|
||||
|
||||
// Encode varints directly into buffer
|
||||
ProtoVarInt(packet.payload_size).encode_to_buffer_unchecked(buf_start + header_offset + 1, size_varint_len);
|
||||
ProtoVarInt(packet.message_type)
|
||||
.encode_to_buffer_unchecked(buf_start + header_offset + 1 + size_varint_len, type_varint_len);
|
||||
|
||||
// Add iovec for this packet (header + payload)
|
||||
size_t packet_len = static_cast<size_t>(total_header_len + packet.payload_size);
|
||||
this->reusable_iovs_.push_back({buf_start + header_offset, packet_len});
|
||||
total_write_len += packet_len;
|
||||
}
|
||||
|
||||
// Send all packets in one writev call
|
||||
return write_raw_(this->reusable_iovs_.data(), this->reusable_iovs_.size(), total_write_len);
|
||||
}
|
||||
|
||||
} // namespace esphome::api
|
||||
#endif // USE_API_PLAINTEXT
|
||||
#endif // USE_API
|
53
esphome/components/api/api_frame_helper_plaintext.h
Normal file
53
esphome/components/api/api_frame_helper_plaintext.h
Normal file
@ -0,0 +1,53 @@
|
||||
#pragma once
|
||||
#include "api_frame_helper.h"
|
||||
#ifdef USE_API
|
||||
#ifdef USE_API_PLAINTEXT
|
||||
|
||||
namespace esphome::api {
|
||||
|
||||
class APIPlaintextFrameHelper : public APIFrameHelper {
|
||||
public:
|
||||
APIPlaintextFrameHelper(std::unique_ptr<socket::Socket> socket, const ClientInfo *client_info)
|
||||
: APIFrameHelper(std::move(socket), client_info) {
|
||||
// Plaintext header structure (worst case):
|
||||
// Pos 0: indicator (0x00)
|
||||
// Pos 1-3: payload size varint (up to 3 bytes)
|
||||
// Pos 4-5: message type varint (up to 2 bytes)
|
||||
// Pos 6+: actual payload data
|
||||
frame_header_padding_ = 6;
|
||||
}
|
||||
~APIPlaintextFrameHelper() override = default;
|
||||
APIError init() override;
|
||||
APIError loop() override;
|
||||
APIError read_packet(ReadPacketBuffer *buffer) override;
|
||||
APIError write_protobuf_packet(uint8_t type, ProtoWriteBuffer buffer) override;
|
||||
APIError write_protobuf_packets(ProtoWriteBuffer buffer, std::span<const PacketInfo> packets) override;
|
||||
uint8_t frame_header_padding() override { return frame_header_padding_; }
|
||||
// Get the frame footer size required by this protocol
|
||||
uint8_t frame_footer_size() override { return frame_footer_size_; }
|
||||
|
||||
protected:
|
||||
APIError try_read_frame_(std::vector<uint8_t> *frame);
|
||||
|
||||
// Group 2-byte aligned types
|
||||
uint16_t rx_header_parsed_type_ = 0;
|
||||
uint16_t rx_header_parsed_len_ = 0;
|
||||
|
||||
// Group 1-byte types together
|
||||
// Fixed-size header buffer for plaintext protocol:
|
||||
// We now store the indicator byte + the two varints.
|
||||
// To match noise protocol's maximum message size (UINT16_MAX = 65535), we need:
|
||||
// 1 byte for indicator + 3 bytes for message size varint (supports up to 2097151) + 2 bytes for message type varint
|
||||
//
|
||||
// While varints could theoretically be up to 10 bytes each for 64-bit values,
|
||||
// attempting to process messages with headers that large would likely crash the
|
||||
// ESP32 due to memory constraints.
|
||||
uint8_t rx_header_buf_[6]; // 1 byte indicator + 5 bytes for varints (3 for size + 2 for type)
|
||||
uint8_t rx_header_buf_pos_ = 0;
|
||||
bool rx_header_parsed_ = false;
|
||||
// 8 bytes total, no padding needed
|
||||
};
|
||||
|
||||
} // namespace esphome::api
|
||||
#endif // USE_API_PLAINTEXT
|
||||
#endif // USE_API
|
@ -3,8 +3,7 @@
|
||||
#include <cstdint>
|
||||
#include "esphome/core/defines.h"
|
||||
|
||||
namespace esphome {
|
||||
namespace api {
|
||||
namespace esphome::api {
|
||||
|
||||
#ifdef USE_API_NOISE
|
||||
using psk_t = std::array<uint8_t, 32>;
|
||||
@ -28,5 +27,4 @@ class APINoiseContext {
|
||||
};
|
||||
#endif // USE_API_NOISE
|
||||
|
||||
} // namespace api
|
||||
} // namespace esphome
|
||||
} // namespace esphome::api
|
||||
|
@ -26,4 +26,6 @@ extend google.protobuf.MessageOptions {
|
||||
|
||||
extend google.protobuf.FieldOptions {
|
||||
optional string field_ifdef = 1042;
|
||||
optional uint32 fixed_array_size = 50007;
|
||||
optional bool no_zero_copy = 50008 [default=false];
|
||||
}
|
||||
|
File diff suppressed because it is too large
Load Diff
@ -3,11 +3,11 @@
|
||||
#pragma once
|
||||
|
||||
#include "esphome/core/defines.h"
|
||||
#include "esphome/core/string_ref.h"
|
||||
|
||||
#include "proto.h"
|
||||
|
||||
namespace esphome {
|
||||
namespace api {
|
||||
namespace esphome::api {
|
||||
|
||||
namespace enums {
|
||||
|
||||
@ -17,27 +17,13 @@ enum EntityCategory : uint32_t {
|
||||
ENTITY_CATEGORY_DIAGNOSTIC = 2,
|
||||
};
|
||||
#ifdef USE_COVER
|
||||
enum LegacyCoverState : uint32_t {
|
||||
LEGACY_COVER_STATE_OPEN = 0,
|
||||
LEGACY_COVER_STATE_CLOSED = 1,
|
||||
};
|
||||
enum CoverOperation : uint32_t {
|
||||
COVER_OPERATION_IDLE = 0,
|
||||
COVER_OPERATION_IS_OPENING = 1,
|
||||
COVER_OPERATION_IS_CLOSING = 2,
|
||||
};
|
||||
enum LegacyCoverCommand : uint32_t {
|
||||
LEGACY_COVER_COMMAND_OPEN = 0,
|
||||
LEGACY_COVER_COMMAND_CLOSE = 1,
|
||||
LEGACY_COVER_COMMAND_STOP = 2,
|
||||
};
|
||||
#endif
|
||||
#ifdef USE_FAN
|
||||
enum FanSpeed : uint32_t {
|
||||
FAN_SPEED_LOW = 0,
|
||||
FAN_SPEED_MEDIUM = 1,
|
||||
FAN_SPEED_HIGH = 2,
|
||||
};
|
||||
enum FanDirection : uint32_t {
|
||||
FAN_DIRECTION_FORWARD = 0,
|
||||
FAN_DIRECTION_REVERSE = 1,
|
||||
@ -65,11 +51,6 @@ enum SensorStateClass : uint32_t {
|
||||
STATE_CLASS_TOTAL_INCREASING = 2,
|
||||
STATE_CLASS_TOTAL = 3,
|
||||
};
|
||||
enum SensorLastResetType : uint32_t {
|
||||
LAST_RESET_NONE = 0,
|
||||
LAST_RESET_NEVER = 1,
|
||||
LAST_RESET_AUTO = 2,
|
||||
};
|
||||
#endif
|
||||
enum LogLevel : uint32_t {
|
||||
LOG_LEVEL_NONE = 0,
|
||||
@ -288,13 +269,20 @@ enum UpdateCommand : uint32_t {
|
||||
class InfoResponseProtoMessage : public ProtoMessage {
|
||||
public:
|
||||
~InfoResponseProtoMessage() override = default;
|
||||
std::string object_id{};
|
||||
StringRef object_id_ref_{};
|
||||
void set_object_id(const StringRef &ref) { this->object_id_ref_ = ref; }
|
||||
uint32_t key{0};
|
||||
std::string name{};
|
||||
StringRef name_ref_{};
|
||||
void set_name(const StringRef &ref) { this->name_ref_ = ref; }
|
||||
bool disabled_by_default{false};
|
||||
std::string icon{};
|
||||
#ifdef USE_ENTITY_ICON
|
||||
StringRef icon_ref_{};
|
||||
void set_icon(const StringRef &ref) { this->icon_ref_ = ref; }
|
||||
#endif
|
||||
enums::EntityCategory entity_category{};
|
||||
#ifdef USE_DEVICES
|
||||
uint32_t device_id{0};
|
||||
#endif
|
||||
|
||||
protected:
|
||||
};
|
||||
@ -303,7 +291,9 @@ class StateResponseProtoMessage : public ProtoMessage {
|
||||
public:
|
||||
~StateResponseProtoMessage() override = default;
|
||||
uint32_t key{0};
|
||||
#ifdef USE_DEVICES
|
||||
uint32_t device_id{0};
|
||||
#endif
|
||||
|
||||
protected:
|
||||
};
|
||||
@ -312,7 +302,9 @@ class CommandProtoMessage : public ProtoDecodableMessage {
|
||||
public:
|
||||
~CommandProtoMessage() override = default;
|
||||
uint32_t key{0};
|
||||
#ifdef USE_DEVICES
|
||||
uint32_t device_id{0};
|
||||
#endif
|
||||
|
||||
protected:
|
||||
};
|
||||
@ -343,8 +335,10 @@ class HelloResponse : public ProtoMessage {
|
||||
#endif
|
||||
uint32_t api_version_major{0};
|
||||
uint32_t api_version_minor{0};
|
||||
std::string server_info{};
|
||||
std::string name{};
|
||||
StringRef server_info_ref_{};
|
||||
void set_server_info(const StringRef &ref) { this->server_info_ref_ = ref; }
|
||||
StringRef name_ref_{};
|
||||
void set_name(const StringRef &ref) { this->name_ref_ = ref; }
|
||||
void encode(ProtoWriteBuffer buffer) const override;
|
||||
void calculate_size(uint32_t &total_size) const override;
|
||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||
@ -449,10 +443,12 @@ class DeviceInfoRequest : public ProtoDecodableMessage {
|
||||
|
||||
protected:
|
||||
};
|
||||
#ifdef USE_AREAS
|
||||
class AreaInfo : public ProtoMessage {
|
||||
public:
|
||||
uint32_t area_id{0};
|
||||
std::string name{};
|
||||
StringRef name_ref_{};
|
||||
void set_name(const StringRef &ref) { this->name_ref_ = ref; }
|
||||
void encode(ProtoWriteBuffer buffer) const override;
|
||||
void calculate_size(uint32_t &total_size) const override;
|
||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||
@ -461,10 +457,13 @@ class AreaInfo : public ProtoMessage {
|
||||
|
||||
protected:
|
||||
};
|
||||
#endif
|
||||
#ifdef USE_DEVICES
|
||||
class DeviceInfo : public ProtoMessage {
|
||||
public:
|
||||
uint32_t device_id{0};
|
||||
std::string name{};
|
||||
StringRef name_ref_{};
|
||||
void set_name(const StringRef &ref) { this->name_ref_ = ref; }
|
||||
uint32_t area_id{0};
|
||||
void encode(ProtoWriteBuffer buffer) const override;
|
||||
void calculate_size(uint32_t &total_size) const override;
|
||||
@ -474,50 +473,58 @@ class DeviceInfo : public ProtoMessage {
|
||||
|
||||
protected:
|
||||
};
|
||||
#endif
|
||||
class DeviceInfoResponse : public ProtoMessage {
|
||||
public:
|
||||
static constexpr uint8_t MESSAGE_TYPE = 10;
|
||||
static constexpr uint8_t ESTIMATED_SIZE = 219;
|
||||
static constexpr uint8_t ESTIMATED_SIZE = 211;
|
||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||
const char *message_name() const override { return "device_info_response"; }
|
||||
#endif
|
||||
#ifdef USE_API_PASSWORD
|
||||
bool uses_password{false};
|
||||
std::string name{};
|
||||
std::string mac_address{};
|
||||
std::string esphome_version{};
|
||||
std::string compilation_time{};
|
||||
std::string model{};
|
||||
#endif
|
||||
StringRef name_ref_{};
|
||||
void set_name(const StringRef &ref) { this->name_ref_ = ref; }
|
||||
StringRef mac_address_ref_{};
|
||||
void set_mac_address(const StringRef &ref) { this->mac_address_ref_ = ref; }
|
||||
StringRef esphome_version_ref_{};
|
||||
void set_esphome_version(const StringRef &ref) { this->esphome_version_ref_ = ref; }
|
||||
StringRef compilation_time_ref_{};
|
||||
void set_compilation_time(const StringRef &ref) { this->compilation_time_ref_ = ref; }
|
||||
StringRef model_ref_{};
|
||||
void set_model(const StringRef &ref) { this->model_ref_ = ref; }
|
||||
#ifdef USE_DEEP_SLEEP
|
||||
bool has_deep_sleep{false};
|
||||
#endif
|
||||
#ifdef ESPHOME_PROJECT_NAME
|
||||
std::string project_name{};
|
||||
StringRef project_name_ref_{};
|
||||
void set_project_name(const StringRef &ref) { this->project_name_ref_ = ref; }
|
||||
#endif
|
||||
#ifdef ESPHOME_PROJECT_NAME
|
||||
std::string project_version{};
|
||||
StringRef project_version_ref_{};
|
||||
void set_project_version(const StringRef &ref) { this->project_version_ref_ = ref; }
|
||||
#endif
|
||||
#ifdef USE_WEBSERVER
|
||||
uint32_t webserver_port{0};
|
||||
#endif
|
||||
#ifdef USE_BLUETOOTH_PROXY
|
||||
uint32_t legacy_bluetooth_proxy_version{0};
|
||||
#endif
|
||||
#ifdef USE_BLUETOOTH_PROXY
|
||||
uint32_t bluetooth_proxy_feature_flags{0};
|
||||
#endif
|
||||
std::string manufacturer{};
|
||||
std::string friendly_name{};
|
||||
#ifdef USE_VOICE_ASSISTANT
|
||||
uint32_t legacy_voice_assistant_version{0};
|
||||
#endif
|
||||
StringRef manufacturer_ref_{};
|
||||
void set_manufacturer(const StringRef &ref) { this->manufacturer_ref_ = ref; }
|
||||
StringRef friendly_name_ref_{};
|
||||
void set_friendly_name(const StringRef &ref) { this->friendly_name_ref_ = ref; }
|
||||
#ifdef USE_VOICE_ASSISTANT
|
||||
uint32_t voice_assistant_feature_flags{0};
|
||||
#endif
|
||||
#ifdef USE_AREAS
|
||||
std::string suggested_area{};
|
||||
StringRef suggested_area_ref_{};
|
||||
void set_suggested_area(const StringRef &ref) { this->suggested_area_ref_ = ref; }
|
||||
#endif
|
||||
#ifdef USE_BLUETOOTH_PROXY
|
||||
std::string bluetooth_mac_address{};
|
||||
StringRef bluetooth_mac_address_ref_{};
|
||||
void set_bluetooth_mac_address(const StringRef &ref) { this->bluetooth_mac_address_ref_ = ref; }
|
||||
#endif
|
||||
#ifdef USE_API_NOISE
|
||||
bool api_encryption_supported{false};
|
||||
@ -586,7 +593,8 @@ class ListEntitiesBinarySensorResponse : public InfoResponseProtoMessage {
|
||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||
const char *message_name() const override { return "list_entities_binary_sensor_response"; }
|
||||
#endif
|
||||
std::string device_class{};
|
||||
StringRef device_class_ref_{};
|
||||
void set_device_class(const StringRef &ref) { this->device_class_ref_ = ref; }
|
||||
bool is_status_binary_sensor{false};
|
||||
void encode(ProtoWriteBuffer buffer) const override;
|
||||
void calculate_size(uint32_t &total_size) const override;
|
||||
@ -625,7 +633,8 @@ class ListEntitiesCoverResponse : public InfoResponseProtoMessage {
|
||||
bool assumed_state{false};
|
||||
bool supports_position{false};
|
||||
bool supports_tilt{false};
|
||||
std::string device_class{};
|
||||
StringRef device_class_ref_{};
|
||||
void set_device_class(const StringRef &ref) { this->device_class_ref_ = ref; }
|
||||
bool supports_stop{false};
|
||||
void encode(ProtoWriteBuffer buffer) const override;
|
||||
void calculate_size(uint32_t &total_size) const override;
|
||||
@ -638,11 +647,10 @@ class ListEntitiesCoverResponse : public InfoResponseProtoMessage {
|
||||
class CoverStateResponse : public StateResponseProtoMessage {
|
||||
public:
|
||||
static constexpr uint8_t MESSAGE_TYPE = 22;
|
||||
static constexpr uint8_t ESTIMATED_SIZE = 23;
|
||||
static constexpr uint8_t ESTIMATED_SIZE = 21;
|
||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||
const char *message_name() const override { return "cover_state_response"; }
|
||||
#endif
|
||||
enums::LegacyCoverState legacy_state{};
|
||||
float position{0.0f};
|
||||
float tilt{0.0f};
|
||||
enums::CoverOperation current_operation{};
|
||||
@ -657,12 +665,10 @@ class CoverStateResponse : public StateResponseProtoMessage {
|
||||
class CoverCommandRequest : public CommandProtoMessage {
|
||||
public:
|
||||
static constexpr uint8_t MESSAGE_TYPE = 30;
|
||||
static constexpr uint8_t ESTIMATED_SIZE = 29;
|
||||
static constexpr uint8_t ESTIMATED_SIZE = 25;
|
||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||
const char *message_name() const override { return "cover_command_request"; }
|
||||
#endif
|
||||
bool has_legacy_command{false};
|
||||
enums::LegacyCoverCommand legacy_command{};
|
||||
bool has_position{false};
|
||||
float position{0.0f};
|
||||
bool has_tilt{false};
|
||||
@ -701,16 +707,16 @@ class ListEntitiesFanResponse : public InfoResponseProtoMessage {
|
||||
class FanStateResponse : public StateResponseProtoMessage {
|
||||
public:
|
||||
static constexpr uint8_t MESSAGE_TYPE = 23;
|
||||
static constexpr uint8_t ESTIMATED_SIZE = 30;
|
||||
static constexpr uint8_t ESTIMATED_SIZE = 28;
|
||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||
const char *message_name() const override { return "fan_state_response"; }
|
||||
#endif
|
||||
bool state{false};
|
||||
bool oscillating{false};
|
||||
enums::FanSpeed speed{};
|
||||
enums::FanDirection direction{};
|
||||
int32_t speed_level{0};
|
||||
std::string preset_mode{};
|
||||
StringRef preset_mode_ref_{};
|
||||
void set_preset_mode(const StringRef &ref) { this->preset_mode_ref_ = ref; }
|
||||
void encode(ProtoWriteBuffer buffer) const override;
|
||||
void calculate_size(uint32_t &total_size) const override;
|
||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||
@ -722,14 +728,12 @@ class FanStateResponse : public StateResponseProtoMessage {
|
||||
class FanCommandRequest : public CommandProtoMessage {
|
||||
public:
|
||||
static constexpr uint8_t MESSAGE_TYPE = 31;
|
||||
static constexpr uint8_t ESTIMATED_SIZE = 42;
|
||||
static constexpr uint8_t ESTIMATED_SIZE = 38;
|
||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||
const char *message_name() const override { return "fan_command_request"; }
|
||||
#endif
|
||||
bool has_state{false};
|
||||
bool state{false};
|
||||
bool has_speed{false};
|
||||
enums::FanSpeed speed{};
|
||||
bool has_oscillating{false};
|
||||
bool oscillating{false};
|
||||
bool has_direction{false};
|
||||
@ -752,15 +756,11 @@ class FanCommandRequest : public CommandProtoMessage {
|
||||
class ListEntitiesLightResponse : public InfoResponseProtoMessage {
|
||||
public:
|
||||
static constexpr uint8_t MESSAGE_TYPE = 15;
|
||||
static constexpr uint8_t ESTIMATED_SIZE = 81;
|
||||
static constexpr uint8_t ESTIMATED_SIZE = 73;
|
||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||
const char *message_name() const override { return "list_entities_light_response"; }
|
||||
#endif
|
||||
std::vector<enums::ColorMode> supported_color_modes{};
|
||||
bool legacy_supports_brightness{false};
|
||||
bool legacy_supports_rgb{false};
|
||||
bool legacy_supports_white_value{false};
|
||||
bool legacy_supports_color_temperature{false};
|
||||
float min_mireds{0.0f};
|
||||
float max_mireds{0.0f};
|
||||
std::vector<std::string> effects{};
|
||||
@ -790,7 +790,8 @@ class LightStateResponse : public StateResponseProtoMessage {
|
||||
float color_temperature{0.0f};
|
||||
float cold_white{0.0f};
|
||||
float warm_white{0.0f};
|
||||
std::string effect{};
|
||||
StringRef effect_ref_{};
|
||||
void set_effect(const StringRef &ref) { this->effect_ref_ = ref; }
|
||||
void encode(ProtoWriteBuffer buffer) const override;
|
||||
void calculate_size(uint32_t &total_size) const override;
|
||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||
@ -846,16 +847,17 @@ class LightCommandRequest : public CommandProtoMessage {
|
||||
class ListEntitiesSensorResponse : public InfoResponseProtoMessage {
|
||||
public:
|
||||
static constexpr uint8_t MESSAGE_TYPE = 16;
|
||||
static constexpr uint8_t ESTIMATED_SIZE = 68;
|
||||
static constexpr uint8_t ESTIMATED_SIZE = 66;
|
||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||
const char *message_name() const override { return "list_entities_sensor_response"; }
|
||||
#endif
|
||||
std::string unit_of_measurement{};
|
||||
StringRef unit_of_measurement_ref_{};
|
||||
void set_unit_of_measurement(const StringRef &ref) { this->unit_of_measurement_ref_ = ref; }
|
||||
int32_t accuracy_decimals{0};
|
||||
bool force_update{false};
|
||||
std::string device_class{};
|
||||
StringRef device_class_ref_{};
|
||||
void set_device_class(const StringRef &ref) { this->device_class_ref_ = ref; }
|
||||
enums::SensorStateClass state_class{};
|
||||
enums::SensorLastResetType legacy_last_reset_type{};
|
||||
void encode(ProtoWriteBuffer buffer) const override;
|
||||
void calculate_size(uint32_t &total_size) const override;
|
||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||
@ -891,7 +893,8 @@ class ListEntitiesSwitchResponse : public InfoResponseProtoMessage {
|
||||
const char *message_name() const override { return "list_entities_switch_response"; }
|
||||
#endif
|
||||
bool assumed_state{false};
|
||||
std::string device_class{};
|
||||
StringRef device_class_ref_{};
|
||||
void set_device_class(const StringRef &ref) { this->device_class_ref_ = ref; }
|
||||
void encode(ProtoWriteBuffer buffer) const override;
|
||||
void calculate_size(uint32_t &total_size) const override;
|
||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||
@ -941,7 +944,8 @@ class ListEntitiesTextSensorResponse : public InfoResponseProtoMessage {
|
||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||
const char *message_name() const override { return "list_entities_text_sensor_response"; }
|
||||
#endif
|
||||
std::string device_class{};
|
||||
StringRef device_class_ref_{};
|
||||
void set_device_class(const StringRef &ref) { this->device_class_ref_ = ref; }
|
||||
void encode(ProtoWriteBuffer buffer) const override;
|
||||
void calculate_size(uint32_t &total_size) const override;
|
||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||
@ -957,7 +961,8 @@ class TextSensorStateResponse : public StateResponseProtoMessage {
|
||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||
const char *message_name() const override { return "text_sensor_state_response"; }
|
||||
#endif
|
||||
std::string state{};
|
||||
StringRef state_ref_{};
|
||||
void set_state(const StringRef &ref) { this->state_ref_ = ref; }
|
||||
bool missing_state{false};
|
||||
void encode(ProtoWriteBuffer buffer) const override;
|
||||
void calculate_size(uint32_t &total_size) const override;
|
||||
@ -987,13 +992,17 @@ class SubscribeLogsRequest : public ProtoDecodableMessage {
|
||||
class SubscribeLogsResponse : public ProtoMessage {
|
||||
public:
|
||||
static constexpr uint8_t MESSAGE_TYPE = 29;
|
||||
static constexpr uint8_t ESTIMATED_SIZE = 13;
|
||||
static constexpr uint8_t ESTIMATED_SIZE = 11;
|
||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||
const char *message_name() const override { return "subscribe_logs_response"; }
|
||||
#endif
|
||||
enums::LogLevel level{};
|
||||
std::string message{};
|
||||
bool send_failed{false};
|
||||
const uint8_t *message_ptr_{nullptr};
|
||||
size_t message_len_{0};
|
||||
void set_message(const uint8_t *data, size_t len) {
|
||||
this->message_ptr_ = data;
|
||||
this->message_len_ = len;
|
||||
}
|
||||
void encode(ProtoWriteBuffer buffer) const override;
|
||||
void calculate_size(uint32_t &total_size) const override;
|
||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||
@ -1035,6 +1044,7 @@ class NoiseEncryptionSetKeyResponse : public ProtoMessage {
|
||||
protected:
|
||||
};
|
||||
#endif
|
||||
#ifdef USE_API_HOMEASSISTANT_SERVICES
|
||||
class SubscribeHomeassistantServicesRequest : public ProtoDecodableMessage {
|
||||
public:
|
||||
static constexpr uint8_t MESSAGE_TYPE = 34;
|
||||
@ -1050,7 +1060,8 @@ class SubscribeHomeassistantServicesRequest : public ProtoDecodableMessage {
|
||||
};
|
||||
class HomeassistantServiceMap : public ProtoMessage {
|
||||
public:
|
||||
std::string key{};
|
||||
StringRef key_ref_{};
|
||||
void set_key(const StringRef &ref) { this->key_ref_ = ref; }
|
||||
std::string value{};
|
||||
void encode(ProtoWriteBuffer buffer) const override;
|
||||
void calculate_size(uint32_t &total_size) const override;
|
||||
@ -1067,7 +1078,8 @@ class HomeassistantServiceResponse : public ProtoMessage {
|
||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||
const char *message_name() const override { return "homeassistant_service_response"; }
|
||||
#endif
|
||||
std::string service{};
|
||||
StringRef service_ref_{};
|
||||
void set_service(const StringRef &ref) { this->service_ref_ = ref; }
|
||||
std::vector<HomeassistantServiceMap> data{};
|
||||
std::vector<HomeassistantServiceMap> data_template{};
|
||||
std::vector<HomeassistantServiceMap> variables{};
|
||||
@ -1080,6 +1092,8 @@ class HomeassistantServiceResponse : public ProtoMessage {
|
||||
|
||||
protected:
|
||||
};
|
||||
#endif
|
||||
#ifdef USE_API_HOMEASSISTANT_STATES
|
||||
class SubscribeHomeAssistantStatesRequest : public ProtoDecodableMessage {
|
||||
public:
|
||||
static constexpr uint8_t MESSAGE_TYPE = 38;
|
||||
@ -1100,8 +1114,10 @@ class SubscribeHomeAssistantStateResponse : public ProtoMessage {
|
||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||
const char *message_name() const override { return "subscribe_home_assistant_state_response"; }
|
||||
#endif
|
||||
std::string entity_id{};
|
||||
std::string attribute{};
|
||||
StringRef entity_id_ref_{};
|
||||
void set_entity_id(const StringRef &ref) { this->entity_id_ref_ = ref; }
|
||||
StringRef attribute_ref_{};
|
||||
void set_attribute(const StringRef &ref) { this->attribute_ref_ = ref; }
|
||||
bool once{false};
|
||||
void encode(ProtoWriteBuffer buffer) const override;
|
||||
void calculate_size(uint32_t &total_size) const override;
|
||||
@ -1128,6 +1144,7 @@ class HomeAssistantStateResponse : public ProtoDecodableMessage {
|
||||
protected:
|
||||
bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override;
|
||||
};
|
||||
#endif
|
||||
class GetTimeRequest : public ProtoDecodableMessage {
|
||||
public:
|
||||
static constexpr uint8_t MESSAGE_TYPE = 36;
|
||||
@ -1161,7 +1178,8 @@ class GetTimeResponse : public ProtoDecodableMessage {
|
||||
#ifdef USE_API_SERVICES
|
||||
class ListEntitiesServicesArgument : public ProtoMessage {
|
||||
public:
|
||||
std::string name{};
|
||||
StringRef name_ref_{};
|
||||
void set_name(const StringRef &ref) { this->name_ref_ = ref; }
|
||||
enums::ServiceArgType type{};
|
||||
void encode(ProtoWriteBuffer buffer) const override;
|
||||
void calculate_size(uint32_t &total_size) const override;
|
||||
@ -1178,7 +1196,8 @@ class ListEntitiesServicesResponse : public ProtoMessage {
|
||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||
const char *message_name() const override { return "list_entities_services_response"; }
|
||||
#endif
|
||||
std::string name{};
|
||||
StringRef name_ref_{};
|
||||
void set_name(const StringRef &ref) { this->name_ref_ = ref; }
|
||||
uint32_t key{0};
|
||||
std::vector<ListEntitiesServicesArgument> args{};
|
||||
void encode(ProtoWriteBuffer buffer) const override;
|
||||
@ -1250,7 +1269,12 @@ class CameraImageResponse : public StateResponseProtoMessage {
|
||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||
const char *message_name() const override { return "camera_image_response"; }
|
||||
#endif
|
||||
std::string data{};
|
||||
const uint8_t *data_ptr_{nullptr};
|
||||
size_t data_len_{0};
|
||||
void set_data(const uint8_t *data, size_t len) {
|
||||
this->data_ptr_ = data;
|
||||
this->data_len_ = len;
|
||||
}
|
||||
bool done{false};
|
||||
void encode(ProtoWriteBuffer buffer) const override;
|
||||
void calculate_size(uint32_t &total_size) const override;
|
||||
@ -1281,7 +1305,7 @@ class CameraImageRequest : public ProtoDecodableMessage {
|
||||
class ListEntitiesClimateResponse : public InfoResponseProtoMessage {
|
||||
public:
|
||||
static constexpr uint8_t MESSAGE_TYPE = 46;
|
||||
static constexpr uint8_t ESTIMATED_SIZE = 147;
|
||||
static constexpr uint8_t ESTIMATED_SIZE = 145;
|
||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||
const char *message_name() const override { return "list_entities_climate_response"; }
|
||||
#endif
|
||||
@ -1291,7 +1315,6 @@ class ListEntitiesClimateResponse : public InfoResponseProtoMessage {
|
||||
float visual_min_temperature{0.0f};
|
||||
float visual_max_temperature{0.0f};
|
||||
float visual_target_temperature_step{0.0f};
|
||||
bool legacy_supports_away{false};
|
||||
bool supports_action{false};
|
||||
std::vector<enums::ClimateFanMode> supported_fan_modes{};
|
||||
std::vector<enums::ClimateSwingMode> supported_swing_modes{};
|
||||
@ -1314,7 +1337,7 @@ class ListEntitiesClimateResponse : public InfoResponseProtoMessage {
|
||||
class ClimateStateResponse : public StateResponseProtoMessage {
|
||||
public:
|
||||
static constexpr uint8_t MESSAGE_TYPE = 47;
|
||||
static constexpr uint8_t ESTIMATED_SIZE = 70;
|
||||
static constexpr uint8_t ESTIMATED_SIZE = 68;
|
||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||
const char *message_name() const override { return "climate_state_response"; }
|
||||
#endif
|
||||
@ -1323,13 +1346,14 @@ class ClimateStateResponse : public StateResponseProtoMessage {
|
||||
float target_temperature{0.0f};
|
||||
float target_temperature_low{0.0f};
|
||||
float target_temperature_high{0.0f};
|
||||
bool unused_legacy_away{false};
|
||||
enums::ClimateAction action{};
|
||||
enums::ClimateFanMode fan_mode{};
|
||||
enums::ClimateSwingMode swing_mode{};
|
||||
std::string custom_fan_mode{};
|
||||
StringRef custom_fan_mode_ref_{};
|
||||
void set_custom_fan_mode(const StringRef &ref) { this->custom_fan_mode_ref_ = ref; }
|
||||
enums::ClimatePreset preset{};
|
||||
std::string custom_preset{};
|
||||
StringRef custom_preset_ref_{};
|
||||
void set_custom_preset(const StringRef &ref) { this->custom_preset_ref_ = ref; }
|
||||
float current_humidity{0.0f};
|
||||
float target_humidity{0.0f};
|
||||
void encode(ProtoWriteBuffer buffer) const override;
|
||||
@ -1343,7 +1367,7 @@ class ClimateStateResponse : public StateResponseProtoMessage {
|
||||
class ClimateCommandRequest : public CommandProtoMessage {
|
||||
public:
|
||||
static constexpr uint8_t MESSAGE_TYPE = 48;
|
||||
static constexpr uint8_t ESTIMATED_SIZE = 88;
|
||||
static constexpr uint8_t ESTIMATED_SIZE = 84;
|
||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||
const char *message_name() const override { return "climate_command_request"; }
|
||||
#endif
|
||||
@ -1355,8 +1379,6 @@ class ClimateCommandRequest : public CommandProtoMessage {
|
||||
float target_temperature_low{0.0f};
|
||||
bool has_target_temperature_high{false};
|
||||
float target_temperature_high{0.0f};
|
||||
bool unused_has_legacy_away{false};
|
||||
bool unused_legacy_away{false};
|
||||
bool has_fan_mode{false};
|
||||
enums::ClimateFanMode fan_mode{};
|
||||
bool has_swing_mode{false};
|
||||
@ -1390,9 +1412,11 @@ class ListEntitiesNumberResponse : public InfoResponseProtoMessage {
|
||||
float min_value{0.0f};
|
||||
float max_value{0.0f};
|
||||
float step{0.0f};
|
||||
std::string unit_of_measurement{};
|
||||
StringRef unit_of_measurement_ref_{};
|
||||
void set_unit_of_measurement(const StringRef &ref) { this->unit_of_measurement_ref_ = ref; }
|
||||
enums::NumberMode mode{};
|
||||
std::string device_class{};
|
||||
StringRef device_class_ref_{};
|
||||
void set_device_class(const StringRef &ref) { this->device_class_ref_ = ref; }
|
||||
void encode(ProtoWriteBuffer buffer) const override;
|
||||
void calculate_size(uint32_t &total_size) const override;
|
||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||
@ -1459,7 +1483,8 @@ class SelectStateResponse : public StateResponseProtoMessage {
|
||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||
const char *message_name() const override { return "select_state_response"; }
|
||||
#endif
|
||||
std::string state{};
|
||||
StringRef state_ref_{};
|
||||
void set_state(const StringRef &ref) { this->state_ref_ = ref; }
|
||||
bool missing_state{false};
|
||||
void encode(ProtoWriteBuffer buffer) const override;
|
||||
void calculate_size(uint32_t &total_size) const override;
|
||||
@ -1558,7 +1583,8 @@ class ListEntitiesLockResponse : public InfoResponseProtoMessage {
|
||||
bool assumed_state{false};
|
||||
bool supports_open{false};
|
||||
bool requires_code{false};
|
||||
std::string code_format{};
|
||||
StringRef code_format_ref_{};
|
||||
void set_code_format(const StringRef &ref) { this->code_format_ref_ = ref; }
|
||||
void encode(ProtoWriteBuffer buffer) const override;
|
||||
void calculate_size(uint32_t &total_size) const override;
|
||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||
@ -1611,7 +1637,8 @@ class ListEntitiesButtonResponse : public InfoResponseProtoMessage {
|
||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||
const char *message_name() const override { return "list_entities_button_response"; }
|
||||
#endif
|
||||
std::string device_class{};
|
||||
StringRef device_class_ref_{};
|
||||
void set_device_class(const StringRef &ref) { this->device_class_ref_ = ref; }
|
||||
void encode(ProtoWriteBuffer buffer) const override;
|
||||
void calculate_size(uint32_t &total_size) const override;
|
||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||
@ -1639,7 +1666,8 @@ class ButtonCommandRequest : public CommandProtoMessage {
|
||||
#ifdef USE_MEDIA_PLAYER
|
||||
class MediaPlayerSupportedFormat : public ProtoMessage {
|
||||
public:
|
||||
std::string format{};
|
||||
StringRef format_ref_{};
|
||||
void set_format(const StringRef &ref) { this->format_ref_ = ref; }
|
||||
uint32_t sample_rate{0};
|
||||
uint32_t num_channels{0};
|
||||
enums::MediaPlayerFormatPurpose purpose{};
|
||||
@ -1728,47 +1756,13 @@ class SubscribeBluetoothLEAdvertisementsRequest : public ProtoDecodableMessage {
|
||||
protected:
|
||||
bool decode_varint(uint32_t field_id, ProtoVarInt value) override;
|
||||
};
|
||||
class BluetoothServiceData : public ProtoMessage {
|
||||
public:
|
||||
std::string uuid{};
|
||||
std::vector<uint32_t> legacy_data{};
|
||||
std::string data{};
|
||||
void encode(ProtoWriteBuffer buffer) const override;
|
||||
void calculate_size(uint32_t &total_size) const override;
|
||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||
void dump_to(std::string &out) const override;
|
||||
#endif
|
||||
|
||||
protected:
|
||||
};
|
||||
class BluetoothLEAdvertisementResponse : public ProtoMessage {
|
||||
public:
|
||||
static constexpr uint8_t MESSAGE_TYPE = 67;
|
||||
static constexpr uint8_t ESTIMATED_SIZE = 107;
|
||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||
const char *message_name() const override { return "bluetooth_le_advertisement_response"; }
|
||||
#endif
|
||||
uint64_t address{0};
|
||||
std::string name{};
|
||||
int32_t rssi{0};
|
||||
std::vector<std::string> service_uuids{};
|
||||
std::vector<BluetoothServiceData> service_data{};
|
||||
std::vector<BluetoothServiceData> manufacturer_data{};
|
||||
uint32_t address_type{0};
|
||||
void encode(ProtoWriteBuffer buffer) const override;
|
||||
void calculate_size(uint32_t &total_size) const override;
|
||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||
void dump_to(std::string &out) const override;
|
||||
#endif
|
||||
|
||||
protected:
|
||||
};
|
||||
class BluetoothLERawAdvertisement : public ProtoMessage {
|
||||
public:
|
||||
uint64_t address{0};
|
||||
int32_t rssi{0};
|
||||
uint32_t address_type{0};
|
||||
std::string data{};
|
||||
uint8_t data[62]{};
|
||||
uint8_t data_len{0};
|
||||
void encode(ProtoWriteBuffer buffer) const override;
|
||||
void calculate_size(uint32_t &total_size) const override;
|
||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||
@ -1847,7 +1841,7 @@ class BluetoothGATTGetServicesRequest : public ProtoDecodableMessage {
|
||||
};
|
||||
class BluetoothGATTDescriptor : public ProtoMessage {
|
||||
public:
|
||||
std::vector<uint64_t> uuid{};
|
||||
std::array<uint64_t, 2> uuid{};
|
||||
uint32_t handle{0};
|
||||
void encode(ProtoWriteBuffer buffer) const override;
|
||||
void calculate_size(uint32_t &total_size) const override;
|
||||
@ -1859,7 +1853,7 @@ class BluetoothGATTDescriptor : public ProtoMessage {
|
||||
};
|
||||
class BluetoothGATTCharacteristic : public ProtoMessage {
|
||||
public:
|
||||
std::vector<uint64_t> uuid{};
|
||||
std::array<uint64_t, 2> uuid{};
|
||||
uint32_t handle{0};
|
||||
uint32_t properties{0};
|
||||
std::vector<BluetoothGATTDescriptor> descriptors{};
|
||||
@ -1873,7 +1867,7 @@ class BluetoothGATTCharacteristic : public ProtoMessage {
|
||||
};
|
||||
class BluetoothGATTService : public ProtoMessage {
|
||||
public:
|
||||
std::vector<uint64_t> uuid{};
|
||||
std::array<uint64_t, 2> uuid{};
|
||||
uint32_t handle{0};
|
||||
std::vector<BluetoothGATTCharacteristic> characteristics{};
|
||||
void encode(ProtoWriteBuffer buffer) const override;
|
||||
@ -1887,12 +1881,12 @@ class BluetoothGATTService : public ProtoMessage {
|
||||
class BluetoothGATTGetServicesResponse : public ProtoMessage {
|
||||
public:
|
||||
static constexpr uint8_t MESSAGE_TYPE = 71;
|
||||
static constexpr uint8_t ESTIMATED_SIZE = 38;
|
||||
static constexpr uint8_t ESTIMATED_SIZE = 21;
|
||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||
const char *message_name() const override { return "bluetooth_gatt_get_services_response"; }
|
||||
#endif
|
||||
uint64_t address{0};
|
||||
std::vector<BluetoothGATTService> services{};
|
||||
std::array<BluetoothGATTService, 1> services{};
|
||||
void encode(ProtoWriteBuffer buffer) const override;
|
||||
void calculate_size(uint32_t &total_size) const override;
|
||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||
@ -1942,7 +1936,12 @@ class BluetoothGATTReadResponse : public ProtoMessage {
|
||||
#endif
|
||||
uint64_t address{0};
|
||||
uint32_t handle{0};
|
||||
std::string data{};
|
||||
const uint8_t *data_ptr_{nullptr};
|
||||
size_t data_len_{0};
|
||||
void set_data(const uint8_t *data, size_t len) {
|
||||
this->data_ptr_ = data;
|
||||
this->data_len_ = len;
|
||||
}
|
||||
void encode(ProtoWriteBuffer buffer) const override;
|
||||
void calculate_size(uint32_t &total_size) const override;
|
||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||
@ -2030,7 +2029,12 @@ class BluetoothGATTNotifyDataResponse : public ProtoMessage {
|
||||
#endif
|
||||
uint64_t address{0};
|
||||
uint32_t handle{0};
|
||||
std::string data{};
|
||||
const uint8_t *data_ptr_{nullptr};
|
||||
size_t data_len_{0};
|
||||
void set_data(const uint8_t *data, size_t len) {
|
||||
this->data_ptr_ = data;
|
||||
this->data_len_ = len;
|
||||
}
|
||||
void encode(ProtoWriteBuffer buffer) const override;
|
||||
void calculate_size(uint32_t &total_size) const override;
|
||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||
@ -2260,10 +2264,12 @@ class VoiceAssistantRequest : public ProtoMessage {
|
||||
const char *message_name() const override { return "voice_assistant_request"; }
|
||||
#endif
|
||||
bool start{false};
|
||||
std::string conversation_id{};
|
||||
StringRef conversation_id_ref_{};
|
||||
void set_conversation_id(const StringRef &ref) { this->conversation_id_ref_ = ref; }
|
||||
uint32_t flags{0};
|
||||
VoiceAssistantAudioSettings audio_settings{};
|
||||
std::string wake_word_phrase{};
|
||||
StringRef wake_word_phrase_ref_{};
|
||||
void set_wake_word_phrase(const StringRef &ref) { this->wake_word_phrase_ref_ = ref; }
|
||||
void encode(ProtoWriteBuffer buffer) const override;
|
||||
void calculate_size(uint32_t &total_size) const override;
|
||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||
@ -2324,6 +2330,12 @@ class VoiceAssistantAudio : public ProtoDecodableMessage {
|
||||
const char *message_name() const override { return "voice_assistant_audio"; }
|
||||
#endif
|
||||
std::string data{};
|
||||
const uint8_t *data_ptr_{nullptr};
|
||||
size_t data_len_{0};
|
||||
void set_data(const uint8_t *data, size_t len) {
|
||||
this->data_ptr_ = data;
|
||||
this->data_len_ = len;
|
||||
}
|
||||
bool end{false};
|
||||
void encode(ProtoWriteBuffer buffer) const override;
|
||||
void calculate_size(uint32_t &total_size) const override;
|
||||
@ -2393,8 +2405,10 @@ class VoiceAssistantAnnounceFinished : public ProtoMessage {
|
||||
};
|
||||
class VoiceAssistantWakeWord : public ProtoMessage {
|
||||
public:
|
||||
std::string id{};
|
||||
std::string wake_word{};
|
||||
StringRef id_ref_{};
|
||||
void set_id(const StringRef &ref) { this->id_ref_ = ref; }
|
||||
StringRef wake_word_ref_{};
|
||||
void set_wake_word(const StringRef &ref) { this->wake_word_ref_ = ref; }
|
||||
std::vector<std::string> trained_languages{};
|
||||
void encode(ProtoWriteBuffer buffer) const override;
|
||||
void calculate_size(uint32_t &total_size) const override;
|
||||
@ -2515,7 +2529,8 @@ class ListEntitiesTextResponse : public InfoResponseProtoMessage {
|
||||
#endif
|
||||
uint32_t min_length{0};
|
||||
uint32_t max_length{0};
|
||||
std::string pattern{};
|
||||
StringRef pattern_ref_{};
|
||||
void set_pattern(const StringRef &ref) { this->pattern_ref_ = ref; }
|
||||
enums::TextMode mode{};
|
||||
void encode(ProtoWriteBuffer buffer) const override;
|
||||
void calculate_size(uint32_t &total_size) const override;
|
||||
@ -2532,7 +2547,8 @@ class TextStateResponse : public StateResponseProtoMessage {
|
||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||
const char *message_name() const override { return "text_state_response"; }
|
||||
#endif
|
||||
std::string state{};
|
||||
StringRef state_ref_{};
|
||||
void set_state(const StringRef &ref) { this->state_ref_ = ref; }
|
||||
bool missing_state{false};
|
||||
void encode(ProtoWriteBuffer buffer) const override;
|
||||
void calculate_size(uint32_t &total_size) const override;
|
||||
@ -2676,7 +2692,8 @@ class ListEntitiesEventResponse : public InfoResponseProtoMessage {
|
||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||
const char *message_name() const override { return "list_entities_event_response"; }
|
||||
#endif
|
||||
std::string device_class{};
|
||||
StringRef device_class_ref_{};
|
||||
void set_device_class(const StringRef &ref) { this->device_class_ref_ = ref; }
|
||||
std::vector<std::string> event_types{};
|
||||
void encode(ProtoWriteBuffer buffer) const override;
|
||||
void calculate_size(uint32_t &total_size) const override;
|
||||
@ -2693,7 +2710,8 @@ class EventResponse : public StateResponseProtoMessage {
|
||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||
const char *message_name() const override { return "event_response"; }
|
||||
#endif
|
||||
std::string event_type{};
|
||||
StringRef event_type_ref_{};
|
||||
void set_event_type(const StringRef &ref) { this->event_type_ref_ = ref; }
|
||||
void encode(ProtoWriteBuffer buffer) const override;
|
||||
void calculate_size(uint32_t &total_size) const override;
|
||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||
@ -2711,7 +2729,8 @@ class ListEntitiesValveResponse : public InfoResponseProtoMessage {
|
||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||
const char *message_name() const override { return "list_entities_valve_response"; }
|
||||
#endif
|
||||
std::string device_class{};
|
||||
StringRef device_class_ref_{};
|
||||
void set_device_class(const StringRef &ref) { this->device_class_ref_ = ref; }
|
||||
bool assumed_state{false};
|
||||
bool supports_position{false};
|
||||
bool supports_stop{false};
|
||||
@ -2817,7 +2836,8 @@ class ListEntitiesUpdateResponse : public InfoResponseProtoMessage {
|
||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||
const char *message_name() const override { return "list_entities_update_response"; }
|
||||
#endif
|
||||
std::string device_class{};
|
||||
StringRef device_class_ref_{};
|
||||
void set_device_class(const StringRef &ref) { this->device_class_ref_ = ref; }
|
||||
void encode(ProtoWriteBuffer buffer) const override;
|
||||
void calculate_size(uint32_t &total_size) const override;
|
||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||
@ -2837,11 +2857,16 @@ class UpdateStateResponse : public StateResponseProtoMessage {
|
||||
bool in_progress{false};
|
||||
bool has_progress{false};
|
||||
float progress{0.0f};
|
||||
std::string current_version{};
|
||||
std::string latest_version{};
|
||||
std::string title{};
|
||||
std::string release_summary{};
|
||||
std::string release_url{};
|
||||
StringRef current_version_ref_{};
|
||||
void set_current_version(const StringRef &ref) { this->current_version_ref_ = ref; }
|
||||
StringRef latest_version_ref_{};
|
||||
void set_latest_version(const StringRef &ref) { this->latest_version_ref_ = ref; }
|
||||
StringRef title_ref_{};
|
||||
void set_title(const StringRef &ref) { this->title_ref_ = ref; }
|
||||
StringRef release_summary_ref_{};
|
||||
void set_release_summary(const StringRef &ref) { this->release_summary_ref_ = ref; }
|
||||
StringRef release_url_ref_{};
|
||||
void set_release_url(const StringRef &ref) { this->release_url_ref_ = ref; }
|
||||
void encode(ProtoWriteBuffer buffer) const override;
|
||||
void calculate_size(uint32_t &total_size) const override;
|
||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||
@ -2868,5 +2893,4 @@ class UpdateCommandRequest : public CommandProtoMessage {
|
||||
};
|
||||
#endif
|
||||
|
||||
} // namespace api
|
||||
} // namespace esphome
|
||||
} // namespace esphome::api
|
||||
|
File diff suppressed because it is too large
Load Diff
@ -3,8 +3,7 @@
|
||||
#include "api_pb2_service.h"
|
||||
#include "esphome/core/log.h"
|
||||
|
||||
namespace esphome {
|
||||
namespace api {
|
||||
namespace esphome::api {
|
||||
|
||||
static const char *const TAG = "api.service";
|
||||
|
||||
@ -16,7 +15,7 @@ void APIServerConnectionBase::log_send_message_(const char *name, const std::str
|
||||
|
||||
void APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type, uint8_t *msg_data) {
|
||||
switch (msg_type) {
|
||||
case 1: {
|
||||
case HelloRequest::MESSAGE_TYPE: {
|
||||
HelloRequest msg;
|
||||
msg.decode(msg_data, msg_size);
|
||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||
@ -25,7 +24,7 @@ void APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type,
|
||||
this->on_hello_request(msg);
|
||||
break;
|
||||
}
|
||||
case 3: {
|
||||
case ConnectRequest::MESSAGE_TYPE: {
|
||||
ConnectRequest msg;
|
||||
msg.decode(msg_data, msg_size);
|
||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||
@ -34,7 +33,7 @@ void APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type,
|
||||
this->on_connect_request(msg);
|
||||
break;
|
||||
}
|
||||
case 5: {
|
||||
case DisconnectRequest::MESSAGE_TYPE: {
|
||||
DisconnectRequest msg;
|
||||
msg.decode(msg_data, msg_size);
|
||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||
@ -43,7 +42,7 @@ void APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type,
|
||||
this->on_disconnect_request(msg);
|
||||
break;
|
||||
}
|
||||
case 6: {
|
||||
case DisconnectResponse::MESSAGE_TYPE: {
|
||||
DisconnectResponse msg;
|
||||
msg.decode(msg_data, msg_size);
|
||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||
@ -52,7 +51,7 @@ void APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type,
|
||||
this->on_disconnect_response(msg);
|
||||
break;
|
||||
}
|
||||
case 7: {
|
||||
case PingRequest::MESSAGE_TYPE: {
|
||||
PingRequest msg;
|
||||
msg.decode(msg_data, msg_size);
|
||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||
@ -61,7 +60,7 @@ void APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type,
|
||||
this->on_ping_request(msg);
|
||||
break;
|
||||
}
|
||||
case 8: {
|
||||
case PingResponse::MESSAGE_TYPE: {
|
||||
PingResponse msg;
|
||||
msg.decode(msg_data, msg_size);
|
||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||
@ -70,7 +69,7 @@ void APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type,
|
||||
this->on_ping_response(msg);
|
||||
break;
|
||||
}
|
||||
case 9: {
|
||||
case DeviceInfoRequest::MESSAGE_TYPE: {
|
||||
DeviceInfoRequest msg;
|
||||
msg.decode(msg_data, msg_size);
|
||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||
@ -79,7 +78,7 @@ void APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type,
|
||||
this->on_device_info_request(msg);
|
||||
break;
|
||||
}
|
||||
case 11: {
|
||||
case ListEntitiesRequest::MESSAGE_TYPE: {
|
||||
ListEntitiesRequest msg;
|
||||
msg.decode(msg_data, msg_size);
|
||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||
@ -88,7 +87,7 @@ void APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type,
|
||||
this->on_list_entities_request(msg);
|
||||
break;
|
||||
}
|
||||
case 20: {
|
||||
case SubscribeStatesRequest::MESSAGE_TYPE: {
|
||||
SubscribeStatesRequest msg;
|
||||
msg.decode(msg_data, msg_size);
|
||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||
@ -97,7 +96,7 @@ void APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type,
|
||||
this->on_subscribe_states_request(msg);
|
||||
break;
|
||||
}
|
||||
case 28: {
|
||||
case SubscribeLogsRequest::MESSAGE_TYPE: {
|
||||
SubscribeLogsRequest msg;
|
||||
msg.decode(msg_data, msg_size);
|
||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||
@ -107,7 +106,7 @@ void APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type,
|
||||
break;
|
||||
}
|
||||
#ifdef USE_COVER
|
||||
case 30: {
|
||||
case CoverCommandRequest::MESSAGE_TYPE: {
|
||||
CoverCommandRequest msg;
|
||||
msg.decode(msg_data, msg_size);
|
||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||
@ -118,7 +117,7 @@ void APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type,
|
||||
}
|
||||
#endif
|
||||
#ifdef USE_FAN
|
||||
case 31: {
|
||||
case FanCommandRequest::MESSAGE_TYPE: {
|
||||
FanCommandRequest msg;
|
||||
msg.decode(msg_data, msg_size);
|
||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||
@ -129,7 +128,7 @@ void APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type,
|
||||
}
|
||||
#endif
|
||||
#ifdef USE_LIGHT
|
||||
case 32: {
|
||||
case LightCommandRequest::MESSAGE_TYPE: {
|
||||
LightCommandRequest msg;
|
||||
msg.decode(msg_data, msg_size);
|
||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||
@ -140,7 +139,7 @@ void APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type,
|
||||
}
|
||||
#endif
|
||||
#ifdef USE_SWITCH
|
||||
case 33: {
|
||||
case SwitchCommandRequest::MESSAGE_TYPE: {
|
||||
SwitchCommandRequest msg;
|
||||
msg.decode(msg_data, msg_size);
|
||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||
@ -150,7 +149,8 @@ void APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type,
|
||||
break;
|
||||
}
|
||||
#endif
|
||||
case 34: {
|
||||
#ifdef USE_API_HOMEASSISTANT_SERVICES
|
||||
case SubscribeHomeassistantServicesRequest::MESSAGE_TYPE: {
|
||||
SubscribeHomeassistantServicesRequest msg;
|
||||
msg.decode(msg_data, msg_size);
|
||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||
@ -159,7 +159,8 @@ void APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type,
|
||||
this->on_subscribe_homeassistant_services_request(msg);
|
||||
break;
|
||||
}
|
||||
case 36: {
|
||||
#endif
|
||||
case GetTimeRequest::MESSAGE_TYPE: {
|
||||
GetTimeRequest msg;
|
||||
msg.decode(msg_data, msg_size);
|
||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||
@ -168,7 +169,7 @@ void APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type,
|
||||
this->on_get_time_request(msg);
|
||||
break;
|
||||
}
|
||||
case 37: {
|
||||
case GetTimeResponse::MESSAGE_TYPE: {
|
||||
GetTimeResponse msg;
|
||||
msg.decode(msg_data, msg_size);
|
||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||
@ -177,7 +178,8 @@ void APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type,
|
||||
this->on_get_time_response(msg);
|
||||
break;
|
||||
}
|
||||
case 38: {
|
||||
#ifdef USE_API_HOMEASSISTANT_STATES
|
||||
case SubscribeHomeAssistantStatesRequest::MESSAGE_TYPE: {
|
||||
SubscribeHomeAssistantStatesRequest msg;
|
||||
msg.decode(msg_data, msg_size);
|
||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||
@ -186,7 +188,9 @@ void APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type,
|
||||
this->on_subscribe_home_assistant_states_request(msg);
|
||||
break;
|
||||
}
|
||||
case 40: {
|
||||
#endif
|
||||
#ifdef USE_API_HOMEASSISTANT_STATES
|
||||
case HomeAssistantStateResponse::MESSAGE_TYPE: {
|
||||
HomeAssistantStateResponse msg;
|
||||
msg.decode(msg_data, msg_size);
|
||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||
@ -195,8 +199,9 @@ void APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type,
|
||||
this->on_home_assistant_state_response(msg);
|
||||
break;
|
||||
}
|
||||
#endif
|
||||
#ifdef USE_API_SERVICES
|
||||
case 42: {
|
||||
case ExecuteServiceRequest::MESSAGE_TYPE: {
|
||||
ExecuteServiceRequest msg;
|
||||
msg.decode(msg_data, msg_size);
|
||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||
@ -207,7 +212,7 @@ void APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type,
|
||||
}
|
||||
#endif
|
||||
#ifdef USE_CAMERA
|
||||
case 45: {
|
||||
case CameraImageRequest::MESSAGE_TYPE: {
|
||||
CameraImageRequest msg;
|
||||
msg.decode(msg_data, msg_size);
|
||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||
@ -218,7 +223,7 @@ void APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type,
|
||||
}
|
||||
#endif
|
||||
#ifdef USE_CLIMATE
|
||||
case 48: {
|
||||
case ClimateCommandRequest::MESSAGE_TYPE: {
|
||||
ClimateCommandRequest msg;
|
||||
msg.decode(msg_data, msg_size);
|
||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||
@ -229,7 +234,7 @@ void APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type,
|
||||
}
|
||||
#endif
|
||||
#ifdef USE_NUMBER
|
||||
case 51: {
|
||||
case NumberCommandRequest::MESSAGE_TYPE: {
|
||||
NumberCommandRequest msg;
|
||||
msg.decode(msg_data, msg_size);
|
||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||
@ -240,7 +245,7 @@ void APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type,
|
||||
}
|
||||
#endif
|
||||
#ifdef USE_SELECT
|
||||
case 54: {
|
||||
case SelectCommandRequest::MESSAGE_TYPE: {
|
||||
SelectCommandRequest msg;
|
||||
msg.decode(msg_data, msg_size);
|
||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||
@ -251,7 +256,7 @@ void APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type,
|
||||
}
|
||||
#endif
|
||||
#ifdef USE_SIREN
|
||||
case 57: {
|
||||
case SirenCommandRequest::MESSAGE_TYPE: {
|
||||
SirenCommandRequest msg;
|
||||
msg.decode(msg_data, msg_size);
|
||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||
@ -262,7 +267,7 @@ void APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type,
|
||||
}
|
||||
#endif
|
||||
#ifdef USE_LOCK
|
||||
case 60: {
|
||||
case LockCommandRequest::MESSAGE_TYPE: {
|
||||
LockCommandRequest msg;
|
||||
msg.decode(msg_data, msg_size);
|
||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||
@ -273,7 +278,7 @@ void APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type,
|
||||
}
|
||||
#endif
|
||||
#ifdef USE_BUTTON
|
||||
case 62: {
|
||||
case ButtonCommandRequest::MESSAGE_TYPE: {
|
||||
ButtonCommandRequest msg;
|
||||
msg.decode(msg_data, msg_size);
|
||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||
@ -284,7 +289,7 @@ void APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type,
|
||||
}
|
||||
#endif
|
||||
#ifdef USE_MEDIA_PLAYER
|
||||
case 65: {
|
||||
case MediaPlayerCommandRequest::MESSAGE_TYPE: {
|
||||
MediaPlayerCommandRequest msg;
|
||||
msg.decode(msg_data, msg_size);
|
||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||
@ -295,7 +300,7 @@ void APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type,
|
||||
}
|
||||
#endif
|
||||
#ifdef USE_BLUETOOTH_PROXY
|
||||
case 66: {
|
||||
case SubscribeBluetoothLEAdvertisementsRequest::MESSAGE_TYPE: {
|
||||
SubscribeBluetoothLEAdvertisementsRequest msg;
|
||||
msg.decode(msg_data, msg_size);
|
||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||
@ -306,7 +311,7 @@ void APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type,
|
||||
}
|
||||
#endif
|
||||
#ifdef USE_BLUETOOTH_PROXY
|
||||
case 68: {
|
||||
case BluetoothDeviceRequest::MESSAGE_TYPE: {
|
||||
BluetoothDeviceRequest msg;
|
||||
msg.decode(msg_data, msg_size);
|
||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||
@ -317,7 +322,7 @@ void APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type,
|
||||
}
|
||||
#endif
|
||||
#ifdef USE_BLUETOOTH_PROXY
|
||||
case 70: {
|
||||
case BluetoothGATTGetServicesRequest::MESSAGE_TYPE: {
|
||||
BluetoothGATTGetServicesRequest msg;
|
||||
msg.decode(msg_data, msg_size);
|
||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||
@ -328,7 +333,7 @@ void APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type,
|
||||
}
|
||||
#endif
|
||||
#ifdef USE_BLUETOOTH_PROXY
|
||||
case 73: {
|
||||
case BluetoothGATTReadRequest::MESSAGE_TYPE: {
|
||||
BluetoothGATTReadRequest msg;
|
||||
msg.decode(msg_data, msg_size);
|
||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||
@ -339,7 +344,7 @@ void APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type,
|
||||
}
|
||||
#endif
|
||||
#ifdef USE_BLUETOOTH_PROXY
|
||||
case 75: {
|
||||
case BluetoothGATTWriteRequest::MESSAGE_TYPE: {
|
||||
BluetoothGATTWriteRequest msg;
|
||||
msg.decode(msg_data, msg_size);
|
||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||
@ -350,7 +355,7 @@ void APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type,
|
||||
}
|
||||
#endif
|
||||
#ifdef USE_BLUETOOTH_PROXY
|
||||
case 76: {
|
||||
case BluetoothGATTReadDescriptorRequest::MESSAGE_TYPE: {
|
||||
BluetoothGATTReadDescriptorRequest msg;
|
||||
msg.decode(msg_data, msg_size);
|
||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||
@ -361,7 +366,7 @@ void APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type,
|
||||
}
|
||||
#endif
|
||||
#ifdef USE_BLUETOOTH_PROXY
|
||||
case 77: {
|
||||
case BluetoothGATTWriteDescriptorRequest::MESSAGE_TYPE: {
|
||||
BluetoothGATTWriteDescriptorRequest msg;
|
||||
msg.decode(msg_data, msg_size);
|
||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||
@ -372,7 +377,7 @@ void APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type,
|
||||
}
|
||||
#endif
|
||||
#ifdef USE_BLUETOOTH_PROXY
|
||||
case 78: {
|
||||
case BluetoothGATTNotifyRequest::MESSAGE_TYPE: {
|
||||
BluetoothGATTNotifyRequest msg;
|
||||
msg.decode(msg_data, msg_size);
|
||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||
@ -383,7 +388,7 @@ void APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type,
|
||||
}
|
||||
#endif
|
||||
#ifdef USE_BLUETOOTH_PROXY
|
||||
case 80: {
|
||||
case SubscribeBluetoothConnectionsFreeRequest::MESSAGE_TYPE: {
|
||||
SubscribeBluetoothConnectionsFreeRequest msg;
|
||||
msg.decode(msg_data, msg_size);
|
||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||
@ -394,7 +399,7 @@ void APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type,
|
||||
}
|
||||
#endif
|
||||
#ifdef USE_BLUETOOTH_PROXY
|
||||
case 87: {
|
||||
case UnsubscribeBluetoothLEAdvertisementsRequest::MESSAGE_TYPE: {
|
||||
UnsubscribeBluetoothLEAdvertisementsRequest msg;
|
||||
msg.decode(msg_data, msg_size);
|
||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||
@ -405,7 +410,7 @@ void APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type,
|
||||
}
|
||||
#endif
|
||||
#ifdef USE_VOICE_ASSISTANT
|
||||
case 89: {
|
||||
case SubscribeVoiceAssistantRequest::MESSAGE_TYPE: {
|
||||
SubscribeVoiceAssistantRequest msg;
|
||||
msg.decode(msg_data, msg_size);
|
||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||
@ -416,7 +421,7 @@ void APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type,
|
||||
}
|
||||
#endif
|
||||
#ifdef USE_VOICE_ASSISTANT
|
||||
case 91: {
|
||||
case VoiceAssistantResponse::MESSAGE_TYPE: {
|
||||
VoiceAssistantResponse msg;
|
||||
msg.decode(msg_data, msg_size);
|
||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||
@ -427,7 +432,7 @@ void APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type,
|
||||
}
|
||||
#endif
|
||||
#ifdef USE_VOICE_ASSISTANT
|
||||
case 92: {
|
||||
case VoiceAssistantEventResponse::MESSAGE_TYPE: {
|
||||
VoiceAssistantEventResponse msg;
|
||||
msg.decode(msg_data, msg_size);
|
||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||
@ -438,7 +443,7 @@ void APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type,
|
||||
}
|
||||
#endif
|
||||
#ifdef USE_ALARM_CONTROL_PANEL
|
||||
case 96: {
|
||||
case AlarmControlPanelCommandRequest::MESSAGE_TYPE: {
|
||||
AlarmControlPanelCommandRequest msg;
|
||||
msg.decode(msg_data, msg_size);
|
||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||
@ -449,7 +454,7 @@ void APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type,
|
||||
}
|
||||
#endif
|
||||
#ifdef USE_TEXT
|
||||
case 99: {
|
||||
case TextCommandRequest::MESSAGE_TYPE: {
|
||||
TextCommandRequest msg;
|
||||
msg.decode(msg_data, msg_size);
|
||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||
@ -460,7 +465,7 @@ void APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type,
|
||||
}
|
||||
#endif
|
||||
#ifdef USE_DATETIME_DATE
|
||||
case 102: {
|
||||
case DateCommandRequest::MESSAGE_TYPE: {
|
||||
DateCommandRequest msg;
|
||||
msg.decode(msg_data, msg_size);
|
||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||
@ -471,7 +476,7 @@ void APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type,
|
||||
}
|
||||
#endif
|
||||
#ifdef USE_DATETIME_TIME
|
||||
case 105: {
|
||||
case TimeCommandRequest::MESSAGE_TYPE: {
|
||||
TimeCommandRequest msg;
|
||||
msg.decode(msg_data, msg_size);
|
||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||
@ -482,7 +487,7 @@ void APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type,
|
||||
}
|
||||
#endif
|
||||
#ifdef USE_VOICE_ASSISTANT
|
||||
case 106: {
|
||||
case VoiceAssistantAudio::MESSAGE_TYPE: {
|
||||
VoiceAssistantAudio msg;
|
||||
msg.decode(msg_data, msg_size);
|
||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||
@ -493,7 +498,7 @@ void APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type,
|
||||
}
|
||||
#endif
|
||||
#ifdef USE_VALVE
|
||||
case 111: {
|
||||
case ValveCommandRequest::MESSAGE_TYPE: {
|
||||
ValveCommandRequest msg;
|
||||
msg.decode(msg_data, msg_size);
|
||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||
@ -504,7 +509,7 @@ void APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type,
|
||||
}
|
||||
#endif
|
||||
#ifdef USE_DATETIME_DATETIME
|
||||
case 114: {
|
||||
case DateTimeCommandRequest::MESSAGE_TYPE: {
|
||||
DateTimeCommandRequest msg;
|
||||
msg.decode(msg_data, msg_size);
|
||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||
@ -515,7 +520,7 @@ void APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type,
|
||||
}
|
||||
#endif
|
||||
#ifdef USE_VOICE_ASSISTANT
|
||||
case 115: {
|
||||
case VoiceAssistantTimerEventResponse::MESSAGE_TYPE: {
|
||||
VoiceAssistantTimerEventResponse msg;
|
||||
msg.decode(msg_data, msg_size);
|
||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||
@ -526,7 +531,7 @@ void APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type,
|
||||
}
|
||||
#endif
|
||||
#ifdef USE_UPDATE
|
||||
case 118: {
|
||||
case UpdateCommandRequest::MESSAGE_TYPE: {
|
||||
UpdateCommandRequest msg;
|
||||
msg.decode(msg_data, msg_size);
|
||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||
@ -537,7 +542,7 @@ void APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type,
|
||||
}
|
||||
#endif
|
||||
#ifdef USE_VOICE_ASSISTANT
|
||||
case 119: {
|
||||
case VoiceAssistantAnnounceRequest::MESSAGE_TYPE: {
|
||||
VoiceAssistantAnnounceRequest msg;
|
||||
msg.decode(msg_data, msg_size);
|
||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||
@ -548,7 +553,7 @@ void APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type,
|
||||
}
|
||||
#endif
|
||||
#ifdef USE_VOICE_ASSISTANT
|
||||
case 121: {
|
||||
case VoiceAssistantConfigurationRequest::MESSAGE_TYPE: {
|
||||
VoiceAssistantConfigurationRequest msg;
|
||||
msg.decode(msg_data, msg_size);
|
||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||
@ -559,7 +564,7 @@ void APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type,
|
||||
}
|
||||
#endif
|
||||
#ifdef USE_VOICE_ASSISTANT
|
||||
case 123: {
|
||||
case VoiceAssistantSetConfiguration::MESSAGE_TYPE: {
|
||||
VoiceAssistantSetConfiguration msg;
|
||||
msg.decode(msg_data, msg_size);
|
||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||
@ -570,7 +575,7 @@ void APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type,
|
||||
}
|
||||
#endif
|
||||
#ifdef USE_API_NOISE
|
||||
case 124: {
|
||||
case NoiseEncryptionSetKeyRequest::MESSAGE_TYPE: {
|
||||
NoiseEncryptionSetKeyRequest msg;
|
||||
msg.decode(msg_data, msg_size);
|
||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||
@ -581,7 +586,7 @@ void APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type,
|
||||
}
|
||||
#endif
|
||||
#ifdef USE_BLUETOOTH_PROXY
|
||||
case 127: {
|
||||
case BluetoothScannerSetModeRequest::MESSAGE_TYPE: {
|
||||
BluetoothScannerSetModeRequest msg;
|
||||
msg.decode(msg_data, msg_size);
|
||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||
@ -597,35 +602,28 @@ void APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type,
|
||||
}
|
||||
|
||||
void APIServerConnection::on_hello_request(const HelloRequest &msg) {
|
||||
HelloResponse ret = this->hello(msg);
|
||||
if (!this->send_message(ret, HelloResponse::MESSAGE_TYPE)) {
|
||||
if (!this->send_hello_response(msg)) {
|
||||
this->on_fatal_error();
|
||||
}
|
||||
}
|
||||
void APIServerConnection::on_connect_request(const ConnectRequest &msg) {
|
||||
ConnectResponse ret = this->connect(msg);
|
||||
if (!this->send_message(ret, ConnectResponse::MESSAGE_TYPE)) {
|
||||
if (!this->send_connect_response(msg)) {
|
||||
this->on_fatal_error();
|
||||
}
|
||||
}
|
||||
void APIServerConnection::on_disconnect_request(const DisconnectRequest &msg) {
|
||||
DisconnectResponse ret = this->disconnect(msg);
|
||||
if (!this->send_message(ret, DisconnectResponse::MESSAGE_TYPE)) {
|
||||
if (!this->send_disconnect_response(msg)) {
|
||||
this->on_fatal_error();
|
||||
}
|
||||
}
|
||||
void APIServerConnection::on_ping_request(const PingRequest &msg) {
|
||||
PingResponse ret = this->ping(msg);
|
||||
if (!this->send_message(ret, PingResponse::MESSAGE_TYPE)) {
|
||||
if (!this->send_ping_response(msg)) {
|
||||
this->on_fatal_error();
|
||||
}
|
||||
}
|
||||
void APIServerConnection::on_device_info_request(const DeviceInfoRequest &msg) {
|
||||
if (this->check_connection_setup_()) {
|
||||
DeviceInfoResponse ret = this->device_info(msg);
|
||||
if (!this->send_message(ret, DeviceInfoResponse::MESSAGE_TYPE)) {
|
||||
this->on_fatal_error();
|
||||
}
|
||||
if (this->check_connection_setup_() && !this->send_device_info_response(msg)) {
|
||||
this->on_fatal_error();
|
||||
}
|
||||
}
|
||||
void APIServerConnection::on_list_entities_request(const ListEntitiesRequest &msg) {
|
||||
@ -643,23 +641,24 @@ void APIServerConnection::on_subscribe_logs_request(const SubscribeLogsRequest &
|
||||
this->subscribe_logs(msg);
|
||||
}
|
||||
}
|
||||
#ifdef USE_API_HOMEASSISTANT_SERVICES
|
||||
void APIServerConnection::on_subscribe_homeassistant_services_request(
|
||||
const SubscribeHomeassistantServicesRequest &msg) {
|
||||
if (this->check_authenticated_()) {
|
||||
this->subscribe_homeassistant_services(msg);
|
||||
}
|
||||
}
|
||||
#endif
|
||||
#ifdef USE_API_HOMEASSISTANT_STATES
|
||||
void APIServerConnection::on_subscribe_home_assistant_states_request(const SubscribeHomeAssistantStatesRequest &msg) {
|
||||
if (this->check_authenticated_()) {
|
||||
this->subscribe_home_assistant_states(msg);
|
||||
}
|
||||
}
|
||||
#endif
|
||||
void APIServerConnection::on_get_time_request(const GetTimeRequest &msg) {
|
||||
if (this->check_connection_setup_()) {
|
||||
GetTimeResponse ret = this->get_time(msg);
|
||||
if (!this->send_message(ret, GetTimeResponse::MESSAGE_TYPE)) {
|
||||
this->on_fatal_error();
|
||||
}
|
||||
if (this->check_connection_setup_() && !this->send_get_time_response(msg)) {
|
||||
this->on_fatal_error();
|
||||
}
|
||||
}
|
||||
#ifdef USE_API_SERVICES
|
||||
@ -671,11 +670,8 @@ void APIServerConnection::on_execute_service_request(const ExecuteServiceRequest
|
||||
#endif
|
||||
#ifdef USE_API_NOISE
|
||||
void APIServerConnection::on_noise_encryption_set_key_request(const NoiseEncryptionSetKeyRequest &msg) {
|
||||
if (this->check_authenticated_()) {
|
||||
NoiseEncryptionSetKeyResponse ret = this->noise_encryption_set_key(msg);
|
||||
if (!this->send_message(ret, NoiseEncryptionSetKeyResponse::MESSAGE_TYPE)) {
|
||||
this->on_fatal_error();
|
||||
}
|
||||
if (this->check_authenticated_() && !this->send_noise_encryption_set_key_response(msg)) {
|
||||
this->on_fatal_error();
|
||||
}
|
||||
}
|
||||
#endif
|
||||
@ -865,11 +861,8 @@ void APIServerConnection::on_bluetooth_gatt_notify_request(const BluetoothGATTNo
|
||||
#ifdef USE_BLUETOOTH_PROXY
|
||||
void APIServerConnection::on_subscribe_bluetooth_connections_free_request(
|
||||
const SubscribeBluetoothConnectionsFreeRequest &msg) {
|
||||
if (this->check_authenticated_()) {
|
||||
BluetoothConnectionsFreeResponse ret = this->subscribe_bluetooth_connections_free(msg);
|
||||
if (!this->send_message(ret, BluetoothConnectionsFreeResponse::MESSAGE_TYPE)) {
|
||||
this->on_fatal_error();
|
||||
}
|
||||
if (this->check_authenticated_() && !this->send_subscribe_bluetooth_connections_free_response(msg)) {
|
||||
this->on_fatal_error();
|
||||
}
|
||||
}
|
||||
#endif
|
||||
@ -897,11 +890,8 @@ void APIServerConnection::on_subscribe_voice_assistant_request(const SubscribeVo
|
||||
#endif
|
||||
#ifdef USE_VOICE_ASSISTANT
|
||||
void APIServerConnection::on_voice_assistant_configuration_request(const VoiceAssistantConfigurationRequest &msg) {
|
||||
if (this->check_authenticated_()) {
|
||||
VoiceAssistantConfigurationResponse ret = this->voice_assistant_get_configuration(msg);
|
||||
if (!this->send_message(ret, VoiceAssistantConfigurationResponse::MESSAGE_TYPE)) {
|
||||
this->on_fatal_error();
|
||||
}
|
||||
if (this->check_authenticated_() && !this->send_voice_assistant_get_configuration_response(msg)) {
|
||||
this->on_fatal_error();
|
||||
}
|
||||
}
|
||||
#endif
|
||||
@ -920,5 +910,4 @@ void APIServerConnection::on_alarm_control_panel_command_request(const AlarmCont
|
||||
}
|
||||
#endif
|
||||
|
||||
} // namespace api
|
||||
} // namespace esphome
|
||||
} // namespace esphome::api
|
||||
|
@ -6,8 +6,7 @@
|
||||
|
||||
#include "api_pb2.h"
|
||||
|
||||
namespace esphome {
|
||||
namespace api {
|
||||
namespace esphome::api {
|
||||
|
||||
class APIServerConnectionBase : public ProtoService {
|
||||
public:
|
||||
@ -61,11 +60,17 @@ class APIServerConnectionBase : public ProtoService {
|
||||
virtual void on_noise_encryption_set_key_request(const NoiseEncryptionSetKeyRequest &value){};
|
||||
#endif
|
||||
|
||||
#ifdef USE_API_HOMEASSISTANT_SERVICES
|
||||
virtual void on_subscribe_homeassistant_services_request(const SubscribeHomeassistantServicesRequest &value){};
|
||||
#endif
|
||||
|
||||
#ifdef USE_API_HOMEASSISTANT_STATES
|
||||
virtual void on_subscribe_home_assistant_states_request(const SubscribeHomeAssistantStatesRequest &value){};
|
||||
#endif
|
||||
|
||||
#ifdef USE_API_HOMEASSISTANT_STATES
|
||||
virtual void on_home_assistant_state_response(const HomeAssistantStateResponse &value){};
|
||||
#endif
|
||||
virtual void on_get_time_request(const GetTimeRequest &value){};
|
||||
virtual void on_get_time_response(const GetTimeResponse &value){};
|
||||
|
||||
@ -207,22 +212,26 @@ class APIServerConnectionBase : public ProtoService {
|
||||
|
||||
class APIServerConnection : public APIServerConnectionBase {
|
||||
public:
|
||||
virtual HelloResponse hello(const HelloRequest &msg) = 0;
|
||||
virtual ConnectResponse connect(const ConnectRequest &msg) = 0;
|
||||
virtual DisconnectResponse disconnect(const DisconnectRequest &msg) = 0;
|
||||
virtual PingResponse ping(const PingRequest &msg) = 0;
|
||||
virtual DeviceInfoResponse device_info(const DeviceInfoRequest &msg) = 0;
|
||||
virtual bool send_hello_response(const HelloRequest &msg) = 0;
|
||||
virtual bool send_connect_response(const ConnectRequest &msg) = 0;
|
||||
virtual bool send_disconnect_response(const DisconnectRequest &msg) = 0;
|
||||
virtual bool send_ping_response(const PingRequest &msg) = 0;
|
||||
virtual bool send_device_info_response(const DeviceInfoRequest &msg) = 0;
|
||||
virtual void list_entities(const ListEntitiesRequest &msg) = 0;
|
||||
virtual void subscribe_states(const SubscribeStatesRequest &msg) = 0;
|
||||
virtual void subscribe_logs(const SubscribeLogsRequest &msg) = 0;
|
||||
#ifdef USE_API_HOMEASSISTANT_SERVICES
|
||||
virtual void subscribe_homeassistant_services(const SubscribeHomeassistantServicesRequest &msg) = 0;
|
||||
#endif
|
||||
#ifdef USE_API_HOMEASSISTANT_STATES
|
||||
virtual void subscribe_home_assistant_states(const SubscribeHomeAssistantStatesRequest &msg) = 0;
|
||||
virtual GetTimeResponse get_time(const GetTimeRequest &msg) = 0;
|
||||
#endif
|
||||
virtual bool send_get_time_response(const GetTimeRequest &msg) = 0;
|
||||
#ifdef USE_API_SERVICES
|
||||
virtual void execute_service(const ExecuteServiceRequest &msg) = 0;
|
||||
#endif
|
||||
#ifdef USE_API_NOISE
|
||||
virtual NoiseEncryptionSetKeyResponse noise_encryption_set_key(const NoiseEncryptionSetKeyRequest &msg) = 0;
|
||||
virtual bool send_noise_encryption_set_key_response(const NoiseEncryptionSetKeyRequest &msg) = 0;
|
||||
#endif
|
||||
#ifdef USE_BUTTON
|
||||
virtual void button_command(const ButtonCommandRequest &msg) = 0;
|
||||
@ -303,7 +312,7 @@ class APIServerConnection : public APIServerConnectionBase {
|
||||
virtual void bluetooth_gatt_notify(const BluetoothGATTNotifyRequest &msg) = 0;
|
||||
#endif
|
||||
#ifdef USE_BLUETOOTH_PROXY
|
||||
virtual BluetoothConnectionsFreeResponse subscribe_bluetooth_connections_free(
|
||||
virtual bool send_subscribe_bluetooth_connections_free_response(
|
||||
const SubscribeBluetoothConnectionsFreeRequest &msg) = 0;
|
||||
#endif
|
||||
#ifdef USE_BLUETOOTH_PROXY
|
||||
@ -316,8 +325,7 @@ class APIServerConnection : public APIServerConnectionBase {
|
||||
virtual void subscribe_voice_assistant(const SubscribeVoiceAssistantRequest &msg) = 0;
|
||||
#endif
|
||||
#ifdef USE_VOICE_ASSISTANT
|
||||
virtual VoiceAssistantConfigurationResponse voice_assistant_get_configuration(
|
||||
const VoiceAssistantConfigurationRequest &msg) = 0;
|
||||
virtual bool send_voice_assistant_get_configuration_response(const VoiceAssistantConfigurationRequest &msg) = 0;
|
||||
#endif
|
||||
#ifdef USE_VOICE_ASSISTANT
|
||||
virtual void voice_assistant_set_configuration(const VoiceAssistantSetConfiguration &msg) = 0;
|
||||
@ -334,8 +342,12 @@ class APIServerConnection : public APIServerConnectionBase {
|
||||
void on_list_entities_request(const ListEntitiesRequest &msg) override;
|
||||
void on_subscribe_states_request(const SubscribeStatesRequest &msg) override;
|
||||
void on_subscribe_logs_request(const SubscribeLogsRequest &msg) override;
|
||||
#ifdef USE_API_HOMEASSISTANT_SERVICES
|
||||
void on_subscribe_homeassistant_services_request(const SubscribeHomeassistantServicesRequest &msg) override;
|
||||
#endif
|
||||
#ifdef USE_API_HOMEASSISTANT_STATES
|
||||
void on_subscribe_home_assistant_states_request(const SubscribeHomeAssistantStatesRequest &msg) override;
|
||||
#endif
|
||||
void on_get_time_request(const GetTimeRequest &msg) override;
|
||||
#ifdef USE_API_SERVICES
|
||||
void on_execute_service_request(const ExecuteServiceRequest &msg) override;
|
||||
@ -445,5 +457,4 @@ class APIServerConnection : public APIServerConnectionBase {
|
||||
#endif
|
||||
};
|
||||
|
||||
} // namespace api
|
||||
} // namespace esphome
|
||||
} // namespace esphome::api
|
||||
|
@ -16,8 +16,7 @@
|
||||
|
||||
#include <algorithm>
|
||||
|
||||
namespace esphome {
|
||||
namespace api {
|
||||
namespace esphome::api {
|
||||
|
||||
static const char *const TAG = "api";
|
||||
|
||||
@ -184,9 +183,9 @@ void APIServer::loop() {
|
||||
|
||||
// Rare case: handle disconnection
|
||||
#ifdef USE_API_CLIENT_DISCONNECTED_TRIGGER
|
||||
this->client_disconnected_trigger_->trigger(client->client_info_, client->client_peername_);
|
||||
this->client_disconnected_trigger_->trigger(client->client_info_.name, client->client_info_.peername);
|
||||
#endif
|
||||
ESP_LOGV(TAG, "Remove connection %s", client->client_info_.c_str());
|
||||
ESP_LOGV(TAG, "Remove connection %s", client->client_info_.name.c_str());
|
||||
|
||||
// Swap with the last element and pop (avoids expensive vector shifts)
|
||||
if (client_index < this->clients_.size() - 1) {
|
||||
@ -370,12 +369,15 @@ void APIServer::set_password(const std::string &password) { this->password_ = pa
|
||||
|
||||
void APIServer::set_batch_delay(uint16_t batch_delay) { this->batch_delay_ = batch_delay; }
|
||||
|
||||
#ifdef USE_API_HOMEASSISTANT_SERVICES
|
||||
void APIServer::send_homeassistant_service_call(const HomeassistantServiceResponse &call) {
|
||||
for (auto &client : this->clients_) {
|
||||
client->send_homeassistant_service_call(call);
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
#ifdef USE_API_HOMEASSISTANT_STATES
|
||||
void APIServer::subscribe_home_assistant_state(std::string entity_id, optional<std::string> attribute,
|
||||
std::function<void(std::string)> f) {
|
||||
this->state_subs_.push_back(HomeAssistantStateSubscription{
|
||||
@ -399,6 +401,7 @@ void APIServer::get_home_assistant_state(std::string entity_id, optional<std::st
|
||||
const std::vector<APIServer::HomeAssistantStateSubscription> &APIServer::get_state_subs() const {
|
||||
return this->state_subs_;
|
||||
}
|
||||
#endif
|
||||
|
||||
uint16_t APIServer::get_port() const { return this->port_; }
|
||||
|
||||
@ -483,6 +486,5 @@ bool APIServer::teardown() {
|
||||
return this->clients_.empty();
|
||||
}
|
||||
|
||||
} // namespace api
|
||||
} // namespace esphome
|
||||
} // namespace esphome::api
|
||||
#endif
|
||||
|
@ -18,8 +18,7 @@
|
||||
|
||||
#include <vector>
|
||||
|
||||
namespace esphome {
|
||||
namespace api {
|
||||
namespace esphome::api {
|
||||
|
||||
#ifdef USE_API_NOISE
|
||||
struct SavedNoisePsk {
|
||||
@ -107,7 +106,9 @@ class APIServer : public Component, public Controller {
|
||||
#ifdef USE_MEDIA_PLAYER
|
||||
void on_media_player_update(media_player::MediaPlayer *obj) override;
|
||||
#endif
|
||||
#ifdef USE_API_HOMEASSISTANT_SERVICES
|
||||
void send_homeassistant_service_call(const HomeassistantServiceResponse &call);
|
||||
#endif
|
||||
#ifdef USE_API_SERVICES
|
||||
void register_user_service(UserServiceDescriptor *descriptor) { this->user_services_.push_back(descriptor); }
|
||||
#endif
|
||||
@ -127,6 +128,7 @@ class APIServer : public Component, public Controller {
|
||||
|
||||
bool is_connected() const;
|
||||
|
||||
#ifdef USE_API_HOMEASSISTANT_STATES
|
||||
struct HomeAssistantStateSubscription {
|
||||
std::string entity_id;
|
||||
optional<std::string> attribute;
|
||||
@ -139,6 +141,7 @@ class APIServer : public Component, public Controller {
|
||||
void get_home_assistant_state(std::string entity_id, optional<std::string> attribute,
|
||||
std::function<void(std::string)> f);
|
||||
const std::vector<HomeAssistantStateSubscription> &get_state_subs() const;
|
||||
#endif
|
||||
#ifdef USE_API_SERVICES
|
||||
const std::vector<UserServiceDescriptor *> &get_user_services() const { return this->user_services_; }
|
||||
#endif
|
||||
@ -172,7 +175,9 @@ class APIServer : public Component, public Controller {
|
||||
std::string password_;
|
||||
#endif
|
||||
std::vector<uint8_t> shared_write_buffer_; // Shared proto write buffer for all connections
|
||||
#ifdef USE_API_HOMEASSISTANT_STATES
|
||||
std::vector<HomeAssistantStateSubscription> state_subs_;
|
||||
#endif
|
||||
#ifdef USE_API_SERVICES
|
||||
std::vector<UserServiceDescriptor *> user_services_;
|
||||
#endif
|
||||
@ -196,6 +201,5 @@ template<typename... Ts> class APIConnectedCondition : public Condition<Ts...> {
|
||||
bool check(Ts... x) override { return global_api_server->is_connected(); }
|
||||
};
|
||||
|
||||
} // namespace api
|
||||
} // namespace esphome
|
||||
} // namespace esphome::api
|
||||
#endif
|
||||
|
@ -14,6 +14,8 @@ with warnings.catch_warnings():
|
||||
from aioesphomeapi import APIClient, parse_log_message
|
||||
from aioesphomeapi.log_runner import async_run
|
||||
|
||||
import contextlib
|
||||
|
||||
from esphome.const import CONF_KEY, CONF_PASSWORD, CONF_PORT, __version__
|
||||
from esphome.core import CORE
|
||||
|
||||
@ -66,7 +68,5 @@ async def async_run_logs(config: dict[str, Any], address: str) -> None:
|
||||
|
||||
def run_logs(config: dict[str, Any], address: str) -> None:
|
||||
"""Run the logs command."""
|
||||
try:
|
||||
with contextlib.suppress(KeyboardInterrupt):
|
||||
asyncio.run(async_run_logs(config, address))
|
||||
except KeyboardInterrupt:
|
||||
pass
|
||||
|
@ -6,8 +6,7 @@
|
||||
#ifdef USE_API_SERVICES
|
||||
#include "user_services.h"
|
||||
#endif
|
||||
namespace esphome {
|
||||
namespace api {
|
||||
namespace esphome::api {
|
||||
|
||||
#ifdef USE_API_SERVICES
|
||||
template<typename T, typename... Ts> class CustomAPIDeviceService : public UserServiceBase<Ts...> {
|
||||
@ -84,6 +83,7 @@ class CustomAPIDevice {
|
||||
}
|
||||
#endif
|
||||
|
||||
#ifdef USE_API_HOMEASSISTANT_STATES
|
||||
/** Subscribe to the state (or attribute state) of an entity from Home Assistant.
|
||||
*
|
||||
* Usage:
|
||||
@ -135,7 +135,9 @@ class CustomAPIDevice {
|
||||
auto f = std::bind(callback, (T *) this, entity_id, std::placeholders::_1);
|
||||
global_api_server->subscribe_home_assistant_state(entity_id, optional<std::string>(attribute), f);
|
||||
}
|
||||
#endif
|
||||
|
||||
#ifdef USE_API_HOMEASSISTANT_SERVICES
|
||||
/** Call a Home Assistant service from ESPHome.
|
||||
*
|
||||
* Usage:
|
||||
@ -148,7 +150,7 @@ class CustomAPIDevice {
|
||||
*/
|
||||
void call_homeassistant_service(const std::string &service_name) {
|
||||
HomeassistantServiceResponse resp;
|
||||
resp.service = service_name;
|
||||
resp.set_service(StringRef(service_name));
|
||||
global_api_server->send_homeassistant_service_call(resp);
|
||||
}
|
||||
|
||||
@ -168,12 +170,12 @@ class CustomAPIDevice {
|
||||
*/
|
||||
void call_homeassistant_service(const std::string &service_name, const std::map<std::string, std::string> &data) {
|
||||
HomeassistantServiceResponse resp;
|
||||
resp.service = service_name;
|
||||
resp.set_service(StringRef(service_name));
|
||||
for (auto &it : data) {
|
||||
HomeassistantServiceMap kv;
|
||||
kv.key = it.first;
|
||||
resp.data.emplace_back();
|
||||
auto &kv = resp.data.back();
|
||||
kv.set_key(StringRef(it.first));
|
||||
kv.value = it.second;
|
||||
resp.data.push_back(kv);
|
||||
}
|
||||
global_api_server->send_homeassistant_service_call(resp);
|
||||
}
|
||||
@ -190,7 +192,7 @@ class CustomAPIDevice {
|
||||
*/
|
||||
void fire_homeassistant_event(const std::string &event_name) {
|
||||
HomeassistantServiceResponse resp;
|
||||
resp.service = event_name;
|
||||
resp.set_service(StringRef(event_name));
|
||||
resp.is_event = true;
|
||||
global_api_server->send_homeassistant_service_call(resp);
|
||||
}
|
||||
@ -210,18 +212,18 @@ class CustomAPIDevice {
|
||||
*/
|
||||
void fire_homeassistant_event(const std::string &service_name, const std::map<std::string, std::string> &data) {
|
||||
HomeassistantServiceResponse resp;
|
||||
resp.service = service_name;
|
||||
resp.set_service(StringRef(service_name));
|
||||
resp.is_event = true;
|
||||
for (auto &it : data) {
|
||||
HomeassistantServiceMap kv;
|
||||
kv.key = it.first;
|
||||
resp.data.emplace_back();
|
||||
auto &kv = resp.data.back();
|
||||
kv.set_key(StringRef(it.first));
|
||||
kv.value = it.second;
|
||||
resp.data.push_back(kv);
|
||||
}
|
||||
global_api_server->send_homeassistant_service_call(resp);
|
||||
}
|
||||
#endif
|
||||
};
|
||||
|
||||
} // namespace api
|
||||
} // namespace esphome
|
||||
} // namespace esphome::api
|
||||
#endif
|
||||
|
@ -2,13 +2,13 @@
|
||||
|
||||
#include "api_server.h"
|
||||
#ifdef USE_API
|
||||
#ifdef USE_API_HOMEASSISTANT_SERVICES
|
||||
#include "api_pb2.h"
|
||||
#include "esphome/core/automation.h"
|
||||
#include "esphome/core/helpers.h"
|
||||
#include <vector>
|
||||
|
||||
namespace esphome {
|
||||
namespace api {
|
||||
namespace esphome::api {
|
||||
|
||||
template<typename... X> class TemplatableStringValue : public TemplatableValue<std::string, X...> {
|
||||
private:
|
||||
@ -36,6 +36,9 @@ template<typename... X> class TemplatableStringValue : public TemplatableValue<s
|
||||
|
||||
template<typename... Ts> class TemplatableKeyValuePair {
|
||||
public:
|
||||
// Keys are always string literals from YAML dictionary keys (e.g., "code", "event")
|
||||
// and never templatable values or lambdas. Only the value parameter can be a lambda/template.
|
||||
// Using pass-by-value with std::move allows optimal performance for both lvalues and rvalues.
|
||||
template<typename T> TemplatableKeyValuePair(std::string key, T value) : key(std::move(key)), value(value) {}
|
||||
std::string key;
|
||||
TemplatableStringValue<Ts...> value;
|
||||
@ -47,37 +50,39 @@ template<typename... Ts> class HomeAssistantServiceCallAction : public Action<Ts
|
||||
|
||||
template<typename T> void set_service(T service) { this->service_ = service; }
|
||||
|
||||
template<typename T> void add_data(std::string key, T value) {
|
||||
this->data_.push_back(TemplatableKeyValuePair<Ts...>(key, value));
|
||||
}
|
||||
// Keys are always string literals from the Python code generation (e.g., cg.add(var.add_data("tag_id", templ))).
|
||||
// The value parameter can be a lambda/template, but keys are never templatable.
|
||||
// Using pass-by-value allows the compiler to optimize for both lvalues and rvalues.
|
||||
template<typename T> void add_data(std::string key, T value) { this->data_.emplace_back(std::move(key), value); }
|
||||
template<typename T> void add_data_template(std::string key, T value) {
|
||||
this->data_template_.push_back(TemplatableKeyValuePair<Ts...>(key, value));
|
||||
this->data_template_.emplace_back(std::move(key), value);
|
||||
}
|
||||
template<typename T> void add_variable(std::string key, T value) {
|
||||
this->variables_.push_back(TemplatableKeyValuePair<Ts...>(key, value));
|
||||
this->variables_.emplace_back(std::move(key), value);
|
||||
}
|
||||
|
||||
void play(Ts... x) override {
|
||||
HomeassistantServiceResponse resp;
|
||||
resp.service = this->service_.value(x...);
|
||||
std::string service_value = this->service_.value(x...);
|
||||
resp.set_service(StringRef(service_value));
|
||||
resp.is_event = this->is_event_;
|
||||
for (auto &it : this->data_) {
|
||||
HomeassistantServiceMap kv;
|
||||
kv.key = it.key;
|
||||
resp.data.emplace_back();
|
||||
auto &kv = resp.data.back();
|
||||
kv.set_key(StringRef(it.key));
|
||||
kv.value = it.value.value(x...);
|
||||
resp.data.push_back(kv);
|
||||
}
|
||||
for (auto &it : this->data_template_) {
|
||||
HomeassistantServiceMap kv;
|
||||
kv.key = it.key;
|
||||
resp.data_template.emplace_back();
|
||||
auto &kv = resp.data_template.back();
|
||||
kv.set_key(StringRef(it.key));
|
||||
kv.value = it.value.value(x...);
|
||||
resp.data_template.push_back(kv);
|
||||
}
|
||||
for (auto &it : this->variables_) {
|
||||
HomeassistantServiceMap kv;
|
||||
kv.key = it.key;
|
||||
resp.variables.emplace_back();
|
||||
auto &kv = resp.variables.back();
|
||||
kv.set_key(StringRef(it.key));
|
||||
kv.value = it.value.value(x...);
|
||||
resp.variables.push_back(kv);
|
||||
}
|
||||
this->parent_->send_homeassistant_service_call(resp);
|
||||
}
|
||||
@ -91,6 +96,6 @@ template<typename... Ts> class HomeAssistantServiceCallAction : public Action<Ts
|
||||
std::vector<TemplatableKeyValuePair<Ts...>> variables_;
|
||||
};
|
||||
|
||||
} // namespace api
|
||||
} // namespace esphome
|
||||
} // namespace esphome::api
|
||||
#endif
|
||||
#endif
|
||||
|
@ -6,8 +6,7 @@
|
||||
#include "esphome/core/log.h"
|
||||
#include "esphome/core/util.h"
|
||||
|
||||
namespace esphome {
|
||||
namespace api {
|
||||
namespace esphome::api {
|
||||
|
||||
// Generate entity handler implementations using macros
|
||||
#ifdef USE_BINARY_SENSOR
|
||||
@ -90,6 +89,5 @@ bool ListEntitiesIterator::on_service(UserServiceDescriptor *service) {
|
||||
}
|
||||
#endif
|
||||
|
||||
} // namespace api
|
||||
} // namespace esphome
|
||||
} // namespace esphome::api
|
||||
#endif
|
||||
|
@ -4,8 +4,7 @@
|
||||
#ifdef USE_API
|
||||
#include "esphome/core/component.h"
|
||||
#include "esphome/core/component_iterator.h"
|
||||
namespace esphome {
|
||||
namespace api {
|
||||
namespace esphome::api {
|
||||
|
||||
class APIConnection;
|
||||
|
||||
@ -96,6 +95,5 @@ class ListEntitiesIterator : public ComponentIterator {
|
||||
APIConnection *client_;
|
||||
};
|
||||
|
||||
} // namespace api
|
||||
} // namespace esphome
|
||||
} // namespace esphome::api
|
||||
#endif
|
||||
|
@ -3,8 +3,7 @@
|
||||
#include "esphome/core/helpers.h"
|
||||
#include "esphome/core/log.h"
|
||||
|
||||
namespace esphome {
|
||||
namespace api {
|
||||
namespace esphome::api {
|
||||
|
||||
static const char *const TAG = "api.proto";
|
||||
|
||||
@ -89,5 +88,4 @@ std::string ProtoMessage::dump() const {
|
||||
}
|
||||
#endif
|
||||
|
||||
} // namespace api
|
||||
} // namespace esphome
|
||||
} // namespace esphome::api
|
||||
|
@ -3,16 +3,47 @@
|
||||
#include "esphome/core/component.h"
|
||||
#include "esphome/core/helpers.h"
|
||||
#include "esphome/core/log.h"
|
||||
#include "esphome/core/string_ref.h"
|
||||
|
||||
#include <cassert>
|
||||
#include <cstring>
|
||||
#include <vector>
|
||||
|
||||
#ifdef ESPHOME_LOG_HAS_VERY_VERBOSE
|
||||
#define HAS_PROTO_MESSAGE_DUMP
|
||||
#endif
|
||||
|
||||
namespace esphome {
|
||||
namespace api {
|
||||
namespace esphome::api {
|
||||
|
||||
/*
|
||||
* StringRef Ownership Model for API Protocol Messages
|
||||
* ===================================================
|
||||
*
|
||||
* StringRef is used for zero-copy string handling in outgoing (SOURCE_SERVER) messages.
|
||||
* It holds a pointer and length to existing string data without copying.
|
||||
*
|
||||
* CRITICAL: The referenced string data MUST remain valid until message encoding completes.
|
||||
*
|
||||
* Safe StringRef Patterns:
|
||||
* 1. String literals: StringRef("literal") - Always safe (static storage duration)
|
||||
* 2. Member variables: StringRef(this->member_string_) - Safe if object outlives encoding
|
||||
* 3. Global/static strings: StringRef(GLOBAL_CONSTANT) - Always safe
|
||||
* 4. Local variables: Safe ONLY if encoding happens before function returns:
|
||||
* std::string temp = compute_value();
|
||||
* msg.set_field(StringRef(temp));
|
||||
* return this->send_message(msg); // temp is valid during encoding
|
||||
*
|
||||
* Unsafe Patterns (WILL cause crashes/corruption):
|
||||
* 1. Temporaries: msg.set_field(StringRef(obj.get_string())) // get_string() returns by value
|
||||
* 2. Concatenation: msg.set_field(StringRef(str1 + str2)) // Result is temporary
|
||||
*
|
||||
* For unsafe patterns, store in a local variable first:
|
||||
* std::string temp = get_string(); // or str1 + str2
|
||||
* msg.set_field(StringRef(temp));
|
||||
*
|
||||
* The send_*_response pattern ensures proper lifetime management by encoding
|
||||
* within the same function scope where temporaries are created.
|
||||
*/
|
||||
|
||||
/// Representation of a VarInt - in ProtoBuf should be 64bit but we only use 32bit
|
||||
class ProtoVarInt {
|
||||
@ -206,12 +237,20 @@ class ProtoWriteBuffer {
|
||||
|
||||
this->encode_field_raw(field_id, 2); // type 2: Length-delimited string
|
||||
this->encode_varint_raw(len);
|
||||
auto *data = reinterpret_cast<const uint8_t *>(string);
|
||||
this->buffer_->insert(this->buffer_->end(), data, data + len);
|
||||
|
||||
// Using resize + memcpy instead of insert provides significant performance improvement:
|
||||
// ~10-11x faster for 16-32 byte strings, ~3x faster for 64-byte strings
|
||||
// as it avoids iterator checks and potential element moves that insert performs
|
||||
size_t old_size = this->buffer_->size();
|
||||
this->buffer_->resize(old_size + len);
|
||||
std::memcpy(this->buffer_->data() + old_size, string, len);
|
||||
}
|
||||
void encode_string(uint32_t field_id, const std::string &value, bool force = false) {
|
||||
this->encode_string(field_id, value.data(), value.size(), force);
|
||||
}
|
||||
void encode_string(uint32_t field_id, const StringRef &ref, bool force = false) {
|
||||
this->encode_string(field_id, ref.c_str(), ref.size(), force);
|
||||
}
|
||||
void encode_bytes(uint32_t field_id, const uint8_t *data, size_t len, bool force = false) {
|
||||
this->encode_string(field_id, reinterpret_cast<const char *>(data), len, force);
|
||||
}
|
||||
@ -527,25 +566,6 @@ class ProtoSize {
|
||||
total_size += field_id_size + 1;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Calculates and adds the size of a fixed field to the total message size
|
||||
*
|
||||
* Fixed fields always take exactly N bytes (4 for fixed32/float, 8 for fixed64/double).
|
||||
*
|
||||
* @tparam NumBytes The number of bytes for this fixed field (4 or 8)
|
||||
* @param is_nonzero Whether the value is non-zero
|
||||
*/
|
||||
template<uint32_t NumBytes>
|
||||
static inline void add_fixed_field(uint32_t &total_size, uint32_t field_id_size, bool is_nonzero) {
|
||||
// Skip calculation if value is zero
|
||||
if (!is_nonzero) {
|
||||
return; // No need to update total_size
|
||||
}
|
||||
|
||||
// Fixed fields always take exactly NumBytes
|
||||
total_size += field_id_size + NumBytes;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Calculates and adds the size of a float field to the total message size
|
||||
*/
|
||||
@ -682,17 +702,16 @@ class ProtoSize {
|
||||
// sint64 type is not supported by ESPHome API to reduce overhead on embedded systems
|
||||
|
||||
/**
|
||||
* @brief Calculates and adds the size of a string/bytes field to the total message size
|
||||
* @brief Calculates and adds the size of a string field using length
|
||||
*/
|
||||
static inline void add_string_field(uint32_t &total_size, uint32_t field_id_size, const std::string &str) {
|
||||
static inline void add_string_field(uint32_t &total_size, uint32_t field_id_size, size_t len) {
|
||||
// Skip calculation if string is empty
|
||||
if (str.empty()) {
|
||||
if (len == 0) {
|
||||
return; // No need to update total_size
|
||||
}
|
||||
|
||||
// Calculate and directly add to total_size
|
||||
const uint32_t str_size = static_cast<uint32_t>(str.size());
|
||||
total_size += field_id_size + varint(str_size) + str_size;
|
||||
// Field ID + length varint + string bytes
|
||||
total_size += field_id_size + varint(static_cast<uint32_t>(len)) + static_cast<uint32_t>(len);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -704,6 +723,19 @@ class ProtoSize {
|
||||
total_size += field_id_size + varint(str_size) + str_size;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Calculates and adds the size of a bytes field to the total message size
|
||||
*/
|
||||
static inline void add_bytes_field(uint32_t &total_size, uint32_t field_id_size, size_t len) {
|
||||
// Skip calculation if bytes is empty
|
||||
if (len == 0) {
|
||||
return; // No need to update total_size
|
||||
}
|
||||
|
||||
// Field ID + length varint + data bytes
|
||||
total_size += field_id_size + varint(static_cast<uint32_t>(len)) + static_cast<uint32_t>(len);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Calculates and adds the size of a nested message field to the total message size
|
||||
*
|
||||
@ -827,7 +859,9 @@ class ProtoService {
|
||||
virtual bool is_authenticated() = 0;
|
||||
virtual bool is_connection_setup() = 0;
|
||||
virtual void on_fatal_error() = 0;
|
||||
#ifdef USE_API_PASSWORD
|
||||
virtual void on_unauthenticated_access() = 0;
|
||||
#endif
|
||||
virtual void on_no_setup_connection() = 0;
|
||||
/**
|
||||
* Create a buffer with a reserved size.
|
||||
@ -865,6 +899,7 @@ class ProtoService {
|
||||
}
|
||||
|
||||
bool check_authenticated_() {
|
||||
#ifdef USE_API_PASSWORD
|
||||
if (!this->check_connection_setup_()) {
|
||||
return false;
|
||||
}
|
||||
@ -873,8 +908,10 @@ class ProtoService {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
#else
|
||||
return this->check_connection_setup_();
|
||||
#endif
|
||||
}
|
||||
};
|
||||
|
||||
} // namespace api
|
||||
} // namespace esphome
|
||||
} // namespace esphome::api
|
||||
|
@ -3,8 +3,7 @@
|
||||
#include "api_connection.h"
|
||||
#include "esphome/core/log.h"
|
||||
|
||||
namespace esphome {
|
||||
namespace api {
|
||||
namespace esphome::api {
|
||||
|
||||
// Generate entity handler implementations using macros
|
||||
#ifdef USE_BINARY_SENSOR
|
||||
@ -69,6 +68,5 @@ INITIAL_STATE_HANDLER(update, update::UpdateEntity)
|
||||
|
||||
InitialStateIterator::InitialStateIterator(APIConnection *client) : client_(client) {}
|
||||
|
||||
} // namespace api
|
||||
} // namespace esphome
|
||||
} // namespace esphome::api
|
||||
#endif
|
||||
|
@ -5,8 +5,7 @@
|
||||
#include "esphome/core/component.h"
|
||||
#include "esphome/core/component_iterator.h"
|
||||
#include "esphome/core/controller.h"
|
||||
namespace esphome {
|
||||
namespace api {
|
||||
namespace esphome::api {
|
||||
|
||||
class APIConnection;
|
||||
|
||||
@ -89,6 +88,5 @@ class InitialStateIterator : public ComponentIterator {
|
||||
APIConnection *client_;
|
||||
};
|
||||
|
||||
} // namespace api
|
||||
} // namespace esphome
|
||||
} // namespace esphome::api
|
||||
#endif
|
||||
|
@ -1,8 +1,7 @@
|
||||
#include "user_services.h"
|
||||
#include "esphome/core/log.h"
|
||||
|
||||
namespace esphome {
|
||||
namespace api {
|
||||
namespace esphome::api {
|
||||
|
||||
template<> bool get_execute_arg_value<bool>(const ExecuteServiceArgument &arg) { return arg.bool_; }
|
||||
template<> int32_t get_execute_arg_value<int32_t>(const ExecuteServiceArgument &arg) {
|
||||
@ -40,5 +39,4 @@ template<> enums::ServiceArgType to_service_arg_type<std::vector<std::string>>()
|
||||
return enums::SERVICE_ARG_TYPE_STRING_ARRAY;
|
||||
}
|
||||
|
||||
} // namespace api
|
||||
} // namespace esphome
|
||||
} // namespace esphome::api
|
||||
|
@ -8,8 +8,7 @@
|
||||
#include "api_pb2.h"
|
||||
|
||||
#ifdef USE_API_SERVICES
|
||||
namespace esphome {
|
||||
namespace api {
|
||||
namespace esphome::api {
|
||||
|
||||
class UserServiceDescriptor {
|
||||
public:
|
||||
@ -33,14 +32,14 @@ template<typename... Ts> class UserServiceBase : public UserServiceDescriptor {
|
||||
|
||||
ListEntitiesServicesResponse encode_list_service_response() override {
|
||||
ListEntitiesServicesResponse msg;
|
||||
msg.name = this->name_;
|
||||
msg.set_name(StringRef(this->name_));
|
||||
msg.key = this->key_;
|
||||
std::array<enums::ServiceArgType, sizeof...(Ts)> arg_types = {to_service_arg_type<Ts>()...};
|
||||
for (int i = 0; i < sizeof...(Ts); i++) {
|
||||
ListEntitiesServicesArgument arg;
|
||||
msg.args.emplace_back();
|
||||
auto &arg = msg.args.back();
|
||||
arg.type = arg_types[i];
|
||||
arg.name = this->arg_names_[i];
|
||||
msg.args.push_back(arg);
|
||||
arg.set_name(StringRef(this->arg_names_[i]));
|
||||
}
|
||||
return msg;
|
||||
}
|
||||
@ -74,6 +73,5 @@ template<typename... Ts> class UserServiceTrigger : public UserServiceBase<Ts...
|
||||
void execute(Ts... x) override { this->trigger(x...); } // NOLINT
|
||||
};
|
||||
|
||||
} // namespace api
|
||||
} // namespace esphome
|
||||
} // namespace esphome::api
|
||||
#endif // USE_API_SERVICES
|
||||
|
@ -7,8 +7,6 @@ namespace as3935 {
|
||||
static const char *const TAG = "as3935";
|
||||
|
||||
void AS3935Component::setup() {
|
||||
ESP_LOGCONFIG(TAG, "Running setup");
|
||||
|
||||
this->irq_pin_->setup();
|
||||
LOG_PIN(" IRQ Pin: ", this->irq_pin_);
|
||||
|
||||
|
@ -7,9 +7,7 @@ namespace as3935_spi {
|
||||
static const char *const TAG = "as3935_spi";
|
||||
|
||||
void SPIAS3935Component::setup() {
|
||||
ESP_LOGI(TAG, "SPIAS3935Component setup started!");
|
||||
this->spi_setup();
|
||||
ESP_LOGI(TAG, "SPI setup finished!");
|
||||
AS3935Component::setup();
|
||||
}
|
||||
|
||||
|
@ -23,8 +23,6 @@ static const uint8_t REGISTER_AGC = 0x1A; // 8 bytes / R
|
||||
static const uint8_t REGISTER_MAGNITUDE = 0x1B; // 16 bytes / R
|
||||
|
||||
void AS5600Component::setup() {
|
||||
ESP_LOGCONFIG(TAG, "Running setup");
|
||||
|
||||
if (!this->read_byte(REGISTER_STATUS).has_value()) {
|
||||
this->mark_failed();
|
||||
return;
|
||||
|
@ -8,7 +8,6 @@ namespace as7341 {
|
||||
static const char *const TAG = "as7341";
|
||||
|
||||
void AS7341Component::setup() {
|
||||
ESP_LOGCONFIG(TAG, "Running setup");
|
||||
LOG_I2C_DEVICE(this);
|
||||
|
||||
// Verify device ID
|
||||
|
@ -71,7 +71,7 @@ bool AT581XComponent::i2c_read_reg(uint8_t addr, uint8_t &data) {
|
||||
return this->read_register(addr, &data, 1) == esphome::i2c::NO_ERROR;
|
||||
}
|
||||
|
||||
void AT581XComponent::setup() { ESP_LOGCONFIG(TAG, "Running setup"); }
|
||||
void AT581XComponent::setup() {}
|
||||
void AT581XComponent::dump_config() { LOG_I2C_DEVICE(this); }
|
||||
#define ARRAY_SIZE(X) (sizeof(X) / sizeof((X)[0]))
|
||||
bool AT581XComponent::i2c_write_config() {
|
||||
|
@ -41,7 +41,6 @@ void ATM90E26Component::update() {
|
||||
}
|
||||
|
||||
void ATM90E26Component::setup() {
|
||||
ESP_LOGCONFIG(TAG, "Running setup");
|
||||
this->spi_setup();
|
||||
|
||||
uint16_t mmode = 0x422; // default values for everything but L/N line current gains
|
||||
|
@ -109,7 +109,6 @@ void ATM90E32Component::update() {
|
||||
}
|
||||
|
||||
void ATM90E32Component::setup() {
|
||||
ESP_LOGCONFIG(TAG, "Running setup");
|
||||
this->spi_setup();
|
||||
|
||||
uint16_t mmode0 = 0x87; // 3P4W 50Hz
|
||||
|
@ -15,7 +15,7 @@ class AudioStreamInfo {
|
||||
* - An audio sample represents a unit of audio for one channel.
|
||||
* - A frame represents a unit of audio with a sample for every channel.
|
||||
*
|
||||
* In gneneral, converting between bytes, samples, and frames shouldn't result in rounding errors so long as frames
|
||||
* In general, converting between bytes, samples, and frames shouldn't result in rounding errors so long as frames
|
||||
* are used as the main unit when transferring audio data. Durations may result in rounding for certain sample rates;
|
||||
* e.g., 44.1 KHz. The ``frames_to_milliseconds_with_remainder`` function should be used for accuracy, as it takes
|
||||
* into account the remainder rather than just ignoring any rounding.
|
||||
@ -76,7 +76,7 @@ class AudioStreamInfo {
|
||||
|
||||
/// @brief Computes the duration, in microseconds, the given amount of frames represents.
|
||||
/// @param frames Number of audio frames
|
||||
/// @return Duration in microseconds `frames` respresents. May be slightly inaccurate due to integer divison rounding
|
||||
/// @return Duration in microseconds `frames` represents. May be slightly inaccurate due to integer division rounding
|
||||
/// for certain sample rates.
|
||||
uint32_t frames_to_microseconds(uint32_t frames) const;
|
||||
|
||||
|
@ -17,7 +17,6 @@ constexpr static const uint8_t AXS_READ_TOUCHPAD[11] = {0xb5, 0xab, 0xa5, 0x5a,
|
||||
}
|
||||
|
||||
void AXS15231Touchscreen::setup() {
|
||||
ESP_LOGCONFIG(TAG, "Running setup");
|
||||
if (this->reset_pin_ != nullptr) {
|
||||
this->reset_pin_->setup();
|
||||
this->reset_pin_->digital_write(false);
|
||||
@ -36,7 +35,6 @@ void AXS15231Touchscreen::setup() {
|
||||
if (this->y_raw_max_ == 0) {
|
||||
this->y_raw_max_ = this->display_->get_native_height();
|
||||
}
|
||||
ESP_LOGCONFIG(TAG, "AXS15231 Touchscreen setup complete");
|
||||
}
|
||||
|
||||
void AXS15231Touchscreen::update_touches() {
|
||||
|
@ -121,8 +121,6 @@ void spi_dma_tx_finish_callback(unsigned int param) {
|
||||
}
|
||||
|
||||
void BekenSPILEDStripLightOutput::setup() {
|
||||
ESP_LOGCONFIG(TAG, "Running setup");
|
||||
|
||||
size_t buffer_size = this->get_buffer_size_();
|
||||
size_t dma_buffer_size = (buffer_size * 8) + (2 * 64);
|
||||
|
||||
|
@ -38,7 +38,6 @@ MTreg:
|
||||
*/
|
||||
|
||||
void BH1750Sensor::setup() {
|
||||
ESP_LOGCONFIG(TAG, "Running setup for '%s'", this->name_.c_str());
|
||||
uint8_t turn_on = BH1750_COMMAND_POWER_ON;
|
||||
if (this->write(&turn_on, 1) != i2c::ERROR_OK) {
|
||||
this->mark_failed();
|
||||
|
@ -266,8 +266,10 @@ async def delayed_off_filter_to_code(config, filter_id):
|
||||
async def autorepeat_filter_to_code(config, filter_id):
|
||||
timings = []
|
||||
if len(config) > 0:
|
||||
for conf in config:
|
||||
timings.append((conf[CONF_DELAY], conf[CONF_TIME_OFF], conf[CONF_TIME_ON]))
|
||||
timings.extend(
|
||||
(conf[CONF_DELAY], conf[CONF_TIME_OFF], conf[CONF_TIME_ON])
|
||||
for conf in config
|
||||
)
|
||||
else:
|
||||
timings.append(
|
||||
(
|
||||
@ -573,16 +575,15 @@ async def setup_binary_sensor_core_(var, config):
|
||||
await automation.build_automation(trigger, [], conf)
|
||||
|
||||
for conf in config.get(CONF_ON_MULTI_CLICK, []):
|
||||
timings = []
|
||||
for tim in conf[CONF_TIMING]:
|
||||
timings.append(
|
||||
cg.StructInitializer(
|
||||
MultiClickTriggerEvent,
|
||||
("state", tim[CONF_STATE]),
|
||||
("min_length", tim[CONF_MIN_LENGTH]),
|
||||
("max_length", tim.get(CONF_MAX_LENGTH, 4294967294)),
|
||||
)
|
||||
timings = [
|
||||
cg.StructInitializer(
|
||||
MultiClickTriggerEvent,
|
||||
("state", tim[CONF_STATE]),
|
||||
("min_length", tim[CONF_MIN_LENGTH]),
|
||||
("max_length", tim.get(CONF_MAX_LENGTH, 4294967294)),
|
||||
)
|
||||
for tim in conf[CONF_TIMING]
|
||||
]
|
||||
trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var, timings)
|
||||
if CONF_INVALID_COOLDOWN in conf:
|
||||
cg.add(trigger.set_invalid_cooldown(conf[CONF_INVALID_COOLDOWN]))
|
||||
|
@ -8,16 +8,184 @@
|
||||
|
||||
#include "bluetooth_proxy.h"
|
||||
|
||||
namespace esphome {
|
||||
namespace bluetooth_proxy {
|
||||
namespace esphome::bluetooth_proxy {
|
||||
|
||||
static const char *const TAG = "bluetooth_proxy.connection";
|
||||
|
||||
static void fill_128bit_uuid_array(std::array<uint64_t, 2> &out, esp_bt_uuid_t uuid_source) {
|
||||
esp_bt_uuid_t uuid = espbt::ESPBTUUID::from_uuid(uuid_source).as_128bit().get_uuid();
|
||||
out[0] = ((uint64_t) uuid.uuid.uuid128[15] << 56) | ((uint64_t) uuid.uuid.uuid128[14] << 48) |
|
||||
((uint64_t) uuid.uuid.uuid128[13] << 40) | ((uint64_t) uuid.uuid.uuid128[12] << 32) |
|
||||
((uint64_t) uuid.uuid.uuid128[11] << 24) | ((uint64_t) uuid.uuid.uuid128[10] << 16) |
|
||||
((uint64_t) uuid.uuid.uuid128[9] << 8) | ((uint64_t) uuid.uuid.uuid128[8]);
|
||||
out[1] = ((uint64_t) uuid.uuid.uuid128[7] << 56) | ((uint64_t) uuid.uuid.uuid128[6] << 48) |
|
||||
((uint64_t) uuid.uuid.uuid128[5] << 40) | ((uint64_t) uuid.uuid.uuid128[4] << 32) |
|
||||
((uint64_t) uuid.uuid.uuid128[3] << 24) | ((uint64_t) uuid.uuid.uuid128[2] << 16) |
|
||||
((uint64_t) uuid.uuid.uuid128[1] << 8) | ((uint64_t) uuid.uuid.uuid128[0]);
|
||||
}
|
||||
|
||||
void BluetoothConnection::dump_config() {
|
||||
ESP_LOGCONFIG(TAG, "BLE Connection:");
|
||||
BLEClientBase::dump_config();
|
||||
}
|
||||
|
||||
void BluetoothConnection::loop() {
|
||||
BLEClientBase::loop();
|
||||
|
||||
// Early return if no active connection or not in service discovery phase
|
||||
if (this->address_ == 0 || this->send_service_ < 0 || this->send_service_ > this->service_count_) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Handle service discovery
|
||||
this->send_service_for_discovery_();
|
||||
}
|
||||
|
||||
void BluetoothConnection::reset_connection_(esp_err_t reason) {
|
||||
// Send disconnection notification
|
||||
this->proxy_->send_device_connection(this->address_, false, 0, reason);
|
||||
|
||||
// Important: If we were in the middle of sending services, we do NOT send
|
||||
// send_gatt_services_done() here. This ensures the client knows that
|
||||
// the service discovery was interrupted and can retry. The client
|
||||
// (aioesphomeapi) implements a 30-second timeout (DEFAULT_BLE_TIMEOUT)
|
||||
// to detect incomplete service discovery rather than relying on us to
|
||||
// tell them about a partial list.
|
||||
this->set_address(0);
|
||||
this->send_service_ = DONE_SENDING_SERVICES;
|
||||
this->proxy_->send_connections_free();
|
||||
}
|
||||
|
||||
void BluetoothConnection::send_service_for_discovery_() {
|
||||
if (this->send_service_ == this->service_count_) {
|
||||
this->send_service_ = DONE_SENDING_SERVICES;
|
||||
this->proxy_->send_gatt_services_done(this->address_);
|
||||
if (this->connection_type_ == espbt::ConnectionType::V3_WITH_CACHE ||
|
||||
this->connection_type_ == espbt::ConnectionType::V3_WITHOUT_CACHE) {
|
||||
this->release_services();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Early return if no API connection
|
||||
auto *api_conn = this->proxy_->get_api_connection();
|
||||
if (api_conn == nullptr) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Send next service
|
||||
esp_gattc_service_elem_t service_result;
|
||||
uint16_t service_count = 1;
|
||||
esp_gatt_status_t service_status = esp_ble_gattc_get_service(this->gattc_if_, this->conn_id_, nullptr,
|
||||
&service_result, &service_count, this->send_service_);
|
||||
this->send_service_++;
|
||||
|
||||
if (service_status != ESP_GATT_OK || service_count == 0) {
|
||||
ESP_LOGE(TAG, "[%d] [%s] esp_ble_gattc_get_service %s, status=%d, service_count=%d, offset=%d",
|
||||
this->connection_index_, this->address_str().c_str(), service_status != ESP_GATT_OK ? "error" : "missing",
|
||||
service_status, service_count, this->send_service_ - 1);
|
||||
return;
|
||||
}
|
||||
|
||||
api::BluetoothGATTGetServicesResponse resp;
|
||||
resp.address = this->address_;
|
||||
auto &service_resp = resp.services[0];
|
||||
fill_128bit_uuid_array(service_resp.uuid, service_result.uuid);
|
||||
service_resp.handle = service_result.start_handle;
|
||||
|
||||
// Get the number of characteristics directly with one call
|
||||
uint16_t total_char_count = 0;
|
||||
esp_gatt_status_t char_count_status =
|
||||
esp_ble_gattc_get_attr_count(this->gattc_if_, this->conn_id_, ESP_GATT_DB_CHARACTERISTIC,
|
||||
service_result.start_handle, service_result.end_handle, 0, &total_char_count);
|
||||
|
||||
if (char_count_status != ESP_GATT_OK) {
|
||||
ESP_LOGW(TAG, "[%d] [%s] Error getting characteristic count, status=%d", this->connection_index_,
|
||||
this->address_str().c_str(), char_count_status);
|
||||
return;
|
||||
}
|
||||
|
||||
if (total_char_count == 0) {
|
||||
// No characteristics, just send the service response
|
||||
api_conn->send_message(resp, api::BluetoothGATTGetServicesResponse::MESSAGE_TYPE);
|
||||
return;
|
||||
}
|
||||
|
||||
// Reserve space and process characteristics
|
||||
service_resp.characteristics.reserve(total_char_count);
|
||||
uint16_t char_offset = 0;
|
||||
esp_gattc_char_elem_t char_result;
|
||||
while (true) { // characteristics
|
||||
uint16_t char_count = 1;
|
||||
esp_gatt_status_t char_status =
|
||||
esp_ble_gattc_get_all_char(this->gattc_if_, this->conn_id_, service_result.start_handle,
|
||||
service_result.end_handle, &char_result, &char_count, char_offset);
|
||||
if (char_status == ESP_GATT_INVALID_OFFSET || char_status == ESP_GATT_NOT_FOUND) {
|
||||
break;
|
||||
}
|
||||
if (char_status != ESP_GATT_OK) {
|
||||
ESP_LOGE(TAG, "[%d] [%s] esp_ble_gattc_get_all_char error, status=%d", this->connection_index_,
|
||||
this->address_str().c_str(), char_status);
|
||||
return;
|
||||
}
|
||||
if (char_count == 0) {
|
||||
break;
|
||||
}
|
||||
|
||||
service_resp.characteristics.emplace_back();
|
||||
auto &characteristic_resp = service_resp.characteristics.back();
|
||||
fill_128bit_uuid_array(characteristic_resp.uuid, char_result.uuid);
|
||||
characteristic_resp.handle = char_result.char_handle;
|
||||
characteristic_resp.properties = char_result.properties;
|
||||
char_offset++;
|
||||
|
||||
// Get the number of descriptors directly with one call
|
||||
uint16_t total_desc_count = 0;
|
||||
esp_gatt_status_t desc_count_status = esp_ble_gattc_get_attr_count(
|
||||
this->gattc_if_, this->conn_id_, ESP_GATT_DB_DESCRIPTOR, 0, 0, char_result.char_handle, &total_desc_count);
|
||||
|
||||
if (desc_count_status != ESP_GATT_OK) {
|
||||
ESP_LOGW(TAG, "[%d] [%s] Error getting descriptor count for char handle %d, status=%d", this->connection_index_,
|
||||
this->address_str().c_str(), char_result.char_handle, desc_count_status);
|
||||
return;
|
||||
}
|
||||
if (total_desc_count == 0) {
|
||||
// No descriptors, continue to next characteristic
|
||||
continue;
|
||||
}
|
||||
|
||||
// Reserve space and process descriptors
|
||||
characteristic_resp.descriptors.reserve(total_desc_count);
|
||||
uint16_t desc_offset = 0;
|
||||
esp_gattc_descr_elem_t desc_result;
|
||||
while (true) { // descriptors
|
||||
uint16_t desc_count = 1;
|
||||
esp_gatt_status_t desc_status = esp_ble_gattc_get_all_descr(
|
||||
this->gattc_if_, this->conn_id_, char_result.char_handle, &desc_result, &desc_count, desc_offset);
|
||||
if (desc_status == ESP_GATT_INVALID_OFFSET || desc_status == ESP_GATT_NOT_FOUND) {
|
||||
break;
|
||||
}
|
||||
if (desc_status != ESP_GATT_OK) {
|
||||
ESP_LOGE(TAG, "[%d] [%s] esp_ble_gattc_get_all_descr error, status=%d", this->connection_index_,
|
||||
this->address_str().c_str(), desc_status);
|
||||
return;
|
||||
}
|
||||
if (desc_count == 0) {
|
||||
break; // No more descriptors
|
||||
}
|
||||
|
||||
characteristic_resp.descriptors.emplace_back();
|
||||
auto &descriptor_resp = characteristic_resp.descriptors.back();
|
||||
fill_128bit_uuid_array(descriptor_resp.uuid, desc_result.uuid);
|
||||
descriptor_resp.handle = desc_result.handle;
|
||||
desc_offset++;
|
||||
}
|
||||
}
|
||||
|
||||
// Send the message (we already checked api_conn is not null at the beginning)
|
||||
api_conn->send_message(resp, api::BluetoothGATTGetServicesResponse::MESSAGE_TYPE);
|
||||
}
|
||||
|
||||
bool BluetoothConnection::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if,
|
||||
esp_ble_gattc_cb_param_t *param) {
|
||||
if (!BLEClientBase::gattc_event_handler(event, gattc_if, param))
|
||||
@ -25,22 +193,16 @@ bool BluetoothConnection::gattc_event_handler(esp_gattc_cb_event_t event, esp_ga
|
||||
|
||||
switch (event) {
|
||||
case ESP_GATTC_DISCONNECT_EVT: {
|
||||
this->proxy_->send_device_connection(this->address_, false, 0, param->disconnect.reason);
|
||||
this->set_address(0);
|
||||
this->proxy_->send_connections_free();
|
||||
this->reset_connection_(param->disconnect.reason);
|
||||
break;
|
||||
}
|
||||
case ESP_GATTC_CLOSE_EVT: {
|
||||
this->proxy_->send_device_connection(this->address_, false, 0, param->close.reason);
|
||||
this->set_address(0);
|
||||
this->proxy_->send_connections_free();
|
||||
this->reset_connection_(param->close.reason);
|
||||
break;
|
||||
}
|
||||
case ESP_GATTC_OPEN_EVT: {
|
||||
if (param->open.status != ESP_GATT_OK && param->open.status != ESP_GATT_ALREADY_OPEN) {
|
||||
this->proxy_->send_device_connection(this->address_, false, 0, param->open.status);
|
||||
this->set_address(0);
|
||||
this->proxy_->send_connections_free();
|
||||
this->reset_connection_(param->open.status);
|
||||
} else if (this->connection_type_ == espbt::ConnectionType::V3_WITH_CACHE) {
|
||||
this->proxy_->send_device_connection(this->address_, true, this->mtu_);
|
||||
this->proxy_->send_connections_free();
|
||||
@ -72,9 +234,7 @@ bool BluetoothConnection::gattc_event_handler(esp_gattc_cb_event_t event, esp_ga
|
||||
api::BluetoothGATTReadResponse resp;
|
||||
resp.address = this->address_;
|
||||
resp.handle = param->read.handle;
|
||||
resp.data.reserve(param->read.value_len);
|
||||
// Use bulk insert instead of individual push_backs
|
||||
resp.data.insert(resp.data.end(), param->read.value, param->read.value + param->read.value_len);
|
||||
resp.set_data(param->read.value, param->read.value_len);
|
||||
this->proxy_->get_api_connection()->send_message(resp, api::BluetoothGATTReadResponse::MESSAGE_TYPE);
|
||||
break;
|
||||
}
|
||||
@ -125,9 +285,7 @@ bool BluetoothConnection::gattc_event_handler(esp_gattc_cb_event_t event, esp_ga
|
||||
api::BluetoothGATTNotifyDataResponse resp;
|
||||
resp.address = this->address_;
|
||||
resp.handle = param->notify.handle;
|
||||
resp.data.reserve(param->notify.value_len);
|
||||
// Use bulk insert instead of individual push_backs
|
||||
resp.data.insert(resp.data.end(), param->notify.value, param->notify.value + param->notify.value_len);
|
||||
resp.set_data(param->notify.value, param->notify.value_len);
|
||||
this->proxy_->get_api_connection()->send_message(resp, api::BluetoothGATTNotifyDataResponse::MESSAGE_TYPE);
|
||||
break;
|
||||
}
|
||||
@ -265,7 +423,6 @@ esp32_ble_tracker::AdvertisementParserType BluetoothConnection::get_advertisemen
|
||||
return this->proxy_->get_advertisement_parser_type();
|
||||
}
|
||||
|
||||
} // namespace bluetooth_proxy
|
||||
} // namespace esphome
|
||||
} // namespace esphome::bluetooth_proxy
|
||||
|
||||
#endif // USE_ESP32
|
||||
|
@ -4,14 +4,14 @@
|
||||
|
||||
#include "esphome/components/esp32_ble_client/ble_client_base.h"
|
||||
|
||||
namespace esphome {
|
||||
namespace bluetooth_proxy {
|
||||
namespace esphome::bluetooth_proxy {
|
||||
|
||||
class BluetoothProxy;
|
||||
|
||||
class BluetoothConnection : public esp32_ble_client::BLEClientBase {
|
||||
public:
|
||||
void dump_config() override;
|
||||
void loop() override;
|
||||
bool gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if,
|
||||
esp_ble_gattc_cb_param_t *param) override;
|
||||
void gap_event_handler(esp_gap_ble_cb_event_t event, esp_ble_gap_cb_param_t *param) override;
|
||||
@ -27,6 +27,9 @@ class BluetoothConnection : public esp32_ble_client::BLEClientBase {
|
||||
protected:
|
||||
friend class BluetoothProxy;
|
||||
|
||||
void send_service_for_discovery_();
|
||||
void reset_connection_(esp_err_t reason);
|
||||
|
||||
// Memory optimized layout for 32-bit systems
|
||||
// Group 1: Pointers (4 bytes each, naturally aligned)
|
||||
BluetoothProxy *proxy_;
|
||||
@ -39,7 +42,6 @@ class BluetoothConnection : public esp32_ble_client::BLEClientBase {
|
||||
// 1 byte used, 1 byte padding
|
||||
};
|
||||
|
||||
} // namespace bluetooth_proxy
|
||||
} // namespace esphome
|
||||
} // namespace esphome::bluetooth_proxy
|
||||
|
||||
#endif // USE_ESP32
|
||||
|
@ -3,30 +3,38 @@
|
||||
#include "esphome/core/log.h"
|
||||
#include "esphome/core/macros.h"
|
||||
#include "esphome/core/application.h"
|
||||
#include <cstring>
|
||||
|
||||
#ifdef USE_ESP32
|
||||
|
||||
namespace esphome {
|
||||
namespace bluetooth_proxy {
|
||||
namespace esphome::bluetooth_proxy {
|
||||
|
||||
static const char *const TAG = "bluetooth_proxy";
|
||||
static const int DONE_SENDING_SERVICES = -2;
|
||||
|
||||
std::vector<uint64_t> get_128bit_uuid_vec(esp_bt_uuid_t uuid_source) {
|
||||
esp_bt_uuid_t uuid = espbt::ESPBTUUID::from_uuid(uuid_source).as_128bit().get_uuid();
|
||||
return std::vector<uint64_t>{((uint64_t) uuid.uuid.uuid128[15] << 56) | ((uint64_t) uuid.uuid.uuid128[14] << 48) |
|
||||
((uint64_t) uuid.uuid.uuid128[13] << 40) | ((uint64_t) uuid.uuid.uuid128[12] << 32) |
|
||||
((uint64_t) uuid.uuid.uuid128[11] << 24) | ((uint64_t) uuid.uuid.uuid128[10] << 16) |
|
||||
((uint64_t) uuid.uuid.uuid128[9] << 8) | ((uint64_t) uuid.uuid.uuid128[8]),
|
||||
((uint64_t) uuid.uuid.uuid128[7] << 56) | ((uint64_t) uuid.uuid.uuid128[6] << 48) |
|
||||
((uint64_t) uuid.uuid.uuid128[5] << 40) | ((uint64_t) uuid.uuid.uuid128[4] << 32) |
|
||||
((uint64_t) uuid.uuid.uuid128[3] << 24) | ((uint64_t) uuid.uuid.uuid128[2] << 16) |
|
||||
((uint64_t) uuid.uuid.uuid128[1] << 8) | ((uint64_t) uuid.uuid.uuid128[0])};
|
||||
}
|
||||
// Batch size for BLE advertisements to maximize WiFi efficiency
|
||||
// Each advertisement is up to 80 bytes when packaged (including protocol overhead)
|
||||
// Most advertisements are 20-30 bytes, allowing even more to fit per packet
|
||||
// 16 advertisements × 80 bytes (worst case) = 1280 bytes out of ~1320 bytes usable payload
|
||||
// This achieves ~97% WiFi MTU utilization while staying under the limit
|
||||
static constexpr size_t FLUSH_BATCH_SIZE = 16;
|
||||
|
||||
// Verify BLE advertisement data array size matches the BLE specification (31 bytes adv + 31 bytes scan response)
|
||||
static_assert(sizeof(((api::BluetoothLERawAdvertisement *) nullptr)->data) == 62,
|
||||
"BLE advertisement data array size mismatch");
|
||||
|
||||
BluetoothProxy::BluetoothProxy() { global_bluetooth_proxy = this; }
|
||||
|
||||
void BluetoothProxy::setup() {
|
||||
// Pre-allocate response object
|
||||
this->response_ = std::make_unique<api::BluetoothLERawAdvertisementsResponse>();
|
||||
|
||||
// Reserve capacity but start with size 0
|
||||
// Reserve 50% since we'll grow naturally and flush at FLUSH_BATCH_SIZE
|
||||
this->response_->advertisements.reserve(FLUSH_BATCH_SIZE / 2);
|
||||
|
||||
// Don't pre-allocate pool - let it grow only if needed in busy environments
|
||||
// Many devices in quiet areas will never need the overflow pool
|
||||
|
||||
this->parent_->add_scanner_state_callback([this](esp32_ble_tracker::ScannerState state) {
|
||||
if (this->api_connection_ != nullptr) {
|
||||
this->send_bluetooth_scanner_state_(state);
|
||||
@ -50,110 +58,74 @@ bool BluetoothProxy::parse_device(const esp32_ble_tracker::ESPBTDevice &device)
|
||||
}
|
||||
#endif
|
||||
|
||||
// Batch size for BLE advertisements to maximize WiFi efficiency
|
||||
// Each advertisement is up to 80 bytes when packaged (including protocol overhead)
|
||||
// Most advertisements are 20-30 bytes, allowing even more to fit per packet
|
||||
// 16 advertisements × 80 bytes (worst case) = 1280 bytes out of ~1320 bytes usable payload
|
||||
// This achieves ~97% WiFi MTU utilization while staying under the limit
|
||||
static constexpr size_t FLUSH_BATCH_SIZE = 16;
|
||||
|
||||
namespace {
|
||||
// Batch buffer in anonymous namespace to avoid guard variable (saves 8 bytes)
|
||||
// This is initialized at program startup before any threads
|
||||
// NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables)
|
||||
std::vector<api::BluetoothLERawAdvertisement> batch_buffer;
|
||||
} // namespace
|
||||
|
||||
static std::vector<api::BluetoothLERawAdvertisement> &get_batch_buffer() { return batch_buffer; }
|
||||
|
||||
bool BluetoothProxy::parse_devices(const esp32_ble::BLEScanResult *scan_results, size_t count) {
|
||||
if (!api::global_api_server->is_connected() || this->api_connection_ == nullptr)
|
||||
return false;
|
||||
|
||||
// Get the batch buffer reference
|
||||
auto &batch_buffer = get_batch_buffer();
|
||||
auto &advertisements = this->response_->advertisements;
|
||||
|
||||
// Reserve additional capacity if needed
|
||||
size_t new_size = batch_buffer.size() + count;
|
||||
if (batch_buffer.capacity() < new_size) {
|
||||
batch_buffer.reserve(new_size);
|
||||
}
|
||||
|
||||
// Add new advertisements to the batch buffer
|
||||
for (size_t i = 0; i < count; i++) {
|
||||
auto &result = scan_results[i];
|
||||
uint8_t length = result.adv_data_len + result.scan_rsp_len;
|
||||
|
||||
batch_buffer.emplace_back();
|
||||
auto &adv = batch_buffer.back();
|
||||
// Check if we need to expand the vector
|
||||
if (this->advertisement_count_ >= advertisements.size()) {
|
||||
if (this->advertisement_pool_.empty()) {
|
||||
// No room in pool, need to allocate
|
||||
advertisements.emplace_back();
|
||||
} else {
|
||||
// Pull from pool
|
||||
advertisements.push_back(std::move(this->advertisement_pool_.back()));
|
||||
this->advertisement_pool_.pop_back();
|
||||
}
|
||||
}
|
||||
|
||||
// Fill in the data directly at current position
|
||||
auto &adv = advertisements[this->advertisement_count_];
|
||||
adv.address = esp32_ble::ble_addr_to_uint64(result.bda);
|
||||
adv.rssi = result.rssi;
|
||||
adv.address_type = result.ble_addr_type;
|
||||
adv.data.assign(&result.ble_adv[0], &result.ble_adv[length]);
|
||||
adv.data_len = length;
|
||||
std::memcpy(adv.data, result.ble_adv, length);
|
||||
|
||||
this->advertisement_count_++;
|
||||
|
||||
ESP_LOGV(TAG, "Queuing raw packet from %02X:%02X:%02X:%02X:%02X:%02X, length %d. RSSI: %d dB", result.bda[0],
|
||||
result.bda[1], result.bda[2], result.bda[3], result.bda[4], result.bda[5], length, result.rssi);
|
||||
}
|
||||
|
||||
// Only send if we've accumulated a good batch size to maximize batching efficiency
|
||||
// https://github.com/esphome/backlog/issues/21
|
||||
if (batch_buffer.size() >= FLUSH_BATCH_SIZE) {
|
||||
this->flush_pending_advertisements();
|
||||
// Flush if we have reached FLUSH_BATCH_SIZE
|
||||
if (this->advertisement_count_ >= FLUSH_BATCH_SIZE) {
|
||||
this->flush_pending_advertisements();
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
void BluetoothProxy::flush_pending_advertisements() {
|
||||
auto &batch_buffer = get_batch_buffer();
|
||||
if (batch_buffer.empty() || !api::global_api_server->is_connected() || this->api_connection_ == nullptr)
|
||||
if (this->advertisement_count_ == 0 || !api::global_api_server->is_connected() || this->api_connection_ == nullptr)
|
||||
return;
|
||||
|
||||
api::BluetoothLERawAdvertisementsResponse resp;
|
||||
resp.advertisements.swap(batch_buffer);
|
||||
this->api_connection_->send_message(resp, api::BluetoothLERawAdvertisementsResponse::MESSAGE_TYPE);
|
||||
auto &advertisements = this->response_->advertisements;
|
||||
|
||||
// Return any items beyond advertisement_count_ to the pool
|
||||
if (advertisements.size() > this->advertisement_count_) {
|
||||
// Move unused items back to pool
|
||||
this->advertisement_pool_.insert(this->advertisement_pool_.end(),
|
||||
std::make_move_iterator(advertisements.begin() + this->advertisement_count_),
|
||||
std::make_move_iterator(advertisements.end()));
|
||||
|
||||
// Resize to actual count
|
||||
advertisements.resize(this->advertisement_count_);
|
||||
}
|
||||
|
||||
// Send the message
|
||||
this->api_connection_->send_message(*this->response_, api::BluetoothLERawAdvertisementsResponse::MESSAGE_TYPE);
|
||||
|
||||
// Reset count - existing items will be overwritten in next batch
|
||||
this->advertisement_count_ = 0;
|
||||
}
|
||||
|
||||
#ifdef USE_ESP32_BLE_DEVICE
|
||||
void BluetoothProxy::send_api_packet_(const esp32_ble_tracker::ESPBTDevice &device) {
|
||||
api::BluetoothLEAdvertisementResponse resp;
|
||||
resp.address = device.address_uint64();
|
||||
resp.address_type = device.get_address_type();
|
||||
if (!device.get_name().empty())
|
||||
resp.name = device.get_name();
|
||||
resp.rssi = device.get_rssi();
|
||||
|
||||
// Pre-allocate vectors based on known sizes
|
||||
auto service_uuids = device.get_service_uuids();
|
||||
resp.service_uuids.reserve(service_uuids.size());
|
||||
for (auto &uuid : service_uuids) {
|
||||
resp.service_uuids.emplace_back(uuid.to_string());
|
||||
}
|
||||
|
||||
// Pre-allocate service data vector
|
||||
auto service_datas = device.get_service_datas();
|
||||
resp.service_data.reserve(service_datas.size());
|
||||
for (auto &data : service_datas) {
|
||||
resp.service_data.emplace_back();
|
||||
auto &service_data = resp.service_data.back();
|
||||
service_data.uuid = data.uuid.to_string();
|
||||
service_data.data.assign(data.data.begin(), data.data.end());
|
||||
}
|
||||
|
||||
// Pre-allocate manufacturer data vector
|
||||
auto manufacturer_datas = device.get_manufacturer_datas();
|
||||
resp.manufacturer_data.reserve(manufacturer_datas.size());
|
||||
for (auto &data : manufacturer_datas) {
|
||||
resp.manufacturer_data.emplace_back();
|
||||
auto &manufacturer_data = resp.manufacturer_data.back();
|
||||
manufacturer_data.uuid = data.uuid.to_string();
|
||||
manufacturer_data.data.assign(data.data.begin(), data.data.end());
|
||||
}
|
||||
|
||||
this->api_connection_->send_message(resp, api::BluetoothLEAdvertisementResponse::MESSAGE_TYPE);
|
||||
}
|
||||
#endif // USE_ESP32_BLE_DEVICE
|
||||
|
||||
void BluetoothProxy::dump_config() {
|
||||
ESP_LOGCONFIG(TAG, "Bluetooth Proxy:");
|
||||
ESP_LOGCONFIG(TAG,
|
||||
@ -187,130 +159,12 @@ void BluetoothProxy::loop() {
|
||||
}
|
||||
|
||||
// Flush any pending BLE advertisements that have been accumulated but not yet sent
|
||||
static uint32_t last_flush_time = 0;
|
||||
uint32_t now = App.get_loop_component_start_time();
|
||||
|
||||
// Flush accumulated advertisements every 100ms
|
||||
if (now - last_flush_time >= 100) {
|
||||
if (now - this->last_advertisement_flush_time_ >= 100) {
|
||||
this->flush_pending_advertisements();
|
||||
last_flush_time = now;
|
||||
}
|
||||
for (auto *connection : this->connections_) {
|
||||
if (connection->send_service_ == connection->service_count_) {
|
||||
connection->send_service_ = DONE_SENDING_SERVICES;
|
||||
this->send_gatt_services_done(connection->get_address());
|
||||
if (connection->connection_type_ == espbt::ConnectionType::V3_WITH_CACHE ||
|
||||
connection->connection_type_ == espbt::ConnectionType::V3_WITHOUT_CACHE) {
|
||||
connection->release_services();
|
||||
}
|
||||
} else if (connection->send_service_ >= 0) {
|
||||
esp_gattc_service_elem_t service_result;
|
||||
uint16_t service_count = 1;
|
||||
esp_gatt_status_t service_status =
|
||||
esp_ble_gattc_get_service(connection->get_gattc_if(), connection->get_conn_id(), nullptr, &service_result,
|
||||
&service_count, connection->send_service_);
|
||||
connection->send_service_++;
|
||||
if (service_status != ESP_GATT_OK) {
|
||||
ESP_LOGE(TAG, "[%d] [%s] esp_ble_gattc_get_service error at offset=%d, status=%d",
|
||||
connection->get_connection_index(), connection->address_str().c_str(), connection->send_service_ - 1,
|
||||
service_status);
|
||||
continue;
|
||||
}
|
||||
if (service_count == 0) {
|
||||
ESP_LOGE(TAG, "[%d] [%s] esp_ble_gattc_get_service missing, service_count=%d",
|
||||
connection->get_connection_index(), connection->address_str().c_str(), service_count);
|
||||
continue;
|
||||
}
|
||||
api::BluetoothGATTGetServicesResponse resp;
|
||||
resp.address = connection->get_address();
|
||||
resp.services.reserve(1); // Always one service per response in this implementation
|
||||
api::BluetoothGATTService service_resp;
|
||||
service_resp.uuid = get_128bit_uuid_vec(service_result.uuid);
|
||||
service_resp.handle = service_result.start_handle;
|
||||
uint16_t char_offset = 0;
|
||||
esp_gattc_char_elem_t char_result;
|
||||
// Get the number of characteristics directly with one call
|
||||
uint16_t total_char_count = 0;
|
||||
esp_gatt_status_t char_count_status = esp_ble_gattc_get_attr_count(
|
||||
connection->get_gattc_if(), connection->get_conn_id(), ESP_GATT_DB_CHARACTERISTIC,
|
||||
service_result.start_handle, service_result.end_handle, 0, &total_char_count);
|
||||
|
||||
if (char_count_status == ESP_GATT_OK && total_char_count > 0) {
|
||||
// Only reserve if we successfully got a count
|
||||
service_resp.characteristics.reserve(total_char_count);
|
||||
} else if (char_count_status != ESP_GATT_OK) {
|
||||
ESP_LOGW(TAG, "[%d] [%s] Error getting characteristic count, status=%d", connection->get_connection_index(),
|
||||
connection->address_str().c_str(), char_count_status);
|
||||
}
|
||||
|
||||
// Now process characteristics
|
||||
while (true) { // characteristics
|
||||
uint16_t char_count = 1;
|
||||
esp_gatt_status_t char_status = esp_ble_gattc_get_all_char(
|
||||
connection->get_gattc_if(), connection->get_conn_id(), service_result.start_handle,
|
||||
service_result.end_handle, &char_result, &char_count, char_offset);
|
||||
if (char_status == ESP_GATT_INVALID_OFFSET || char_status == ESP_GATT_NOT_FOUND) {
|
||||
break;
|
||||
}
|
||||
if (char_status != ESP_GATT_OK) {
|
||||
ESP_LOGE(TAG, "[%d] [%s] esp_ble_gattc_get_all_char error, status=%d", connection->get_connection_index(),
|
||||
connection->address_str().c_str(), char_status);
|
||||
break;
|
||||
}
|
||||
if (char_count == 0) {
|
||||
break;
|
||||
}
|
||||
api::BluetoothGATTCharacteristic characteristic_resp;
|
||||
characteristic_resp.uuid = get_128bit_uuid_vec(char_result.uuid);
|
||||
characteristic_resp.handle = char_result.char_handle;
|
||||
characteristic_resp.properties = char_result.properties;
|
||||
char_offset++;
|
||||
|
||||
// Get the number of descriptors directly with one call
|
||||
uint16_t total_desc_count = 0;
|
||||
esp_gatt_status_t desc_count_status =
|
||||
esp_ble_gattc_get_attr_count(connection->get_gattc_if(), connection->get_conn_id(), ESP_GATT_DB_DESCRIPTOR,
|
||||
char_result.char_handle, service_result.end_handle, 0, &total_desc_count);
|
||||
|
||||
if (desc_count_status == ESP_GATT_OK && total_desc_count > 0) {
|
||||
// Only reserve if we successfully got a count
|
||||
characteristic_resp.descriptors.reserve(total_desc_count);
|
||||
} else if (desc_count_status != ESP_GATT_OK) {
|
||||
ESP_LOGW(TAG, "[%d] [%s] Error getting descriptor count for char handle %d, status=%d",
|
||||
connection->get_connection_index(), connection->address_str().c_str(), char_result.char_handle,
|
||||
desc_count_status);
|
||||
}
|
||||
|
||||
// Now process descriptors
|
||||
uint16_t desc_offset = 0;
|
||||
esp_gattc_descr_elem_t desc_result;
|
||||
while (true) { // descriptors
|
||||
uint16_t desc_count = 1;
|
||||
esp_gatt_status_t desc_status =
|
||||
esp_ble_gattc_get_all_descr(connection->get_gattc_if(), connection->get_conn_id(),
|
||||
char_result.char_handle, &desc_result, &desc_count, desc_offset);
|
||||
if (desc_status == ESP_GATT_INVALID_OFFSET || desc_status == ESP_GATT_NOT_FOUND) {
|
||||
break;
|
||||
}
|
||||
if (desc_status != ESP_GATT_OK) {
|
||||
ESP_LOGE(TAG, "[%d] [%s] esp_ble_gattc_get_all_descr error, status=%d", connection->get_connection_index(),
|
||||
connection->address_str().c_str(), desc_status);
|
||||
break;
|
||||
}
|
||||
if (desc_count == 0) {
|
||||
break;
|
||||
}
|
||||
api::BluetoothGATTDescriptor descriptor_resp;
|
||||
descriptor_resp.uuid = get_128bit_uuid_vec(desc_result.uuid);
|
||||
descriptor_resp.handle = desc_result.handle;
|
||||
characteristic_resp.descriptors.push_back(std::move(descriptor_resp));
|
||||
desc_offset++;
|
||||
}
|
||||
service_resp.characteristics.push_back(std::move(characteristic_resp));
|
||||
}
|
||||
resp.services.push_back(std::move(service_resp));
|
||||
this->api_connection_->send_message(resp, api::BluetoothGATTGetServicesResponse::MESSAGE_TYPE);
|
||||
}
|
||||
this->last_advertisement_flush_time_ = now;
|
||||
}
|
||||
}
|
||||
|
||||
@ -647,7 +501,6 @@ void BluetoothProxy::bluetooth_scanner_set_mode(bool active) {
|
||||
|
||||
BluetoothProxy *global_bluetooth_proxy = nullptr; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
|
||||
|
||||
} // namespace bluetooth_proxy
|
||||
} // namespace esphome
|
||||
} // namespace esphome::bluetooth_proxy
|
||||
|
||||
#endif // USE_ESP32
|
||||
|
@ -18,10 +18,10 @@
|
||||
#include <esp_bt.h>
|
||||
#include <esp_bt_device.h>
|
||||
|
||||
namespace esphome {
|
||||
namespace bluetooth_proxy {
|
||||
namespace esphome::bluetooth_proxy {
|
||||
|
||||
static const esp_err_t ESP_GATT_NOT_CONNECTED = -1;
|
||||
static const int DONE_SENDING_SERVICES = -2;
|
||||
|
||||
using namespace esp32_ble_client;
|
||||
|
||||
@ -131,9 +131,6 @@ class BluetoothProxy : public esp32_ble_tracker::ESPBTDeviceListener, public Com
|
||||
}
|
||||
|
||||
protected:
|
||||
#ifdef USE_ESP32_BLE_DEVICE
|
||||
void send_api_packet_(const esp32_ble_tracker::ESPBTDevice &device);
|
||||
#endif
|
||||
void send_bluetooth_scanner_state_(esp32_ble_tracker::ScannerState state);
|
||||
|
||||
BluetoothConnection *get_connection_(uint64_t address, bool reserve);
|
||||
@ -145,14 +142,21 @@ class BluetoothProxy : public esp32_ble_tracker::ESPBTDeviceListener, public Com
|
||||
// Group 2: Container types (typically 12 bytes on 32-bit)
|
||||
std::vector<BluetoothConnection *> connections_{};
|
||||
|
||||
// Group 3: 1-byte types grouped together
|
||||
// BLE advertisement batching
|
||||
std::vector<api::BluetoothLERawAdvertisement> advertisement_pool_;
|
||||
std::unique_ptr<api::BluetoothLERawAdvertisementsResponse> response_;
|
||||
|
||||
// Group 3: 4-byte types
|
||||
uint32_t last_advertisement_flush_time_{0};
|
||||
|
||||
// Group 4: 1-byte types grouped together
|
||||
bool active_;
|
||||
// 1 byte used, 3 bytes padding
|
||||
uint8_t advertisement_count_{0};
|
||||
// 2 bytes used, 2 bytes padding
|
||||
};
|
||||
|
||||
extern BluetoothProxy *global_bluetooth_proxy; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
|
||||
|
||||
} // namespace bluetooth_proxy
|
||||
} // namespace esphome
|
||||
} // namespace esphome::bluetooth_proxy
|
||||
|
||||
#endif // USE_ESP32
|
||||
|
@ -88,7 +88,6 @@ const char *oversampling_to_str(BME280Oversampling oversampling) { // NOLINT
|
||||
}
|
||||
|
||||
void BME280Component::setup() {
|
||||
ESP_LOGCONFIG(TAG, "Running setup");
|
||||
uint8_t chip_id = 0;
|
||||
|
||||
// Mark as not failed before initializing. Some devices will turn off sensors to save on batteries
|
||||
|
@ -71,7 +71,6 @@ static const char *iir_filter_to_str(BME680IIRFilter filter) {
|
||||
}
|
||||
|
||||
void BME680Component::setup() {
|
||||
ESP_LOGCONFIG(TAG, "Running setup");
|
||||
uint8_t chip_id;
|
||||
if (!this->read_byte(BME680_REGISTER_CHIPID, &chip_id) || chip_id != 0x61) {
|
||||
this->mark_failed();
|
||||
|
@ -1,7 +1,7 @@
|
||||
import esphome.codegen as cg
|
||||
from esphome.components import esp32, i2c
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import CONF_ID, CONF_SAMPLE_RATE, CONF_TEMPERATURE_OFFSET
|
||||
from esphome.const import CONF_ID, CONF_SAMPLE_RATE, CONF_TEMPERATURE_OFFSET, Framework
|
||||
|
||||
CODEOWNERS = ["@trvrnrth"]
|
||||
DEPENDENCIES = ["i2c"]
|
||||
@ -56,7 +56,15 @@ CONFIG_SCHEMA = cv.All(
|
||||
): cv.positive_time_period_minutes,
|
||||
}
|
||||
).extend(i2c.i2c_device_schema(0x76)),
|
||||
cv.only_with_arduino,
|
||||
cv.only_with_framework(
|
||||
frameworks=Framework.ARDUINO,
|
||||
suggestions={
|
||||
Framework.ESP_IDF: (
|
||||
"bme68x_bsec2_i2c",
|
||||
"sensor/bme68x_bsec2",
|
||||
)
|
||||
},
|
||||
),
|
||||
cv.Any(
|
||||
cv.only_on_esp8266,
|
||||
cv.All(
|
||||
|
@ -15,8 +15,6 @@ std::vector<BME680BSECComponent *>
|
||||
uint8_t BME680BSECComponent::work_buffer_[BSEC_MAX_WORKBUFFER_SIZE] = {0};
|
||||
|
||||
void BME680BSECComponent::setup() {
|
||||
ESP_LOGCONFIG(TAG, "Running setup for '%s'", this->device_id_.c_str());
|
||||
|
||||
uint8_t new_idx = BME680BSECComponent::instances.size();
|
||||
BME680BSECComponent::instances.push_back(this);
|
||||
|
||||
|
@ -21,8 +21,6 @@ static const char *const TAG = "bme68x_bsec2.sensor";
|
||||
static const std::string IAQ_ACCURACY_STATES[4] = {"Stabilizing", "Uncertain", "Calibrating", "Calibrated"};
|
||||
|
||||
void BME68xBSEC2Component::setup() {
|
||||
ESP_LOGCONFIG(TAG, "Running setup");
|
||||
|
||||
this->bsec_status_ = bsec_init_m(&this->bsec_instance_);
|
||||
if (this->bsec_status_ != BSEC_OK) {
|
||||
this->mark_failed();
|
||||
|
@ -119,7 +119,6 @@ const float GRAVITY_EARTH = 9.80665f;
|
||||
void BMI160Component::internal_setup_(int stage) {
|
||||
switch (stage) {
|
||||
case 0:
|
||||
ESP_LOGCONFIG(TAG, "Running setup");
|
||||
uint8_t chipid;
|
||||
if (!this->read_byte(BMI160_REGISTER_CHIPID, &chipid) || (chipid != 0b11010001)) {
|
||||
this->mark_failed();
|
||||
|
@ -20,7 +20,6 @@ void BMP085Component::update() {
|
||||
this->set_timeout("temperature", 5, [this]() { this->read_temperature_(); });
|
||||
}
|
||||
void BMP085Component::setup() {
|
||||
ESP_LOGCONFIG(TAG, "Running setup");
|
||||
uint8_t data[22];
|
||||
if (!this->read_bytes(BMP085_REGISTER_AC1_H, data, 22)) {
|
||||
this->mark_failed();
|
||||
|
@ -57,7 +57,6 @@ static const char *iir_filter_to_str(BMP280IIRFilter filter) {
|
||||
}
|
||||
|
||||
void BMP280Component::setup() {
|
||||
ESP_LOGCONFIG(TAG, "Running setup");
|
||||
uint8_t chip_id = 0;
|
||||
|
||||
// Read the chip id twice, to work around a bug where the first read is 0.
|
||||
|
@ -70,7 +70,6 @@ static const LogString *iir_filter_to_str(IIRFilter filter) {
|
||||
|
||||
void BMP3XXComponent::setup() {
|
||||
this->error_code_ = NONE;
|
||||
ESP_LOGCONFIG(TAG, "Running setup");
|
||||
// Call the Device base class "initialise" function
|
||||
if (!reset()) {
|
||||
ESP_LOGE(TAG, "Failed to reset");
|
||||
|
@ -128,8 +128,6 @@ void BMP581Component::setup() {
|
||||
*/
|
||||
|
||||
this->error_code_ = NONE;
|
||||
ESP_LOGCONFIG(TAG, "Running setup");
|
||||
|
||||
////////////////////
|
||||
// 1) Soft reboot //
|
||||
////////////////////
|
||||
|
@ -15,7 +15,6 @@ static const uint8_t BP1658CJ_ADDR_START_5CH = 0x30;
|
||||
static const uint8_t BP1658CJ_DELAY = 2;
|
||||
|
||||
void BP1658CJ::setup() {
|
||||
ESP_LOGCONFIG(TAG, "Running setup");
|
||||
this->data_pin_->setup();
|
||||
this->data_pin_->digital_write(false);
|
||||
this->clock_pin_->setup();
|
||||
|
@ -20,7 +20,6 @@ static const uint8_t BP5758D_ALL_DATA_CHANNEL_ENABLEMENT = 0b00011111;
|
||||
static const uint8_t BP5758D_DELAY = 2;
|
||||
|
||||
void BP5758D::setup() {
|
||||
ESP_LOGCONFIG(TAG, "Running setup");
|
||||
this->data_pin_->setup();
|
||||
this->data_pin_->digital_write(false);
|
||||
delayMicroseconds(BP5758D_DELAY);
|
||||
|
@ -22,9 +22,8 @@ def validate_id(config):
|
||||
if CONF_CAN_ID in config:
|
||||
can_id = config[CONF_CAN_ID]
|
||||
id_ext = config[CONF_USE_EXTENDED_ID]
|
||||
if not id_ext:
|
||||
if can_id > 0x7FF:
|
||||
raise cv.Invalid("Standard IDs must be 11 Bit (0x000-0x7ff / 0-2047)")
|
||||
if not id_ext and can_id > 0x7FF:
|
||||
raise cv.Invalid("Standard IDs must be 11 Bit (0x000-0x7ff / 0-2047)")
|
||||
return config
|
||||
|
||||
|
||||
|
@ -7,7 +7,6 @@ namespace canbus {
|
||||
static const char *const TAG = "canbus";
|
||||
|
||||
void Canbus::setup() {
|
||||
ESP_LOGCONFIG(TAG, "Running setup");
|
||||
if (!this->setup_internal()) {
|
||||
ESP_LOGE(TAG, "setup error!");
|
||||
this->mark_failed();
|
||||
|
@ -8,8 +8,6 @@ namespace cap1188 {
|
||||
static const char *const TAG = "cap1188";
|
||||
|
||||
void CAP1188Component::setup() {
|
||||
ESP_LOGCONFIG(TAG, "Running setup");
|
||||
|
||||
// Reset device using the reset pin
|
||||
if (this->reset_pin_ != nullptr) {
|
||||
this->reset_pin_->setup();
|
||||
|
@ -10,8 +10,6 @@ static const char *const TAG = "cd74hc4067";
|
||||
float CD74HC4067Component::get_setup_priority() const { return setup_priority::DATA; }
|
||||
|
||||
void CD74HC4067Component::setup() {
|
||||
ESP_LOGCONFIG(TAG, "Running setup");
|
||||
|
||||
this->pin_s0_->setup();
|
||||
this->pin_s1_->setup();
|
||||
this->pin_s2_->setup();
|
||||
|
@ -14,7 +14,6 @@ static const uint8_t CH422G_REG_OUT_UPPER = 0x23; // write reg for output bit
|
||||
static const char *const TAG = "ch422g";
|
||||
|
||||
void CH422GComponent::setup() {
|
||||
ESP_LOGCONFIG(TAG, "Running setup");
|
||||
// set outputs before mode
|
||||
this->write_outputs_();
|
||||
// Set mode and check for errors
|
||||
|
@ -4,7 +4,6 @@ namespace esphome {
|
||||
namespace chsc6x {
|
||||
|
||||
void CHSC6XTouchscreen::setup() {
|
||||
ESP_LOGCONFIG(TAG, "Running setup");
|
||||
if (this->interrupt_pin_ != nullptr) {
|
||||
this->interrupt_pin_->setup();
|
||||
this->attach_interrupt_(this->interrupt_pin_, gpio::INTERRUPT_FALLING_EDGE);
|
||||
@ -15,8 +14,6 @@ void CHSC6XTouchscreen::setup() {
|
||||
if (this->y_raw_max_ == this->y_raw_min_) {
|
||||
this->y_raw_max_ = this->display_->get_native_height();
|
||||
}
|
||||
|
||||
ESP_LOGCONFIG(TAG, "CHSC6X Touchscreen setup complete");
|
||||
}
|
||||
|
||||
void CHSC6XTouchscreen::update_touches() {
|
||||
|
@ -20,7 +20,6 @@ uint8_t cm1106_checksum(const uint8_t *response, size_t len) {
|
||||
}
|
||||
|
||||
void CM1106Component::setup() {
|
||||
ESP_LOGCONFIG(TAG, "Running setup");
|
||||
uint8_t response[8] = {0};
|
||||
if (!this->cm1106_write_command_(C_M1106_CMD_GET_CO2, sizeof(C_M1106_CMD_GET_CO2), response, sizeof(response))) {
|
||||
ESP_LOGE(TAG, ESP_LOG_MSG_COMM_FAIL);
|
||||
|
@ -3,7 +3,12 @@
|
||||
CODEOWNERS = ["@esphome/core"]
|
||||
|
||||
CONF_BYTE_ORDER = "byte_order"
|
||||
BYTE_ORDER_LITTLE = "little_endian"
|
||||
BYTE_ORDER_BIG = "big_endian"
|
||||
|
||||
CONF_COLOR_DEPTH = "color_depth"
|
||||
CONF_DRAW_ROUNDING = "draw_rounding"
|
||||
CONF_ON_RECEIVE = "on_receive"
|
||||
CONF_ON_STATE_CHANGE = "on_state_change"
|
||||
CONF_REQUEST_HEADERS = "request_headers"
|
||||
CONF_USE_PSRAM = "use_psram"
|
||||
|
@ -52,8 +52,6 @@ bool CS5460AComponent::softreset_() {
|
||||
}
|
||||
|
||||
void CS5460AComponent::setup() {
|
||||
ESP_LOGCONFIG(TAG, "Running setup");
|
||||
|
||||
float current_full_scale = (pga_gain_ == CS5460A_PGA_GAIN_10X) ? 0.25 : 0.10;
|
||||
float voltage_full_scale = 0.25;
|
||||
current_multiplier_ = current_full_scale / (fabsf(current_gain_) * 0x1000000);
|
||||
|
@ -42,7 +42,6 @@ static const uint8_t CSE7761_CMD_ENABLE_WRITE = 0xE5; // Enable write operation
|
||||
enum CSE7761 { RMS_IAC, RMS_IBC, RMS_UC, POWER_PAC, POWER_PBC, POWER_SC, ENERGY_AC, ENERGY_BC };
|
||||
|
||||
void CSE7761Component::setup() {
|
||||
ESP_LOGCONFIG(TAG, "Running setup");
|
||||
this->write_(CSE7761_SPECIAL_COMMAND, CSE7761_CMD_RESET);
|
||||
uint16_t syscon = this->read_(0x00, 2); // Default 0x0A04
|
||||
if ((0x0A04 == syscon) && this->chip_init_()) {
|
||||
|
@ -6,7 +6,6 @@ namespace cst226 {
|
||||
static const char *const TAG = "cst226.touchscreen";
|
||||
|
||||
void CST226Touchscreen::setup() {
|
||||
ESP_LOGCONFIG(TAG, "Running setup");
|
||||
if (this->reset_pin_ != nullptr) {
|
||||
this->reset_pin_->setup();
|
||||
this->reset_pin_->digital_write(true);
|
||||
@ -95,7 +94,6 @@ void CST226Touchscreen::continue_setup_() {
|
||||
}
|
||||
}
|
||||
this->setup_complete_ = true;
|
||||
ESP_LOGCONFIG(TAG, "CST226 Touchscreen setup complete");
|
||||
}
|
||||
void CST226Touchscreen::update_button_state_(bool state) {
|
||||
if (this->button_touched_ == state)
|
||||
|
@ -35,11 +35,9 @@ void CST816Touchscreen::continue_setup_() {
|
||||
if (this->y_raw_max_ == this->y_raw_min_) {
|
||||
this->y_raw_max_ = this->display_->get_native_height();
|
||||
}
|
||||
ESP_LOGCONFIG(TAG, "CST816 Touchscreen setup complete");
|
||||
}
|
||||
|
||||
void CST816Touchscreen::setup() {
|
||||
ESP_LOGCONFIG(TAG, "Running setup");
|
||||
if (this->reset_pin_ != nullptr) {
|
||||
this->reset_pin_->setup();
|
||||
this->reset_pin_->digital_write(true);
|
||||
|
@ -20,8 +20,6 @@ static const uint8_t DAC7678_REG_INTERNAL_REF_0 = 0x80;
|
||||
static const uint8_t DAC7678_REG_INTERNAL_REF_1 = 0x90;
|
||||
|
||||
void DAC7678Output::setup() {
|
||||
ESP_LOGCONFIG(TAG, "Running setup");
|
||||
|
||||
ESP_LOGV(TAG, "Resetting device");
|
||||
|
||||
// Reset device
|
||||
|
@ -70,7 +70,6 @@ bool DallasTemperatureSensor::read_scratch_pad_() {
|
||||
}
|
||||
|
||||
void DallasTemperatureSensor::setup() {
|
||||
ESP_LOGCONFIG(TAG, "Running setup");
|
||||
if (!this->check_address_())
|
||||
return;
|
||||
if (!this->read_scratch_pad_())
|
||||
|
@ -12,7 +12,6 @@ static const uint32_t TEARDOWN_TIMEOUT_DEEP_SLEEP_MS = 5000;
|
||||
bool global_has_deep_sleep = false; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
|
||||
|
||||
void DeepSleepComponent::setup() {
|
||||
ESP_LOGCONFIG(TAG, "Running setup");
|
||||
global_has_deep_sleep = true;
|
||||
|
||||
const optional<uint32_t> run_duration = get_run_duration_();
|
||||
|
@ -74,8 +74,7 @@ def range_segment_list(input):
|
||||
if isinstance(input, list):
|
||||
for list_item in input:
|
||||
if isinstance(list_item, list):
|
||||
for item in list_item:
|
||||
flat_list.append(item)
|
||||
flat_list.extend(list_item)
|
||||
else:
|
||||
flat_list.append(list_item)
|
||||
else:
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user