Merge branch 'dev' into remove_posix_tz_parser

This commit is contained in:
J. Nick Koston
2026-03-10 18:45:54 -10:00
committed by GitHub
880 changed files with 25818 additions and 7092 deletions
+1
View File
@@ -286,6 +286,7 @@ This document provides essential context for AI models interacting with this pro
* **Documentation Contributions:** * **Documentation Contributions:**
* Documentation is hosted in the separate `esphome/esphome-docs` repository. * Documentation is hosted in the separate `esphome/esphome-docs` repository.
* The contribution workflow is the same as for the codebase. * The contribution workflow is the same as for the codebase.
* When editing a component's documentation page, also update the corresponding component index page to ensure both pages remain in sync.
* **Best Practices:** * **Best Practices:**
* **Component Development:** Keep dependencies minimal, provide clear error messages, and write comprehensive docstrings and tests. * **Component Development:** Keep dependencies minimal, provide clear error messages, and write comprehensive docstrings and tests.
+1 -1
View File
@@ -1 +1 @@
b97e16a84153b2a4cfc51137cd6121db3c32374504b2bea55144413b3e573052 e4b9c4b54e705d3c9400e1cdda8ba0b32634780cfa5f32271832e911bdcafe7e
+1 -1
View File
@@ -25,7 +25,7 @@
- [ ] ESP32 - [ ] ESP32
- [ ] ESP32 IDF - [ ] ESP32 IDF
- [ ] ESP8266 - [ ] ESP8266
- [ ] RP2040 - [ ] RP2040/RP2350
- [ ] BK72xx - [ ] BK72xx
- [ ] RTL87xx - [ ] RTL87xx
- [ ] LN882x - [ ] LN882x
+2 -2
View File
@@ -47,7 +47,7 @@ runs:
- name: Build and push to ghcr by digest - name: Build and push to ghcr by digest
id: build-ghcr id: build-ghcr
uses: docker/build-push-action@10e90e3645eae34f1e60eeb005ba3a3d33f178e8 # v6.19.2 uses: docker/build-push-action@d08e5c354a6adb9ed34480a06d141179aa583294 # v7.0.0
env: env:
DOCKER_BUILD_SUMMARY: false DOCKER_BUILD_SUMMARY: false
DOCKER_BUILD_RECORD_UPLOAD: false DOCKER_BUILD_RECORD_UPLOAD: false
@@ -73,7 +73,7 @@ runs:
- name: Build and push to dockerhub by digest - name: Build and push to dockerhub by digest
id: build-dockerhub id: build-dockerhub
uses: docker/build-push-action@10e90e3645eae34f1e60eeb005ba3a3d33f178e8 # v6.19.2 uses: docker/build-push-action@d08e5c354a6adb9ed34480a06d141179aa583294 # v7.0.0
env: env:
DOCKER_BUILD_SUMMARY: false DOCKER_BUILD_SUMMARY: false
DOCKER_BUILD_RECORD_UPLOAD: false DOCKER_BUILD_RECORD_UPLOAD: false
@@ -14,6 +14,7 @@ module.exports = {
'chained-pr', 'chained-pr',
'core', 'core',
'small-pr', 'small-pr',
'medium-pr',
'dashboard', 'dashboard',
'github-actions', 'github-actions',
'by-code-owner', 'by-code-owner',
+12 -42
View File
@@ -7,6 +7,7 @@ const {
hasDashboardChanges, hasDashboardChanges,
hasGitHubActionsChanges, hasGitHubActionsChanges,
} = require('../detect-tags'); } = require('../detect-tags');
const { loadCodeowners, getEffectiveOwners } = require('../codeowners');
// Strategy: Merge branch detection // Strategy: Merge branch detection
async function detectMergeBranch(context) { async function detectMergeBranch(context) {
@@ -102,7 +103,7 @@ async function detectCoreChanges(changedFiles) {
} }
// Strategy: PR size detection // Strategy: PR size detection
async function detectPRSize(prFiles, totalAdditions, totalDeletions, totalChanges, isMegaPR, SMALL_PR_THRESHOLD, TOO_BIG_THRESHOLD) { async function detectPRSize(prFiles, totalAdditions, totalDeletions, totalChanges, isMegaPR, SMALL_PR_THRESHOLD, MEDIUM_PR_THRESHOLD, TOO_BIG_THRESHOLD) {
const labels = new Set(); const labels = new Set();
if (totalChanges <= SMALL_PR_THRESHOLD) { if (totalChanges <= SMALL_PR_THRESHOLD) {
@@ -110,6 +111,11 @@ async function detectPRSize(prFiles, totalAdditions, totalDeletions, totalChange
return labels; return labels;
} }
if (totalChanges <= MEDIUM_PR_THRESHOLD) {
labels.add('medium-pr');
return labels;
}
const testAdditions = prFiles const testAdditions = prFiles
.filter(file => file.filename.startsWith('tests/')) .filter(file => file.filename.startsWith('tests/'))
.reduce((sum, file) => sum + (file.additions || 0), 0); .reduce((sum, file) => sum + (file.additions || 0), 0);
@@ -148,51 +154,15 @@ async function detectGitHubActionsChanges(changedFiles) {
// Strategy: Code owner detection // Strategy: Code owner detection
async function detectCodeOwner(github, context, changedFiles) { async function detectCodeOwner(github, context, changedFiles) {
const labels = new Set(); const labels = new Set();
const { owner, repo } = context.repo;
try { try {
const { data: codeownersFile } = await github.rest.repos.getContent({ const codeownersPatterns = loadCodeowners();
owner,
repo,
path: 'CODEOWNERS',
});
const codeownersContent = Buffer.from(codeownersFile.content, 'base64').toString('utf8');
const prAuthor = context.payload.pull_request.user.login; const prAuthor = context.payload.pull_request.user.login;
const codeownersLines = codeownersContent.split('\n') // Check if PR author is a codeowner of any changed file
.map(line => line.trim()) const effective = getEffectiveOwners(changedFiles, codeownersPatterns);
.filter(line => line && !line.startsWith('#')); if (effective.users.has(prAuthor)) {
labels.add('by-code-owner');
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) { } catch (error) {
console.log('Failed to read or parse CODEOWNERS file:', error.message); console.log('Failed to read or parse CODEOWNERS file:', error.message);
+2 -1
View File
@@ -35,6 +35,7 @@ async function fetchApiData() {
module.exports = async ({ github, context }) => { module.exports = async ({ github, context }) => {
// Environment variables // Environment variables
const SMALL_PR_THRESHOLD = parseInt(process.env.SMALL_PR_THRESHOLD); const SMALL_PR_THRESHOLD = parseInt(process.env.SMALL_PR_THRESHOLD);
const MEDIUM_PR_THRESHOLD = parseInt(process.env.MEDIUM_PR_THRESHOLD);
const MAX_LABELS = parseInt(process.env.MAX_LABELS); const MAX_LABELS = parseInt(process.env.MAX_LABELS);
const TOO_BIG_THRESHOLD = parseInt(process.env.TOO_BIG_THRESHOLD); const TOO_BIG_THRESHOLD = parseInt(process.env.TOO_BIG_THRESHOLD);
const COMPONENT_LABEL_THRESHOLD = parseInt(process.env.COMPONENT_LABEL_THRESHOLD); const COMPONENT_LABEL_THRESHOLD = parseInt(process.env.COMPONENT_LABEL_THRESHOLD);
@@ -120,7 +121,7 @@ module.exports = async ({ github, context }) => {
detectNewComponents(prFiles), detectNewComponents(prFiles),
detectNewPlatforms(prFiles, apiData), detectNewPlatforms(prFiles, apiData),
detectCoreChanges(changedFiles), detectCoreChanges(changedFiles),
detectPRSize(prFiles, totalAdditions, totalDeletions, totalChanges, isMegaPR, SMALL_PR_THRESHOLD, TOO_BIG_THRESHOLD), detectPRSize(prFiles, totalAdditions, totalDeletions, totalChanges, isMegaPR, SMALL_PR_THRESHOLD, MEDIUM_PR_THRESHOLD, TOO_BIG_THRESHOLD),
detectDashboardChanges(changedFiles), detectDashboardChanges(changedFiles),
detectGitHubActionsChanges(changedFiles), detectGitHubActionsChanges(changedFiles),
detectCodeOwner(github, context, changedFiles), detectCodeOwner(github, context, changedFiles),
+227
View File
@@ -0,0 +1,227 @@
// Shared CODEOWNERS parsing and matching utilities.
//
// Used by:
// - codeowner-review-request.yml
// - codeowner-approved-label-update.yml
// - auto-label-pr/detectors.js (detectCodeOwner)
/**
* Convert a CODEOWNERS glob pattern to a RegExp.
*
* Handles **, *, and ? wildcards after escaping regex-special characters.
*/
function globToRegex(pattern) {
let regexStr = pattern
.replace(/([.+^=!:${}()|[\]\\])/g, '\\$1')
.replace(/\*\*/g, '\x00GLOBSTAR\x00') // protect ** from next replace
.replace(/\*/g, '[^/]*') // single star
.replace(/\x00GLOBSTAR\x00/g, '.*') // restore globstar
.replace(/\?/g, '.');
return new RegExp('^' + regexStr + '$');
}
/**
* Parse raw CODEOWNERS file content into an array of
* { pattern, regex, owners } objects.
*
* Each `owners` entry is the raw string from the file (e.g. "@user" or
* "@esphome/core").
*/
function parseCodeowners(content) {
const lines = content
.split('\n')
.map(line => line.trim())
.filter(line => line && !line.startsWith('#'));
const patterns = [];
for (const line of lines) {
const parts = line.split(/\s+/);
if (parts.length < 2) continue;
const pattern = parts[0];
const owners = parts.slice(1);
const regex = globToRegex(pattern);
patterns.push({ pattern, regex, owners });
}
return patterns;
}
/**
* Fetch and parse the CODEOWNERS file via the GitHub API.
*
* @param {object} github - octokit instance from actions/github-script
* @param {string} owner - repo owner
* @param {string} repo - repo name
* @param {string} [ref] - git ref (SHA / branch) to read from
* @returns {Array<{pattern: string, regex: RegExp, owners: string[]}>}
*/
async function fetchCodeowners(github, owner, repo, ref) {
const params = { owner, repo, path: 'CODEOWNERS' };
if (ref) params.ref = ref;
const { data: file } = await github.rest.repos.getContent(params);
const content = Buffer.from(file.content, 'base64').toString('utf8');
return parseCodeowners(content);
}
/**
* Classify raw owner strings into individual users and teams.
*
* @param {string[]} rawOwners - e.g. ["@user1", "@esphome/core"]
* @returns {{ users: string[], teams: string[] }}
* users – login names without "@"
* teams – team slugs without the "org/" prefix
*/
function classifyOwners(rawOwners) {
const users = [];
const teams = [];
for (const o of rawOwners) {
const clean = o.startsWith('@') ? o.slice(1) : o;
if (clean.includes('/')) {
teams.push(clean.split('/')[1]);
} else {
users.push(clean);
}
}
return { users, teams };
}
/**
* For each file, find its effective codeowners using GitHub's
* "last match wins" semantics, then union across all files.
*
* @param {string[]} files - list of file paths
* @param {Array} codeownersPatterns - from parseCodeowners / fetchCodeowners
* @returns {{ users: Set<string>, teams: Set<string>, matchedFileCount: number }}
*/
function getEffectiveOwners(files, codeownersPatterns) {
const users = new Set();
const teams = new Set();
let matchedFileCount = 0;
for (const file of files) {
// Last matching pattern wins for each file
let effectiveOwners = null;
for (const { regex, owners } of codeownersPatterns) {
if (regex.test(file)) {
effectiveOwners = owners;
}
}
if (effectiveOwners) {
matchedFileCount++;
const classified = classifyOwners(effectiveOwners);
for (const u of classified.users) users.add(u);
for (const t of classified.teams) teams.add(t);
}
}
return { users, teams, matchedFileCount };
}
/**
* Read and parse the CODEOWNERS file from disk.
*
* Use this when the repo is already checked out (avoids an API call).
*
* @param {string} [repoRoot='.'] - path to the repo root
* @returns {Array<{pattern: string, regex: RegExp, owners: string[]}>}
*/
function loadCodeowners(repoRoot = '.') {
const fs = require('fs');
const path = require('path');
const content = fs.readFileSync(path.join(repoRoot, 'CODEOWNERS'), 'utf8');
return parseCodeowners(content);
}
/** Possible label actions returned by determineLabelAction. */
const LabelAction = Object.freeze({
ADD: 'add',
REMOVE: 'remove',
NONE: 'none',
});
/**
* Determine what label action is needed for a PR based on codeowner approvals.
*
* Checks changed files against CODEOWNERS patterns, reviews, and current labels
* to decide if the label should be added, removed, or left unchanged.
*
* @param {object} github - octokit instance from actions/github-script
* @param {string} owner - repo owner
* @param {string} repo - repo name
* @param {number} pr_number - pull request number
* @param {Array} codeownersPatterns - from loadCodeowners / fetchCodeowners
* @param {string} labelName - label to manage
* @returns {Promise<LabelAction>}
*/
async function determineLabelAction(github, owner, repo, pr_number, codeownersPatterns, labelName) {
// Get the list of changed files in this PR
const prFiles = await github.paginate(
github.rest.pulls.listFiles,
{ owner, repo, pull_number: pr_number }
);
const changedFiles = prFiles.map(file => file.filename);
console.log(`Found ${changedFiles.length} changed files`);
if (changedFiles.length === 0) {
console.log('No changed files found');
return LabelAction.NONE;
}
// Get effective owners using last-match-wins semantics
const effective = getEffectiveOwners(changedFiles, codeownersPatterns);
const componentCodeowners = effective.users;
console.log(`Component-specific codeowners: ${Array.from(componentCodeowners).join(', ') || '(none)'}`);
// Get current labels
const { data: currentLabels } = await github.rest.issues.listLabelsOnIssue({
owner, repo, issue_number: pr_number
});
const hasLabel = currentLabels.some(label => label.name === labelName);
if (componentCodeowners.size === 0) {
console.log('No component-specific codeowners found');
return hasLabel ? LabelAction.REMOVE : LabelAction.NONE;
}
// Get all reviews and find latest per user
const reviews = await github.paginate(
github.rest.pulls.listReviews,
{ owner, repo, pull_number: pr_number }
);
const latestReviewByUser = new Map();
for (const review of reviews) {
if (!review.user || review.user.type === 'Bot' || review.state === 'COMMENTED') continue;
latestReviewByUser.set(review.user.login, review);
}
// Check if any component-specific codeowner has an active approval
let hasCodeownerApproval = false;
for (const [login, review] of latestReviewByUser) {
if (review.state === 'APPROVED' && componentCodeowners.has(login)) {
console.log(`Codeowner '${login}' has approved`);
hasCodeownerApproval = true;
break;
}
}
if (hasCodeownerApproval && !hasLabel) return LabelAction.ADD;
if (!hasCodeownerApproval && hasLabel) return LabelAction.REMOVE;
console.log(`Label already ${hasLabel ? 'present' : 'absent'}, no change needed`);
return LabelAction.NONE;
}
module.exports = {
globToRegex,
parseCodeowners,
fetchCodeowners,
loadCodeowners,
classifyOwners,
getEffectiveOwners,
LabelAction,
determineLabelAction
};
+1
View File
@@ -12,6 +12,7 @@ permissions:
env: env:
SMALL_PR_THRESHOLD: 30 SMALL_PR_THRESHOLD: 30
MEDIUM_PR_THRESHOLD: 100
MAX_LABELS: 15 MAX_LABELS: 15
TOO_BIG_THRESHOLD: 1000 TOO_BIG_THRESHOLD: 1000
COMPONENT_LABEL_THRESHOLD: 10 COMPONENT_LABEL_THRESHOLD: 10
+1 -1
View File
@@ -49,7 +49,7 @@ jobs:
with: with:
python-version: "3.11" python-version: "3.11"
- name: Set up Docker Buildx - name: Set up Docker Buildx
uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3.12.0 uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4.0.0
- name: Set TAG - name: Set TAG
run: | run: |
@@ -0,0 +1,78 @@
# Adds/removes a 'code-owner-approved' label when a component-specific
# codeowner approves (or dismisses) a PR.
#
# Uses pull_request_target so that fork PRs do not require workflow approval.
# The label is reconciled on every PR update; for review events specifically,
# this means the label is applied on the next push after a codeowner review.
name: Codeowner Approved Label
on:
pull_request_target:
types: [opened, synchronize, reopened, ready_for_review]
permissions:
issues: write
pull-requests: read
contents: read
jobs:
codeowner-approved:
name: Run
if: ${{ github.repository == 'esphome/esphome' }}
runs-on: ubuntu-latest
steps:
- name: Checkout base branch
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
ref: ${{ github.event.pull_request.base.sha }}
sparse-checkout: |
.github/scripts/codeowners.js
CODEOWNERS
- name: Check codeowner approval and update label
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
env:
PR_NUMBER: ${{ github.event.pull_request.number }}
with:
script: |
const { loadCodeowners, determineLabelAction, LabelAction } = require('./.github/scripts/codeowners.js');
const owner = context.repo.owner;
const repo = context.repo.repo;
const pr_number = parseInt(process.env.PR_NUMBER, 10);
const LABEL_NAME = 'code-owner-approved';
console.log(`Processing PR #${pr_number} for codeowner approval label`);
const codeownersPatterns = loadCodeowners();
const action = await determineLabelAction(
github, owner, repo, pr_number, codeownersPatterns, LABEL_NAME
);
if (action === LabelAction.NONE) {
console.log('No label change needed');
return;
}
try {
if (action === LabelAction.ADD) {
await github.rest.issues.addLabels({
owner, repo, issue_number: pr_number, labels: [LABEL_NAME]
});
console.log(`Added '${LABEL_NAME}' label`);
} else if (action === LabelAction.REMOVE) {
await github.rest.issues.removeLabel({
owner, repo, issue_number: pr_number, name: LABEL_NAME
});
console.log(`Removed '${LABEL_NAME}' label`);
}
} catch (error) {
if (error.status === 403) {
console.log(`Warning: insufficient permissions to update label (expected for fork PRs)`);
} else if (error.status === 404) {
console.log(`Label '${LABEL_NAME}' not present, nothing to remove`);
} else {
throw error;
}
}
+35 -83
View File
@@ -24,10 +24,17 @@ jobs:
if: ${{ github.repository == 'esphome/esphome' && !github.event.pull_request.draft }} if: ${{ github.repository == 'esphome/esphome' && !github.event.pull_request.draft }}
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Checkout base branch
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
ref: ${{ github.event.pull_request.base.sha }}
- name: Request reviews from component codeowners - name: Request reviews from component codeowners
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
with: with:
script: | script: |
const { loadCodeowners, getEffectiveOwners } = require('./.github/scripts/codeowners.js');
const owner = context.repo.owner; const owner = context.repo.owner;
const repo = context.repo.repo; const repo = context.repo.repo;
const pr_number = context.payload.pull_request.number; const pr_number = context.payload.pull_request.number;
@@ -38,12 +45,15 @@ jobs:
const BOT_COMMENT_MARKER = '<!-- codeowner-review-request-bot -->'; const BOT_COMMENT_MARKER = '<!-- codeowner-review-request-bot -->';
try { try {
// Get the list of changed files in this PR // Get the list of changed files in this PR (with pagination)
const { data: files } = await github.rest.pulls.listFiles({ const files = await github.paginate(
owner, github.rest.pulls.listFiles,
repo, {
pull_number: pr_number owner,
}); repo,
pull_number: pr_number
}
);
const changedFiles = files.map(file => file.filename); const changedFiles = files.map(file => file.filename);
console.log(`Found ${changedFiles.length} changed files`); console.log(`Found ${changedFiles.length} changed files`);
@@ -53,32 +63,10 @@ jobs:
return; return;
} }
// Fetch CODEOWNERS file from root // Parse CODEOWNERS from the checked-out base branch
const { data: codeownersFile } = await github.rest.repos.getContent({ const codeownersPatterns = loadCodeowners();
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 console.log(`Parsed ${codeownersPatterns.length} codeowner patterns`);
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 // Helper function to create comment body
function createCommentBody(reviewersList, teamsList, matchedFileCount, isSuccessful = true) { function createCommentBody(reviewersList, teamsList, matchedFileCount, isSuccessful = true) {
@@ -93,50 +81,11 @@ jobs:
} }
} }
for (const line of codeownersLines) { // Match changed files against CODEOWNERS patterns using last-match-wins semantics
const parts = line.split(/\s+/); const effective = getEffectiveOwners(changedFiles, codeownersPatterns);
if (parts.length < 2) continue; const matchedOwners = effective.users;
const matchedTeams = effective.teams;
const pattern = parts[0]; const matchedFileCount = effective.matchedFileCount;
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) { if (matchedOwners.size === 0 && matchedTeams.size === 0) {
console.log('No codeowners found for any changed files'); console.log('No codeowners found for any changed files');
@@ -170,11 +119,14 @@ jobs:
} }
// Check for completed reviews to avoid re-requesting users who have already reviewed // Check for completed reviews to avoid re-requesting users who have already reviewed
const { data: reviews } = await github.rest.pulls.listReviews({ const reviews = await github.paginate(
owner, github.rest.pulls.listReviews,
repo, {
pull_number: pr_number owner,
}); repo,
pull_number: pr_number
}
);
const reviewedUsers = new Set(); const reviewedUsers = new Set();
reviews.forEach(review => { reviews.forEach(review => {
@@ -247,7 +199,7 @@ jobs:
} }
const totalReviewers = reviewersList.length + teamsList.length; const totalReviewers = reviewersList.length + teamsList.length;
console.log(`Requesting reviews from ${reviewersList.length} users and ${teamsList.length} teams for ${fileMatches.size} matched files`); console.log(`Requesting reviews from ${reviewersList.length} users and ${teamsList.length} teams for ${matchedFileCount} matched files`);
// Request reviews // Request reviews
try { try {
@@ -279,7 +231,7 @@ jobs:
// Only add a comment if there are new codeowners to mention (not previously pinged) // Only add a comment if there are new codeowners to mention (not previously pinged)
if (reviewersList.length > 0 || teamsList.length > 0) { if (reviewersList.length > 0 || teamsList.length > 0) {
const commentBody = createCommentBody(reviewersList, teamsList, fileMatches.size, true); const commentBody = createCommentBody(reviewersList, teamsList, matchedFileCount, true);
await github.rest.issues.createComment({ await github.rest.issues.createComment({
owner, owner,
@@ -297,7 +249,7 @@ jobs:
// Only try to add a comment if there are new codeowners to mention // Only try to add a comment if there are new codeowners to mention
if (reviewersList.length > 0 || teamsList.length > 0) { if (reviewersList.length > 0 || teamsList.length > 0) {
const commentBody = createCommentBody(reviewersList, teamsList, fileMatches.size, false); const commentBody = createCommentBody(reviewersList, teamsList, matchedFileCount, false);
try { try {
await github.rest.issues.createComment({ await github.rest.issues.createComment({
+2 -2
View File
@@ -58,7 +58,7 @@ jobs:
# Initializes the CodeQL tools for scanning. # Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL - name: Initialize CodeQL
uses: github/codeql-action/init@89a39a4e59826350b863aa6b6252a07ad50cf83e # v4.32.4 uses: github/codeql-action/init@0d579ffd059c29b07949a3cce3983f0780820c98 # v4.32.6
with: with:
languages: ${{ matrix.language }} languages: ${{ matrix.language }}
build-mode: ${{ matrix.build-mode }} build-mode: ${{ matrix.build-mode }}
@@ -86,6 +86,6 @@ jobs:
exit 1 exit 1
- name: Perform CodeQL Analysis - name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@89a39a4e59826350b863aa6b6252a07ad50cf83e # v4.32.4 uses: github/codeql-action/analyze@0d579ffd059c29b07949a3cce3983f0780820c98 # v4.32.6
with: with:
category: "/language:${{matrix.language}}" category: "/language:${{matrix.language}}"
+7 -2
View File
@@ -26,14 +26,19 @@ jobs:
} = require('./.github/scripts/detect-tags.js'); } = require('./.github/scripts/detect-tags.js');
const title = context.payload.pull_request.title; const title = context.payload.pull_request.title;
const author = context.payload.pull_request.user.login;
// Skip bot PRs (e.g. dependabot) - they have their own title format
if (author === 'dependabot[bot]') {
return;
}
// Block titles starting with "word:" or "word(scope):" patterns // Block titles starting with "word:" or "word(scope):" patterns
const commitStylePattern = /^\w+(\(.*?\))?[!]?\s*:/; const commitStylePattern = /^\w+(\(.*?\))?[!]?\s*:/;
if (commitStylePattern.test(title)) { if (commitStylePattern.test(title)) {
core.setFailed( core.setFailed(
`PR title should not start with a "prefix:" style format.\n` + `PR title should not start with a "prefix:" style format.\n` +
`Please use the format: [component] Brief description\n` + `Please use the format: [component] Brief description\n`
`Example: [pn532] Add health checking and auto-reset`
); );
return; return;
} }
+6 -6
View File
@@ -99,15 +99,15 @@ jobs:
python-version: "3.11" python-version: "3.11"
- name: Set up Docker Buildx - name: Set up Docker Buildx
uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3.12.0 uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4.0.0
- name: Log in to docker hub - name: Log in to docker hub
uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3.7.0 uses: docker/login-action@b45d80f862d83dbcd57f89517bcf500b2ab88fb2 # v4.0.0
with: with:
username: ${{ secrets.DOCKER_USER }} username: ${{ secrets.DOCKER_USER }}
password: ${{ secrets.DOCKER_PASSWORD }} password: ${{ secrets.DOCKER_PASSWORD }}
- name: Log in to the GitHub container registry - name: Log in to the GitHub container registry
uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3.7.0 uses: docker/login-action@b45d80f862d83dbcd57f89517bcf500b2ab88fb2 # v4.0.0
with: with:
registry: ghcr.io registry: ghcr.io
username: ${{ github.actor }} username: ${{ github.actor }}
@@ -178,17 +178,17 @@ jobs:
merge-multiple: true merge-multiple: true
- name: Set up Docker Buildx - name: Set up Docker Buildx
uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3.12.0 uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4.0.0
- name: Log in to docker hub - name: Log in to docker hub
if: matrix.registry == 'dockerhub' if: matrix.registry == 'dockerhub'
uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3.7.0 uses: docker/login-action@b45d80f862d83dbcd57f89517bcf500b2ab88fb2 # v4.0.0
with: with:
username: ${{ secrets.DOCKER_USER }} username: ${{ secrets.DOCKER_USER }}
password: ${{ secrets.DOCKER_PASSWORD }} password: ${{ secrets.DOCKER_PASSWORD }}
- name: Log in to the GitHub container registry - name: Log in to the GitHub container registry
if: matrix.registry == 'ghcr' if: matrix.registry == 'ghcr'
uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3.7.0 uses: docker/login-action@b45d80f862d83dbcd57f89517bcf500b2ab88fb2 # v4.0.0
with: with:
registry: ghcr.io registry: ghcr.io
username: ${{ github.actor }} username: ${{ github.actor }}
+1 -1
View File
@@ -11,7 +11,7 @@ ci:
repos: repos:
- repo: https://github.com/astral-sh/ruff-pre-commit - repo: https://github.com/astral-sh/ruff-pre-commit
# Ruff version. # Ruff version.
rev: v0.15.3 rev: v0.15.5
hooks: hooks:
# Run the linter. # Run the linter.
- id: ruff - id: ruff
+9 -1
View File
@@ -54,6 +54,8 @@ esphome/components/atm90e32/* @circuitsetup @descipher
esphome/components/audio/* @kahrendt esphome/components/audio/* @kahrendt
esphome/components/audio_adc/* @kbx81 esphome/components/audio_adc/* @kbx81
esphome/components/audio_dac/* @kbx81 esphome/components/audio_dac/* @kbx81
esphome/components/audio_file/* @kahrendt
esphome/components/audio_file/media_source/* @kahrendt
esphome/components/axs15231/* @clydebarrow esphome/components/axs15231/* @clydebarrow
esphome/components/b_parasite/* @rbaron esphome/components/b_parasite/* @rbaron
esphome/components/ballu/* @bazuchan esphome/components/ballu/* @bazuchan
@@ -130,6 +132,7 @@ esphome/components/dashboard_import/* @esphome/core
esphome/components/datetime/* @jesserockz @rfdarter esphome/components/datetime/* @jesserockz @rfdarter
esphome/components/debug/* @esphome/core esphome/components/debug/* @esphome/core
esphome/components/delonghi/* @grob6000 esphome/components/delonghi/* @grob6000
esphome/components/dew_point/* @CFlix
esphome/components/dfplayer/* @glmnet esphome/components/dfplayer/* @glmnet
esphome/components/dfrobot_sen0395/* @niklasweber esphome/components/dfrobot_sen0395/* @niklasweber
esphome/components/dht/* @OttoWinter esphome/components/dht/* @OttoWinter
@@ -316,6 +319,7 @@ esphome/components/mcp9808/* @k7hpn
esphome/components/md5/* @esphome/core esphome/components/md5/* @esphome/core
esphome/components/mdns/* @esphome/core esphome/components/mdns/* @esphome/core
esphome/components/media_player/* @jesserockz esphome/components/media_player/* @jesserockz
esphome/components/media_source/* @kahrendt
esphome/components/micro_wake_word/* @jesserockz @kahrendt esphome/components/micro_wake_word/* @jesserockz @kahrendt
esphome/components/micronova/* @edenhaus @jorre05 esphome/components/micronova/* @edenhaus @jorre05
esphome/components/microphone/* @jesserockz @kahrendt esphome/components/microphone/* @jesserockz @kahrendt
@@ -407,11 +411,12 @@ esphome/components/restart/* @esphome/core
esphome/components/rf_bridge/* @jesserockz esphome/components/rf_bridge/* @jesserockz
esphome/components/rgbct/* @jesserockz esphome/components/rgbct/* @jesserockz
esphome/components/rp2040/* @jesserockz esphome/components/rp2040/* @jesserockz
esphome/components/rp2040_ble/* @bdraco
esphome/components/rp2040_pio_led_strip/* @Papa-DMan esphome/components/rp2040_pio_led_strip/* @Papa-DMan
esphome/components/rp2040_pwm/* @jesserockz esphome/components/rp2040_pwm/* @jesserockz
esphome/components/rpi_dpi_rgb/* @clydebarrow esphome/components/rpi_dpi_rgb/* @clydebarrow
esphome/components/rtl87xx/* @kuba2k2 esphome/components/rtl87xx/* @kuba2k2
esphome/components/rtttl/* @glmnet esphome/components/rtttl/* @glmnet @ximex
esphome/components/runtime_image/* @clydebarrow @guillempages @kahrendt esphome/components/runtime_image/* @clydebarrow @guillempages @kahrendt
esphome/components/runtime_stats/* @bdraco esphome/components/runtime_stats/* @bdraco
esphome/components/rx8130/* @beormund esphome/components/rx8130/* @beormund
@@ -432,6 +437,7 @@ esphome/components/sen5x/* @martgras
esphome/components/sen6x/* @martgras @mebner86 @mikelawrence @tuct esphome/components/sen6x/* @martgras @mebner86 @mikelawrence @tuct
esphome/components/sensirion_common/* @martgras esphome/components/sensirion_common/* @martgras
esphome/components/sensor/* @esphome/core esphome/components/sensor/* @esphome/core
esphome/components/serial_proxy/* @kbx81
esphome/components/sfa30/* @ghsensdev esphome/components/sfa30/* @ghsensdev
esphome/components/sgp40/* @SenexCrenshaw esphome/components/sgp40/* @SenexCrenshaw
esphome/components/sgp4x/* @martgras @SenexCrenshaw esphome/components/sgp4x/* @martgras @SenexCrenshaw
@@ -454,6 +460,7 @@ esphome/components/sonoff_d1/* @anatoly-savchenkov
esphome/components/sound_level/* @kahrendt esphome/components/sound_level/* @kahrendt
esphome/components/speaker/* @jesserockz @kahrendt esphome/components/speaker/* @jesserockz @kahrendt
esphome/components/speaker/media_player/* @kahrendt @synesthesiam esphome/components/speaker/media_player/* @kahrendt @synesthesiam
esphome/components/speaker_source/* @kahrendt
esphome/components/spi/* @clydebarrow @esphome/core esphome/components/spi/* @clydebarrow @esphome/core
esphome/components/spi_device/* @clydebarrow esphome/components/spi_device/* @clydebarrow
esphome/components/spi_led_strip/* @clydebarrow esphome/components/spi_led_strip/* @clydebarrow
@@ -584,6 +591,7 @@ esphome/components/xl9535/* @mreditor97
esphome/components/xpt2046/touchscreen/* @nielsnl68 @numo68 esphome/components/xpt2046/touchscreen/* @nielsnl68 @numo68
esphome/components/xxtea/* @clydebarrow esphome/components/xxtea/* @clydebarrow
esphome/components/zephyr/* @tomaszduda23 esphome/components/zephyr/* @tomaszduda23
esphome/components/zephyr_mcumgr/ota/* @tomaszduda23
esphome/components/zhlt01/* @cfeenstra1024 esphome/components/zhlt01/* @cfeenstra1024
esphome/components/zigbee/* @tomaszduda23 esphome/components/zigbee/* @tomaszduda23
esphome/components/zio_ultrasonic/* @kahrendt esphome/components/zio_ultrasonic/* @kahrendt
+271 -7
View File
@@ -1,6 +1,7 @@
# PYTHON_ARGCOMPLETE_OK # PYTHON_ARGCOMPLETE_OK
import argparse import argparse
from collections.abc import Callable from collections.abc import Callable
from contextlib import suppress
from datetime import datetime from datetime import datetime
import functools import functools
import getpass import getpass
@@ -9,6 +10,8 @@ import logging
import os import os
from pathlib import Path from pathlib import Path
import re import re
import shutil
import subprocess
import sys import sys
import time import time
from typing import Protocol from typing import Protocol
@@ -23,6 +26,7 @@ import esphome.codegen as cg
from esphome.config import iter_component_configs, read_config, strip_default_ids from esphome.config import iter_component_configs, read_config, strip_default_ids
from esphome.const import ( from esphome.const import (
ALLOWED_NAME_CHARS, ALLOWED_NAME_CHARS,
ARGUMENT_HELP_DEVICE,
CONF_API, CONF_API,
CONF_BAUD_RATE, CONF_BAUD_RATE,
CONF_BROKER, CONF_BROKER,
@@ -43,7 +47,9 @@ from esphome.const import (
CONF_SUBSTITUTIONS, CONF_SUBSTITUTIONS,
CONF_TOPIC, CONF_TOPIC,
ENV_NOGITIGNORE, ENV_NOGITIGNORE,
KEY_CORE,
KEY_NATIVE_IDF, KEY_NATIVE_IDF,
KEY_TARGET_PLATFORM,
PLATFORM_ESP32, PLATFORM_ESP32,
PLATFORM_ESP8266, PLATFORM_ESP8266,
PLATFORM_RP2040, PLATFORM_RP2040,
@@ -55,7 +61,11 @@ from esphome.helpers import get_bool_env, indent, is_ip_address
from esphome.log import AnsiFore, color, setup_log from esphome.log import AnsiFore, color, setup_log
from esphome.types import ConfigType from esphome.types import ConfigType
from esphome.util import ( from esphome.util import (
PICOTOOL_PACKAGE,
detect_rp2040_bootsel,
get_picotool_path,
get_serial_ports, get_serial_ports,
is_picotool_usb_permission_error,
list_yaml_files, list_yaml_files,
run_external_command, run_external_command,
run_external_process, run_external_process,
@@ -67,6 +77,21 @@ _LOGGER = logging.getLogger(__name__)
# Maximum buffer size for serial log reading to prevent unbounded memory growth # Maximum buffer size for serial log reading to prevent unbounded memory growth
SERIAL_BUFFER_MAX_SIZE = 65536 SERIAL_BUFFER_MAX_SIZE = 65536
_RP2040_BOOTSEL_INSTRUCTIONS = (
"To enter BOOTSEL mode:\n"
" 1. Unplug the device\n"
" 2. Hold the BOOT/BOOTSEL button\n"
" 3. Plug in the USB cable while holding the button\n"
" 4. Release the button - the device should appear as a USB drive (RPI-RP2)\n"
"Then run the upload command again."
)
_RP2040_UDEV_HINT = (
"You may need to add a udev rule for RP2040 devices. "
"See: https://github.com/raspberrypi/picotool"
"/blob/master/udev/60-picotool.rules"
)
# Special non-component keys that appear in configs # Special non-component keys that appear in configs
_NON_COMPONENT_KEYS = frozenset( _NON_COMPONENT_KEYS = frozenset(
{ {
@@ -162,6 +187,7 @@ class PortType(StrEnum):
NETWORK = "NETWORK" NETWORK = "NETWORK"
MQTT = "MQTT" MQTT = "MQTT"
MQTTIP = "MQTTIP" MQTTIP = "MQTTIP"
BOOTSEL = "BOOTSEL"
# Magic MQTT port types that require special handling # Magic MQTT port types that require special handling
@@ -240,6 +266,19 @@ def choose_upload_log_host(
(f"{port.path} ({port.description})", port.path) for port in get_serial_ports() (f"{port.path} ({port.description})", port.path) for port in get_serial_ports()
] ]
# Add RP2040 BOOTSEL device option when uploading
bootsel_permission_error = False
if (
purpose == Purpose.UPLOADING
and CORE.data.get(KEY_CORE, {}).get(KEY_TARGET_PLATFORM) == PLATFORM_RP2040
and (picotool := _find_picotool()) is not None
):
bootsel = detect_rp2040_bootsel(picotool)
if bootsel.device_count > 0:
options.append(("RP2040 BOOTSEL (via picotool)", "BOOTSEL"))
elif bootsel.permission_error:
bootsel_permission_error = True
if purpose == Purpose.LOGGING: if purpose == Purpose.LOGGING:
if has_mqtt_logging(): if has_mqtt_logging():
mqtt_config = CORE.config[CONF_MQTT] mqtt_config = CORE.config[CONF_MQTT]
@@ -257,6 +296,25 @@ def choose_upload_log_host(
if has_mqtt_ip_lookup(): if has_mqtt_ip_lookup():
options.append(("Over The Air (MQTT IP lookup)", "MQTTIP")) options.append(("Over The Air (MQTT IP lookup)", "MQTTIP"))
# Show helpful BOOTSEL instructions for RP2040 when no BOOTSEL device is found
if (
purpose == Purpose.UPLOADING
and CORE.data.get(KEY_CORE, {}).get(KEY_TARGET_PLATFORM) == PLATFORM_RP2040
and not any(get_port_type(opt[1]) == PortType.BOOTSEL for opt in options)
):
if bootsel_permission_error:
_LOGGER.warning(
"An RP2040 device in BOOTSEL mode was detected but could "
"not be accessed due to USB permissions."
)
if sys.platform.startswith("linux"):
_LOGGER.warning(_RP2040_UDEV_HINT)
if not options:
raise EsphomeError(
f"No RP2040 device found. {_RP2040_BOOTSEL_INSTRUCTIONS}"
)
_LOGGER.info("Tip: %s", _RP2040_BOOTSEL_INSTRUCTIONS)
if check_default is not None and check_default in [opt[1] for opt in options]: if check_default is not None and check_default in [opt[1] for opt in options]:
return [check_default] return [check_default]
return [choose_prompt(options, purpose=purpose)] return [choose_prompt(options, purpose=purpose)]
@@ -403,10 +461,13 @@ def get_port_type(port: str) -> PortType:
Returns: Returns:
PortType.SERIAL for serial ports (/dev/ttyUSB0, COM1, etc.) PortType.SERIAL for serial ports (/dev/ttyUSB0, COM1, etc.)
PortType.BOOTSEL for RP2040 BOOTSEL upload via picotool
PortType.MQTT for MQTT logging PortType.MQTT for MQTT logging
PortType.MQTTIP for MQTT IP lookup PortType.MQTTIP for MQTT IP lookup
PortType.NETWORK for IP addresses, hostnames, or mDNS names PortType.NETWORK for IP addresses, hostnames, or mDNS names
""" """
if port == "BOOTSEL":
return PortType.BOOTSEL
if port.startswith("/") or port.startswith("COM"): if port.startswith("/") or port.startswith("COM"):
return PortType.SERIAL return PortType.SERIAL
if port == "MQTT": if port == "MQTT":
@@ -627,6 +688,47 @@ def _check_and_emit_build_info() -> None:
) )
def _get_configured_xtal_freq() -> int | None:
"""Read the configured crystal frequency from the sdkconfig file."""
sdkconfig_path = CORE.relative_build_path(f"sdkconfig.{CORE.name}")
if not sdkconfig_path.is_file():
return None
with suppress(OSError, ValueError):
content = sdkconfig_path.read_text()
for line in content.splitlines():
if line.startswith("CONFIG_XTAL_FREQ="):
return int(line.split("=", 1)[1])
return None
def _make_crystal_freq_callback(
configured_freq: int,
) -> Callable[[str], str | None]:
"""Create a callback that checks esptool crystal frequency output."""
crystal_re = re.compile(r"Crystal frequency:\s+(\d+)\s*MHz")
def check_crystal_line(line: str) -> str | None:
if not (match := crystal_re.search(line)):
return None
detected = int(match.group(1))
if detected == configured_freq:
return None
return (
f"\n\033[33mWARNING: Crystal frequency mismatch! "
f"Device reports {detected}MHz but firmware is configured "
f"for {configured_freq}MHz.\n"
f"UART logging and other clock-dependent features will not "
f"work correctly.\n"
f"Set the correct crystal frequency with sdkconfig_options:\n"
f" esp32:\n"
f" framework:\n"
f" sdkconfig_options:\n"
f" CONFIG_XTAL_FREQ_{detected}: 'y'\033[0m\n\n"
)
return check_crystal_line
def upload_using_esptool( def upload_using_esptool(
config: ConfigType, port: str, file: str, speed: int config: ConfigType, port: str, file: str, speed: int
) -> str | int: ) -> str | int:
@@ -655,6 +757,14 @@ def upload_using_esptool(
mcu = get_esp32_variant().lower() mcu = get_esp32_variant().lower()
line_callbacks: list[Callable[[str], str | None]] = []
if (
CORE.is_esp32
and file is None
and (configured_freq := _get_configured_xtal_freq()) is not None
):
line_callbacks.append(_make_crystal_freq_callback(configured_freq))
def run_esptool(baud_rate): def run_esptool(baud_rate):
cmd = [ cmd = [
"esptool", "esptool",
@@ -679,9 +789,13 @@ def upload_using_esptool(
if os.environ.get("ESPHOME_USE_SUBPROCESS") is None: if os.environ.get("ESPHOME_USE_SUBPROCESS") is None:
import esptool import esptool
return run_external_command(esptool.main, *cmd) # pylint: disable=no-member return run_external_command(
esptool.main, # pylint: disable=no-member
*cmd,
line_callbacks=line_callbacks,
)
return run_external_process(*cmd) return run_external_process(*cmd, line_callbacks=line_callbacks)
rc = run_esptool(first_baudrate) rc = run_esptool(first_baudrate)
if rc == 0 or first_baudrate == 115200: if rc == 0 or first_baudrate == 115200:
@@ -694,15 +808,140 @@ def upload_using_esptool(
return run_esptool(115200) return run_esptool(115200)
def upload_using_platformio(config: ConfigType, port: str): def upload_using_platformio(config: ConfigType, port: str) -> int:
from esphome import platformio_api from esphome import platformio_api
# RP2040 platform-raspberrypi build recipe expects firmware.bin.signed for
# the upload target, but 'nobuild' skips the build phase that creates it.
# Create it here so the upload doesn't fail.
if CORE.data.get(KEY_CORE, {}).get(KEY_TARGET_PLATFORM) == PLATFORM_RP2040:
idedata = platformio_api.get_idedata(config)
build_dir = Path(idedata.firmware_elf_path).parent
firmware_bin = build_dir / "firmware.bin"
signed_bin = build_dir / "firmware.bin.signed"
if firmware_bin.is_file() and not signed_bin.is_file():
shutil.copy2(firmware_bin, signed_bin)
upload_args = ["-t", "upload", "-t", "nobuild"] upload_args = ["-t", "upload", "-t", "nobuild"]
if port is not None: if port is not None:
upload_args += ["--upload-port", port] upload_args += ["--upload-port", port]
return platformio_api.run_platformio_cli_run(config, CORE.verbose, *upload_args) return platformio_api.run_platformio_cli_run(config, CORE.verbose, *upload_args)
def _find_picotool() -> Path | None:
"""Find the picotool binary from PlatformIO packages."""
from esphome import platformio_api
try:
idedata = platformio_api.get_idedata(CORE.config)
except Exception: # noqa: BLE001 # pylint: disable=broad-except
return None
return get_picotool_path(idedata.cc_path)
def upload_using_picotool(config: ConfigType) -> int:
"""Upload firmware to RP2040 in BOOTSEL mode using picotool.
Uses picotool to load the ELF firmware directly via USB, avoiding
the mass storage copy approach that causes "disk not ejected properly"
warnings on macOS.
"""
from esphome import platformio_api
idedata = platformio_api.get_idedata(config)
firmware_elf = Path(idedata.firmware_elf_path)
if not firmware_elf.is_file():
_LOGGER.error(
"Firmware ELF file not found at %s. "
"Make sure the project has been compiled first.",
firmware_elf,
)
return 1
picotool = get_picotool_path(idedata.cc_path)
if picotool is None:
_LOGGER.error(
"picotool not found. Ensure the RP2040 PlatformIO platform "
"is installed (%s).",
PICOTOOL_PACKAGE,
)
return 1
_LOGGER.info("Uploading firmware to RP2040 via picotool...")
try:
# Don't capture stdout — let picotool write directly to the terminal
# so progress bars display in real-time with \r updates.
# Capture stderr only so we can detect permission errors.
result = subprocess.run(
[str(picotool), "load", "-v", "-x", str(firmware_elf)],
stderr=subprocess.PIPE,
timeout=60,
check=False,
)
except subprocess.TimeoutExpired:
_LOGGER.error("picotool upload timed out after 60 seconds.")
return 1
except OSError as err:
_LOGGER.error("Failed to run picotool: %s", err)
return 1
if result.returncode != 0:
stderr = result.stderr.decode("utf-8", errors="replace").strip()
if stderr:
for line in stderr.splitlines():
safe_print(line)
if is_picotool_usb_permission_error(stderr):
msg = "Permission denied accessing USB device."
if sys.platform.startswith("linux"):
msg += f" {_RP2040_UDEV_HINT}"
_LOGGER.error(msg)
else:
_LOGGER.error("picotool upload failed (exit code %d).", result.returncode)
return 1
return 0
def _wait_for_serial_port(
port: str | None = None,
timeout: float = 30.0,
known_ports: set[str] | None = None,
) -> None:
"""Wait for a serial port to appear, e.g. after a device reboot.
USB-CDC devices disappear briefly after flashing while the device
reboots and re-enumerates on the USB bus.
If port is given, wait for that specific path. If known_ports is
given, wait for a new port that wasn't in the set. Otherwise wait
for any serial port to appear.
"""
def _port_found() -> bool:
if port is not None:
if os.name == "posix":
return os.path.exists(port)
return any(p.path == port for p in get_serial_ports())
ports = get_serial_ports()
if known_ports is not None:
return any(p.path not in known_ports for p in ports)
return bool(ports)
if _port_found():
return
if port is not None:
_LOGGER.info("Waiting for %s to come online...", port)
else:
_LOGGER.info("Waiting for device to reboot...")
start = time.monotonic()
while time.monotonic() - start < timeout:
time.sleep(0.05)
if _port_found():
time.sleep(0.05)
return
def check_permissions(port: str): def check_permissions(port: str):
if os.name == "posix" and get_port_type(port) == PortType.SERIAL: if os.name == "posix" and get_port_type(port) == PortType.SERIAL:
# Check if we can open selected serial port # Check if we can open selected serial port
@@ -732,7 +971,15 @@ def upload_program(
except AttributeError: except AttributeError:
pass pass
if get_port_type(host) == PortType.SERIAL: port_type = get_port_type(host)
if port_type == PortType.BOOTSEL:
exit_code = upload_using_picotool(config)
# Return None for device - BOOTSEL can't be used for logging,
# so command_run will show the interactive chooser for log source
return exit_code, None
if port_type == PortType.SERIAL:
check_permissions(host) check_permissions(host)
exit_code = 1 exit_code = 1
@@ -786,6 +1033,7 @@ def show_logs(config: ConfigType, args: ArgsProtocol, devices: list[str]) -> int
port_type = get_port_type(port) port_type = get_port_type(port)
if port_type == PortType.SERIAL: if port_type == PortType.SERIAL:
_wait_for_serial_port(port)
check_permissions(port) check_permissions(port)
return run_miniterm(config, port, args) return run_miniterm(config, port, args)
@@ -924,6 +1172,9 @@ def command_run(args: ArgsProtocol, config: ConfigType) -> int | None:
purpose=Purpose.UPLOADING, purpose=Purpose.UPLOADING,
) )
# Snapshot current serial ports before upload so we can detect new ones
pre_upload_ports = {p.path for p in get_serial_ports()}
exit_code, successful_device = upload_program(config, args, devices) exit_code, successful_device = upload_program(config, args, devices)
if exit_code == 0: if exit_code == 0:
_LOGGER.info("Successfully uploaded program.") _LOGGER.info("Successfully uploaded program.")
@@ -934,6 +1185,19 @@ def command_run(args: ArgsProtocol, config: ConfigType) -> int | None:
if args.no_logs: if args.no_logs:
return 0 return 0
# After BOOTSEL upload, wait for a new serial port to appear
# so it shows up in the log chooser
if (
successful_device is None
and CORE.data.get(KEY_CORE, {}).get(KEY_TARGET_PLATFORM) == PLATFORM_RP2040
):
_wait_for_serial_port(known_ports=pre_upload_ports)
# If exactly one new serial port appeared, use it directly
serial_ports = get_serial_ports()
new_ports = [p for p in serial_ports if p.path not in pre_upload_ports]
if len(new_ports) == 1:
successful_device = new_ports[0].path
# For logs, prefer the device we successfully uploaded to # For logs, prefer the device we successfully uploaded to
devices = choose_upload_log_host( devices = choose_upload_log_host(
default=successful_device, default=successful_device,
@@ -1367,7 +1631,7 @@ def parse_args(argv):
parser_upload.add_argument( parser_upload.add_argument(
"--device", "--device",
action="append", action="append",
help="Manually specify the serial port/address to use, for example /dev/ttyUSB0. Can be specified multiple times for fallback addresses.", help=ARGUMENT_HELP_DEVICE,
) )
parser_upload.add_argument( parser_upload.add_argument(
"--upload_speed", "--upload_speed",
@@ -1390,7 +1654,7 @@ def parse_args(argv):
parser_logs.add_argument( parser_logs.add_argument(
"--device", "--device",
action="append", action="append",
help="Manually specify the serial port/address to use, for example /dev/ttyUSB0. Can be specified multiple times for fallback addresses.", help=ARGUMENT_HELP_DEVICE,
) )
parser_logs.add_argument( parser_logs.add_argument(
"--reset", "--reset",
@@ -1420,7 +1684,7 @@ def parse_args(argv):
parser_run.add_argument( parser_run.add_argument(
"--device", "--device",
action="append", action="append",
help="Manually specify the serial port/address to use, for example /dev/ttyUSB0. Can be specified multiple times for fallback addresses.", help=ARGUMENT_HELP_DEVICE,
) )
parser_run.add_argument( parser_run.add_argument(
"--upload_speed", "--upload_speed",
+30 -8
View File
@@ -1,3 +1,5 @@
import logging
import esphome.codegen as cg import esphome.codegen as cg
import esphome.config_validation as cv import esphome.config_validation as cv
from esphome.const import ( from esphome.const import (
@@ -57,22 +59,41 @@ def maybe_conf(conf, *validators):
return validate return validate
_LOGGER = logging.getLogger(__name__)
def register_action( def register_action(
name: str, name: str,
action_type: MockObjClass, action_type: MockObjClass,
schema: cv.Schema, schema: cv.Schema,
*, *,
synchronous: bool = False, synchronous: bool | None = None,
): ):
"""Register an action type. """Register an action type.
Actions default to ``synchronous=False`` (safe default), meaning string All callers must pass ``synchronous`` explicitly.
arguments use owning std::string to prevent dangling references.
Set ``synchronous=True`` only for actions that complete synchronously ``synchronous=True`` — the action never defers ``play_next_()`` to a
and never store trigger arguments for later execution. This allows later point (callback, timer, or ``loop()``). Trigger arguments are
the code generator to use non-owning StringRef for zero-copy access. only used during the initial call, so string args can use non-owning
StringRef for zero-copy access.
``synchronous=False`` — the action defers ``play_next_()`` via a
callback, timer, or ``Component::loop()``. Trigger arguments must
outlive the initial call, so string args use owning std::string to
prevent dangling references.
""" """
if synchronous is None:
_LOGGER.warning(
"register_action('%s', ...) is missing the synchronous= parameter. "
"Defaulting to synchronous=False (safe but prevents StringRef "
"optimization). Check the C++ class: use synchronous=False if "
"play_next_() is deferred to a callback, timer, or loop(); "
"use synchronous=True if play_next_() always runs before the "
"initial play/play_complex call returns",
name,
)
synchronous = False
return ACTION_REGISTRY.register(name, action_type, schema, synchronous=synchronous) return ACTION_REGISTRY.register(name, action_type, schema, synchronous=synchronous)
@@ -353,6 +374,7 @@ async def component_is_idle_condition_to_code(
"delay", "delay",
DelayAction, DelayAction,
cv.templatable(cv.positive_time_period_milliseconds), cv.templatable(cv.positive_time_period_milliseconds),
synchronous=False,
) )
async def delay_action_to_code( async def delay_action_to_code(
config: ConfigType, config: ConfigType,
@@ -465,7 +487,7 @@ _validate_wait_until = cv.maybe_simple_value(
) )
@register_action("wait_until", WaitUntilAction, _validate_wait_until) @register_action("wait_until", WaitUntilAction, _validate_wait_until, synchronous=False)
async def wait_until_action_to_code( async def wait_until_action_to_code(
config: ConfigType, config: ConfigType,
action_id: ID, action_id: ID,
@@ -611,7 +633,7 @@ def has_non_synchronous_actions(actions: ConfigType) -> bool:
Non-synchronous actions (delay, wait_until, script.wait, etc.) store Non-synchronous actions (delay, wait_until, script.wait, etc.) store
trigger args for later execution, making non-owning types like StringRef trigger args for later execution, making non-owning types like StringRef
unsafe. Actions that haven't been audited default to non-synchronous. unsafe.
""" """
if isinstance(actions, list): if isinstance(actions, list):
return any(has_non_synchronous_actions(item) for item in actions) return any(has_non_synchronous_actions(item) for item in actions)
+3 -3
View File
@@ -72,7 +72,7 @@ def get_component_cmakelists(minimal: bool = False) -> str:
# Extract compile definitions from build flags (-DXXX -> XXX) # Extract compile definitions from build flags (-DXXX -> XXX)
compile_defs = [flag[2:] for flag in CORE.build_flags if flag.startswith("-D")] compile_defs = [flag[2:] for flag in CORE.build_flags if flag.startswith("-D")]
compile_defs_str = "\n ".join(compile_defs) if compile_defs else "" compile_defs_str = "\n ".join(sorted(compile_defs)) if compile_defs else ""
# Extract compile options (-W flags, excluding linker flags) # Extract compile options (-W flags, excluding linker flags)
compile_opts = [ compile_opts = [
@@ -80,11 +80,11 @@ def get_component_cmakelists(minimal: bool = False) -> str:
for flag in CORE.build_flags for flag in CORE.build_flags
if flag.startswith("-W") and not flag.startswith("-Wl,") if flag.startswith("-W") and not flag.startswith("-Wl,")
] ]
compile_opts_str = "\n ".join(compile_opts) if compile_opts else "" compile_opts_str = "\n ".join(sorted(compile_opts)) if compile_opts else ""
# Extract linker options (-Wl, flags) # Extract linker options (-Wl, flags)
link_opts = [flag for flag in CORE.build_flags if flag.startswith("-Wl,")] link_opts = [flag for flag in CORE.build_flags if flag.startswith("-Wl,")]
link_opts_str = "\n ".join(link_opts) if link_opts else "" link_opts_str = "\n ".join(sorted(link_opts)) if link_opts else ""
return f"""\ return f"""\
# Auto-generated by ESPHome # Auto-generated by ESPHome
+1
View File
@@ -11,6 +11,7 @@
from esphome.cpp_generator import ( # noqa: F401 from esphome.cpp_generator import ( # noqa: F401
ArrayInitializer, ArrayInitializer,
Expression, Expression,
FlashStringLiteral,
LineComment, LineComment,
LogStringLiteral, LogStringLiteral,
MockObj, MockObj,
@@ -58,7 +58,10 @@ void HOT AddressableLightDisplay::draw_absolute_pixel_internal(int x, int y, Col
if (this->pixel_mapper_f_.has_value()) { if (this->pixel_mapper_f_.has_value()) {
// Params are passed by reference, so they may be modified in call. // Params are passed by reference, so they may be modified in call.
this->addressable_light_buffer_[(*this->pixel_mapper_f_)(x, y)] = color; int index = (*this->pixel_mapper_f_)(x, y);
if (index < 0 || static_cast<size_t>(index) >= this->addressable_light_buffer_.size())
return;
this->addressable_light_buffer_[index] = color;
} else { } else {
this->addressable_light_buffer_[y * this->get_width_internal() + x] = color; this->addressable_light_buffer_[y * this->get_width_internal() + x] = color;
} }
+3 -3
View File
@@ -121,7 +121,7 @@ void ADE7880::update() {
this->update_sensor_from_s32_register16_(chan->forward_active_energy, AFWATTHR, [&chan](float val) { this->update_sensor_from_s32_register16_(chan->forward_active_energy, AFWATTHR, [&chan](float val) {
return chan->forward_active_energy_total += val / 14400.0f; return chan->forward_active_energy_total += val / 14400.0f;
}); });
this->update_sensor_from_s32_register16_(chan->reverse_active_energy, AFWATTHR, [&chan](float val) { this->update_sensor_from_s32_register16_(chan->reverse_active_energy, ARWATTHR, [&chan](float val) {
return chan->reverse_active_energy_total += val / 14400.0f; return chan->reverse_active_energy_total += val / 14400.0f;
}); });
} }
@@ -137,7 +137,7 @@ void ADE7880::update() {
this->update_sensor_from_s32_register16_(chan->forward_active_energy, BFWATTHR, [&chan](float val) { this->update_sensor_from_s32_register16_(chan->forward_active_energy, BFWATTHR, [&chan](float val) {
return chan->forward_active_energy_total += val / 14400.0f; return chan->forward_active_energy_total += val / 14400.0f;
}); });
this->update_sensor_from_s32_register16_(chan->reverse_active_energy, BFWATTHR, [&chan](float val) { this->update_sensor_from_s32_register16_(chan->reverse_active_energy, BRWATTHR, [&chan](float val) {
return chan->reverse_active_energy_total += val / 14400.0f; return chan->reverse_active_energy_total += val / 14400.0f;
}); });
} }
@@ -153,7 +153,7 @@ void ADE7880::update() {
this->update_sensor_from_s32_register16_(chan->forward_active_energy, CFWATTHR, [&chan](float val) { this->update_sensor_from_s32_register16_(chan->forward_active_energy, CFWATTHR, [&chan](float val) {
return chan->forward_active_energy_total += val / 14400.0f; return chan->forward_active_energy_total += val / 14400.0f;
}); });
this->update_sensor_from_s32_register16_(chan->reverse_active_energy, CFWATTHR, [&chan](float val) { this->update_sensor_from_s32_register16_(chan->reverse_active_energy, CRWATTHR, [&chan](float val) {
return chan->reverse_active_energy_total += val / 14400.0f; return chan->reverse_active_energy_total += val / 14400.0f;
}); });
} }
@@ -85,6 +85,9 @@ constexpr uint16_t CWATTHR = 0xE402;
constexpr uint16_t AFWATTHR = 0xE403; constexpr uint16_t AFWATTHR = 0xE403;
constexpr uint16_t BFWATTHR = 0xE404; constexpr uint16_t BFWATTHR = 0xE404;
constexpr uint16_t CFWATTHR = 0xE405; constexpr uint16_t CFWATTHR = 0xE405;
constexpr uint16_t ARWATTHR = 0xE406;
constexpr uint16_t BRWATTHR = 0xE407;
constexpr uint16_t CRWATTHR = 0xE408;
constexpr uint16_t AFVARHR = 0xE409; constexpr uint16_t AFVARHR = 0xE409;
constexpr uint16_t BFVARHR = 0xE40A; constexpr uint16_t BFVARHR = 0xE40A;
constexpr uint16_t CFVARHR = 0xE40B; constexpr uint16_t CFVARHR = 0xE40B;
+2 -13
View File
@@ -173,19 +173,8 @@ float ADS1115Component::request_measurement(ADS1115Multiplexer multiplexer, ADS1
} }
if (resolution == ADS1015_12_BITS) { if (resolution == ADS1015_12_BITS) {
bool negative = (raw_conversion >> 15) == 1; // ADS1015 returns 12-bit value left-justified in 16 bits; shift right and sign-extend
raw_conversion = static_cast<uint16_t>(static_cast<int16_t>(raw_conversion) >> (16 - ADS1015_12_BITS));
// shift raw_conversion as it's only 12-bits, left justified
raw_conversion = raw_conversion >> (16 - ADS1015_12_BITS);
// check if number was negative in order to keep the sign
if (negative) {
// the number was negative
// 1) set the negative bit back
raw_conversion |= 0x8000;
// 2) reset the former (shifted) negative bit
raw_conversion &= 0xF7FF;
}
} }
auto signed_conversion = static_cast<int16_t>(raw_conversion); auto signed_conversion = static_cast<int16_t>(raw_conversion);
+2
View File
@@ -92,6 +92,7 @@ AGS10_NEW_I2C_ADDRESS_SCHEMA = cv.maybe_simple_value(
"ags10.new_i2c_address", "ags10.new_i2c_address",
AGS10NewI2cAddressAction, AGS10NewI2cAddressAction,
AGS10_NEW_I2C_ADDRESS_SCHEMA, AGS10_NEW_I2C_ADDRESS_SCHEMA,
synchronous=True,
) )
async def ags10newi2caddress_to_code(config, action_id, template_arg, args): async def ags10newi2caddress_to_code(config, action_id, template_arg, args):
var = cg.new_Pvariable(action_id, template_arg) var = cg.new_Pvariable(action_id, template_arg)
@@ -121,6 +122,7 @@ AGS10_SET_ZERO_POINT_SCHEMA = cv.Schema(
"ags10.set_zero_point", "ags10.set_zero_point",
AGS10SetZeroPointAction, AGS10SetZeroPointAction,
AGS10_SET_ZERO_POINT_SCHEMA, AGS10_SET_ZERO_POINT_SCHEMA,
synchronous=True,
) )
async def ags10setzeropoint_to_code(config, action_id, template_arg, args): async def ags10setzeropoint_to_code(config, action_id, template_arg, args):
var = cg.new_Pvariable(action_id, template_arg) var = cg.new_Pvariable(action_id, template_arg)
+4 -1
View File
@@ -34,7 +34,10 @@ SET_AUTO_MUTE_ACTION_SCHEMA = cv.maybe_simple_value(
@automation.register_action( @automation.register_action(
"aic3204.set_auto_mute_mode", SetAutoMuteAction, SET_AUTO_MUTE_ACTION_SCHEMA "aic3204.set_auto_mute_mode",
SetAutoMuteAction,
SET_AUTO_MUTE_ACTION_SCHEMA,
synchronous=True,
) )
async def aic3204_set_volume_to_code(config, action_id, template_arg, args): async def aic3204_set_volume_to_code(config, action_id, template_arg, args):
paren = await cg.get_variable(config[CONF_ID]) paren = await cg.get_variable(config[CONF_ID])
@@ -186,8 +186,8 @@ ALARM_CONTROL_PANEL_CONDITION_SCHEMA = maybe_simple_id(
) )
@setup_entity("alarm_control_panel")
async def setup_alarm_control_panel_core_(var, config): async def setup_alarm_control_panel_core_(var, config):
await setup_entity(var, config, "alarm_control_panel")
for conf in config.get(CONF_ON_STATE, []): for conf in config.get(CONF_ON_STATE, []):
trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var)
await automation.build_automation(trigger, [], conf) await automation.build_automation(trigger, [], conf)
@@ -243,7 +243,10 @@ async def new_alarm_control_panel(config, *args):
@automation.register_action( @automation.register_action(
"alarm_control_panel.arm_away", ArmAwayAction, ALARM_CONTROL_PANEL_ACTION_SCHEMA "alarm_control_panel.arm_away",
ArmAwayAction,
ALARM_CONTROL_PANEL_ACTION_SCHEMA,
synchronous=True,
) )
async def alarm_action_arm_away_to_code(config, action_id, template_arg, args): async def alarm_action_arm_away_to_code(config, action_id, template_arg, args):
paren = await cg.get_variable(config[CONF_ID]) paren = await cg.get_variable(config[CONF_ID])
@@ -255,7 +258,10 @@ async def alarm_action_arm_away_to_code(config, action_id, template_arg, args):
@automation.register_action( @automation.register_action(
"alarm_control_panel.arm_home", ArmHomeAction, ALARM_CONTROL_PANEL_ACTION_SCHEMA "alarm_control_panel.arm_home",
ArmHomeAction,
ALARM_CONTROL_PANEL_ACTION_SCHEMA,
synchronous=True,
) )
async def alarm_action_arm_home_to_code(config, action_id, template_arg, args): async def alarm_action_arm_home_to_code(config, action_id, template_arg, args):
paren = await cg.get_variable(config[CONF_ID]) paren = await cg.get_variable(config[CONF_ID])
@@ -267,7 +273,10 @@ async def alarm_action_arm_home_to_code(config, action_id, template_arg, args):
@automation.register_action( @automation.register_action(
"alarm_control_panel.arm_night", ArmNightAction, ALARM_CONTROL_PANEL_ACTION_SCHEMA "alarm_control_panel.arm_night",
ArmNightAction,
ALARM_CONTROL_PANEL_ACTION_SCHEMA,
synchronous=True,
) )
async def alarm_action_arm_night_to_code(config, action_id, template_arg, args): async def alarm_action_arm_night_to_code(config, action_id, template_arg, args):
paren = await cg.get_variable(config[CONF_ID]) paren = await cg.get_variable(config[CONF_ID])
@@ -279,7 +288,10 @@ async def alarm_action_arm_night_to_code(config, action_id, template_arg, args):
@automation.register_action( @automation.register_action(
"alarm_control_panel.disarm", DisarmAction, ALARM_CONTROL_PANEL_ACTION_SCHEMA "alarm_control_panel.disarm",
DisarmAction,
ALARM_CONTROL_PANEL_ACTION_SCHEMA,
synchronous=True,
) )
async def alarm_action_disarm_to_code(config, action_id, template_arg, args): async def alarm_action_disarm_to_code(config, action_id, template_arg, args):
paren = await cg.get_variable(config[CONF_ID]) paren = await cg.get_variable(config[CONF_ID])
@@ -291,7 +303,10 @@ async def alarm_action_disarm_to_code(config, action_id, template_arg, args):
@automation.register_action( @automation.register_action(
"alarm_control_panel.pending", PendingAction, ALARM_CONTROL_PANEL_ACTION_SCHEMA "alarm_control_panel.pending",
PendingAction,
ALARM_CONTROL_PANEL_ACTION_SCHEMA,
synchronous=True,
) )
async def alarm_action_pending_to_code(config, action_id, template_arg, args): async def alarm_action_pending_to_code(config, action_id, template_arg, args):
paren = await cg.get_variable(config[CONF_ID]) paren = await cg.get_variable(config[CONF_ID])
@@ -299,7 +314,10 @@ async def alarm_action_pending_to_code(config, action_id, template_arg, args):
@automation.register_action( @automation.register_action(
"alarm_control_panel.triggered", TriggeredAction, ALARM_CONTROL_PANEL_ACTION_SCHEMA "alarm_control_panel.triggered",
TriggeredAction,
ALARM_CONTROL_PANEL_ACTION_SCHEMA,
synchronous=True,
) )
async def alarm_action_trigger_to_code(config, action_id, template_arg, args): async def alarm_action_trigger_to_code(config, action_id, template_arg, args):
paren = await cg.get_variable(config[CONF_ID]) paren = await cg.get_variable(config[CONF_ID])
@@ -307,7 +325,10 @@ async def alarm_action_trigger_to_code(config, action_id, template_arg, args):
@automation.register_action( @automation.register_action(
"alarm_control_panel.chime", ChimeAction, ALARM_CONTROL_PANEL_ACTION_SCHEMA "alarm_control_panel.chime",
ChimeAction,
ALARM_CONTROL_PANEL_ACTION_SCHEMA,
synchronous=True,
) )
async def alarm_action_chime_to_code(config, action_id, template_arg, args): async def alarm_action_chime_to_code(config, action_id, template_arg, args):
paren = await cg.get_variable(config[CONF_ID]) paren = await cg.get_variable(config[CONF_ID])
@@ -315,7 +336,10 @@ async def alarm_action_chime_to_code(config, action_id, template_arg, args):
@automation.register_action( @automation.register_action(
"alarm_control_panel.ready", ReadyAction, ALARM_CONTROL_PANEL_ACTION_SCHEMA "alarm_control_panel.ready",
ReadyAction,
ALARM_CONTROL_PANEL_ACTION_SCHEMA,
synchronous=True,
) )
@automation.register_condition( @automation.register_condition(
"alarm_control_panel.ready", "alarm_control_panel.ready",
@@ -12,7 +12,14 @@ AlarmControlPanelCall::AlarmControlPanelCall(AlarmControlPanel *parent) : parent
AlarmControlPanelCall &AlarmControlPanelCall::set_code(const char *code) { AlarmControlPanelCall &AlarmControlPanelCall::set_code(const char *code) {
if (code != nullptr) { if (code != nullptr) {
this->code_ = std::string(code); return this->set_code(code, strlen(code));
}
return *this;
}
AlarmControlPanelCall &AlarmControlPanelCall::set_code(const char *code, size_t len) {
if (code != nullptr) {
this->code_ = std::string(code, len);
} }
return *this; return *this;
} }
@@ -15,7 +15,8 @@ class AlarmControlPanelCall {
AlarmControlPanelCall(AlarmControlPanel *parent); AlarmControlPanelCall(AlarmControlPanel *parent);
AlarmControlPanelCall &set_code(const char *code); AlarmControlPanelCall &set_code(const char *code);
AlarmControlPanelCall &set_code(const std::string &code) { return this->set_code(code.c_str()); } AlarmControlPanelCall &set_code(const char *code, size_t len);
AlarmControlPanelCall &set_code(const std::string &code) { return this->set_code(code.c_str(), code.size()); }
AlarmControlPanelCall &arm_away(); AlarmControlPanelCall &arm_away();
AlarmControlPanelCall &arm_home(); AlarmControlPanelCall &arm_home();
AlarmControlPanelCall &arm_night(); AlarmControlPanelCall &arm_night();
+1 -1
View File
@@ -125,7 +125,7 @@ void Alpha3::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc
this->current_sensor_->publish_state(NAN); this->current_sensor_->publish_state(NAN);
if (this->speed_sensor_ != nullptr) if (this->speed_sensor_ != nullptr)
this->speed_sensor_->publish_state(NAN); this->speed_sensor_->publish_state(NAN);
if (this->speed_sensor_ != nullptr) if (this->voltage_sensor_ != nullptr)
this->voltage_sensor_->publish_state(NAN); this->voltage_sensor_->publish_state(NAN);
break; break;
} }
+3 -2
View File
@@ -63,8 +63,9 @@ void Am43Component::control(const CoverCall &call) {
ESP_LOGW(TAG, "[%s] Error writing stop command to device, error = %d", this->get_name().c_str(), status); ESP_LOGW(TAG, "[%s] Error writing stop command to device, error = %d", this->get_name().c_str(), status);
} }
} }
if (call.get_position().has_value()) { auto opt_pos = call.get_position();
auto pos = *call.get_position(); if (opt_pos.has_value()) {
auto pos = *opt_pos;
if (this->invert_position_) if (this->invert_position_)
pos = 1 - pos; pos = 1 - pos;
+9 -3
View File
@@ -69,9 +69,15 @@ SET_FRAME_SCHEMA = cv.Schema(
) )
@automation.register_action("animation.next_frame", NextFrameAction, NEXT_FRAME_SCHEMA) @automation.register_action(
@automation.register_action("animation.prev_frame", PrevFrameAction, PREV_FRAME_SCHEMA) "animation.next_frame", NextFrameAction, NEXT_FRAME_SCHEMA, synchronous=True
@automation.register_action("animation.set_frame", SetFrameAction, SET_FRAME_SCHEMA) )
@automation.register_action(
"animation.prev_frame", PrevFrameAction, PREV_FRAME_SCHEMA, synchronous=True
)
@automation.register_action(
"animation.set_frame", SetFrameAction, SET_FRAME_SCHEMA, synchronous=True
)
async def animation_action_to_code(config, action_id, template_arg, args): async def animation_action_to_code(config, action_id, template_arg, args):
paren = await cg.get_variable(config[CONF_ID]) paren = await cg.get_variable(config[CONF_ID])
var = cg.new_Pvariable(action_id, template_arg, paren) var = cg.new_Pvariable(action_id, template_arg, paren)
+12 -7
View File
@@ -24,8 +24,9 @@ void Anova::loop() {
} }
void Anova::control(const ClimateCall &call) { void Anova::control(const ClimateCall &call) {
if (call.get_mode().has_value()) { auto mode_val = call.get_mode();
ClimateMode mode = *call.get_mode(); if (mode_val.has_value()) {
ClimateMode mode = *mode_val;
AnovaPacket *pkt; AnovaPacket *pkt;
switch (mode) { switch (mode) {
case climate::CLIMATE_MODE_OFF: case climate::CLIMATE_MODE_OFF:
@@ -45,8 +46,9 @@ void Anova::control(const ClimateCall &call) {
ESP_LOGW(TAG, "[%s] esp_ble_gattc_write_char failed, status=%d", this->parent_->address_str(), status); ESP_LOGW(TAG, "[%s] esp_ble_gattc_write_char failed, status=%d", this->parent_->address_str(), status);
} }
} }
if (call.get_target_temperature().has_value()) { auto target_temp = call.get_target_temperature();
auto *pkt = this->codec_->get_set_target_temp_request(*call.get_target_temperature()); if (target_temp.has_value()) {
auto *pkt = this->codec_->get_set_target_temp_request(*target_temp);
auto status = auto status =
esp_ble_gattc_write_char(this->parent_->get_gattc_if(), this->parent_->get_conn_id(), this->char_handle_, esp_ble_gattc_write_char(this->parent_->get_gattc_if(), this->parent_->get_conn_id(), this->char_handle_,
pkt->length, pkt->data, ESP_GATT_WRITE_TYPE_NO_RSP, ESP_GATT_AUTH_REQ_NONE); pkt->length, pkt->data, ESP_GATT_WRITE_TYPE_NO_RSP, ESP_GATT_AUTH_REQ_NONE);
@@ -142,9 +144,12 @@ void Anova::update() {
return; return;
if (this->current_request_ < 2) { if (this->current_request_ < 2) {
auto *pkt = this->codec_->get_read_device_status_request(); AnovaPacket *pkt;
if (this->current_request_ == 0) if (this->current_request_ == 0) {
this->codec_->get_set_unit_request(this->fahrenheit_ ? 'f' : 'c'); pkt = this->codec_->get_set_unit_request(this->fahrenheit_ ? 'f' : 'c');
} else {
pkt = this->codec_->get_read_device_status_request();
}
auto status = auto status =
esp_ble_gattc_write_char(this->parent_->get_gattc_if(), this->parent_->get_conn_id(), this->char_handle_, esp_ble_gattc_write_char(this->parent_->get_gattc_if(), this->parent_->get_conn_id(), this->char_handle_,
pkt->length, pkt->data, ESP_GATT_WRITE_TYPE_NO_RSP, ESP_GATT_AUTH_REQ_NONE); pkt->length, pkt->data, ESP_GATT_WRITE_TYPE_NO_RSP, ESP_GATT_AUTH_REQ_NONE);
+26 -11
View File
@@ -453,7 +453,7 @@ async def to_code(config: ConfigType) -> None:
# and plaintext disabled. Only a factory reset can remove it. # and plaintext disabled. Only a factory reset can remove it.
cg.add_define("USE_API_PLAINTEXT") cg.add_define("USE_API_PLAINTEXT")
cg.add_define("USE_API_NOISE") cg.add_define("USE_API_NOISE")
cg.add_library("esphome/noise-c", "0.1.10") cg.add_library("esphome/noise-c", "0.1.11")
else: else:
cg.add_define("USE_API_PLAINTEXT") cg.add_define("USE_API_PLAINTEXT")
@@ -535,24 +535,31 @@ async def homeassistant_service_to_code(
cg.add_define("USE_API_HOMEASSISTANT_SERVICES") cg.add_define("USE_API_HOMEASSISTANT_SERVICES")
serv = await cg.get_variable(config[CONF_ID]) serv = await cg.get_variable(config[CONF_ID])
var = cg.new_Pvariable(action_id, template_arg, serv, False) var = cg.new_Pvariable(action_id, template_arg, serv, False)
templ = await cg.templatable(config[CONF_ACTION], args, None) templ = await cg.templatable(config[CONF_ACTION], args, cg.std_string)
cg.add(var.set_service(templ)) cg.add(var.set_service(templ))
# Initialize FixedVectors with exact sizes from config # Initialize FixedVectors with exact sizes from config
cg.add(var.init_data(len(config[CONF_DATA]))) cg.add(var.init_data(len(config[CONF_DATA])))
for key, value in config[CONF_DATA].items(): for key, value in config[CONF_DATA].items():
# output_type=None because lambdas can return non-string types (int,
# float, char*) that TemplatableStringValue converts via to_string.
# Static strings are manually wrapped for PROGMEM on ESP8266.
templ = await cg.templatable(value, args, None) templ = await cg.templatable(value, args, None)
cg.add(var.add_data(key, templ)) if isinstance(templ, str):
templ = cg.FlashStringLiteral(templ)
cg.add(var.add_data(cg.FlashStringLiteral(key), templ))
cg.add(var.init_data_template(len(config[CONF_DATA_TEMPLATE]))) cg.add(var.init_data_template(len(config[CONF_DATA_TEMPLATE])))
for key, value in config[CONF_DATA_TEMPLATE].items(): for key, value in config[CONF_DATA_TEMPLATE].items():
templ = await cg.templatable(value, args, None) templ = await cg.templatable(value, args, None)
cg.add(var.add_data_template(key, templ)) if isinstance(templ, str):
templ = cg.FlashStringLiteral(templ)
cg.add(var.add_data_template(cg.FlashStringLiteral(key), templ))
cg.add(var.init_variables(len(config[CONF_VARIABLES]))) cg.add(var.init_variables(len(config[CONF_VARIABLES])))
for key, value in config[CONF_VARIABLES].items(): for key, value in config[CONF_VARIABLES].items():
templ = await cg.templatable(value, args, None) templ = await cg.templatable(value, args, None)
cg.add(var.add_variable(key, templ)) cg.add(var.add_variable(cg.FlashStringLiteral(key), templ))
if on_error := config.get(CONF_ON_ERROR): if on_error := config.get(CONF_ON_ERROR):
cg.add_define("USE_API_HOMEASSISTANT_ACTION_RESPONSES") cg.add_define("USE_API_HOMEASSISTANT_ACTION_RESPONSES")
@@ -621,24 +628,31 @@ async def homeassistant_event_to_code(config, action_id, template_arg, args):
cg.add_define("USE_API_HOMEASSISTANT_SERVICES") cg.add_define("USE_API_HOMEASSISTANT_SERVICES")
serv = await cg.get_variable(config[CONF_ID]) serv = await cg.get_variable(config[CONF_ID])
var = cg.new_Pvariable(action_id, template_arg, serv, True) var = cg.new_Pvariable(action_id, template_arg, serv, True)
templ = await cg.templatable(config[CONF_EVENT], args, None) templ = await cg.templatable(config[CONF_EVENT], args, cg.std_string)
cg.add(var.set_service(templ)) cg.add(var.set_service(templ))
# Initialize FixedVectors with exact sizes from config # Initialize FixedVectors with exact sizes from config
cg.add(var.init_data(len(config[CONF_DATA]))) cg.add(var.init_data(len(config[CONF_DATA])))
for key, value in config[CONF_DATA].items(): for key, value in config[CONF_DATA].items():
# output_type=None because lambdas can return non-string types (int,
# float, char*) that TemplatableStringValue converts via to_string.
# Static strings are manually wrapped for PROGMEM on ESP8266.
templ = await cg.templatable(value, args, None) templ = await cg.templatable(value, args, None)
cg.add(var.add_data(key, templ)) if isinstance(templ, str):
templ = cg.FlashStringLiteral(templ)
cg.add(var.add_data(cg.FlashStringLiteral(key), templ))
cg.add(var.init_data_template(len(config[CONF_DATA_TEMPLATE]))) cg.add(var.init_data_template(len(config[CONF_DATA_TEMPLATE])))
for key, value in config[CONF_DATA_TEMPLATE].items(): for key, value in config[CONF_DATA_TEMPLATE].items():
templ = await cg.templatable(value, args, None) templ = await cg.templatable(value, args, None)
cg.add(var.add_data_template(key, templ)) if isinstance(templ, str):
templ = cg.FlashStringLiteral(templ)
cg.add(var.add_data_template(cg.FlashStringLiteral(key), templ))
cg.add(var.init_variables(len(config[CONF_VARIABLES]))) cg.add(var.init_variables(len(config[CONF_VARIABLES])))
for key, value in config[CONF_VARIABLES].items(): for key, value in config[CONF_VARIABLES].items():
templ = await cg.templatable(value, args, None) templ = await cg.templatable(value, args, None)
cg.add(var.add_variable(key, templ)) cg.add(var.add_variable(cg.FlashStringLiteral(key), templ))
return var return var
@@ -662,11 +676,11 @@ async def homeassistant_tag_scanned_to_code(config, action_id, template_arg, arg
cg.add_define("USE_API_HOMEASSISTANT_SERVICES") cg.add_define("USE_API_HOMEASSISTANT_SERVICES")
serv = await cg.get_variable(config[CONF_ID]) serv = await cg.get_variable(config[CONF_ID])
var = cg.new_Pvariable(action_id, template_arg, serv, True) var = cg.new_Pvariable(action_id, template_arg, serv, True)
cg.add(var.set_service("esphome.tag_scanned")) cg.add(var.set_service(cg.FlashStringLiteral("esphome.tag_scanned")))
# Initialize FixedVector with exact size (1 data field) # Initialize FixedVector with exact size (1 data field)
cg.add(var.init_data(1)) cg.add(var.init_data(1))
templ = await cg.templatable(config[CONF_TAG], args, cg.std_string) templ = await cg.templatable(config[CONF_TAG], args, cg.std_string)
cg.add(var.add_data("tag_id", templ)) cg.add(var.add_data(cg.FlashStringLiteral("tag_id"), templ))
return var return var
@@ -698,6 +712,7 @@ API_RESPOND_ACTION_SCHEMA = cv.All(
"api.respond", "api.respond",
APIRespondAction, APIRespondAction,
API_RESPOND_ACTION_SCHEMA, API_RESPOND_ACTION_SCHEMA,
synchronous=True,
) )
async def api_respond_to_code( async def api_respond_to_code(
config: ConfigType, config: ConfigType,
+155 -3
View File
@@ -58,6 +58,7 @@ service APIConnection {
rpc subscribe_bluetooth_connections_free(SubscribeBluetoothConnectionsFreeRequest) returns (BluetoothConnectionsFreeResponse) {} rpc subscribe_bluetooth_connections_free(SubscribeBluetoothConnectionsFreeRequest) returns (BluetoothConnectionsFreeResponse) {}
rpc unsubscribe_bluetooth_le_advertisements(UnsubscribeBluetoothLEAdvertisementsRequest) returns (void) {} rpc unsubscribe_bluetooth_le_advertisements(UnsubscribeBluetoothLEAdvertisementsRequest) returns (void) {}
rpc bluetooth_scanner_set_mode(BluetoothScannerSetModeRequest) returns (void) {} rpc bluetooth_scanner_set_mode(BluetoothScannerSetModeRequest) returns (void) {}
rpc bluetooth_set_connection_params(BluetoothSetConnectionParamsRequest) returns (BluetoothSetConnectionParamsResponse) {}
rpc subscribe_voice_assistant(SubscribeVoiceAssistantRequest) returns (void) {} rpc subscribe_voice_assistant(SubscribeVoiceAssistantRequest) returns (void) {}
rpc voice_assistant_get_configuration(VoiceAssistantConfigurationRequest) returns (VoiceAssistantConfigurationResponse) {} rpc voice_assistant_get_configuration(VoiceAssistantConfigurationRequest) returns (VoiceAssistantConfigurationResponse) {}
@@ -69,6 +70,12 @@ service APIConnection {
rpc zwave_proxy_request(ZWaveProxyRequest) returns (void) {} rpc zwave_proxy_request(ZWaveProxyRequest) returns (void) {}
rpc infrared_rf_transmit_raw_timings(InfraredRFTransmitRawTimingsRequest) returns (void) {} rpc infrared_rf_transmit_raw_timings(InfraredRFTransmitRawTimingsRequest) returns (void) {}
rpc serial_proxy_configure(SerialProxyConfigureRequest) returns (void) {}
rpc serial_proxy_write(SerialProxyWriteRequest) returns (void) {}
rpc serial_proxy_set_modem_pins(SerialProxySetModemPinsRequest) returns (void) {}
rpc serial_proxy_get_modem_pins(SerialProxyGetModemPinsRequest) returns (void) {}
rpc serial_proxy_request(SerialProxyRequest) returns (void) {}
} }
@@ -198,6 +205,17 @@ message DeviceInfo {
uint32 area_id = 3; uint32 area_id = 3;
} }
enum SerialProxyPortType {
SERIAL_PROXY_PORT_TYPE_TTL = 0;
SERIAL_PROXY_PORT_TYPE_RS232 = 1;
SERIAL_PROXY_PORT_TYPE_RS485 = 2;
}
message SerialProxyInfo {
string name = 1; // Human-readable port name
SerialProxyPortType port_type = 2; // Port type (RS232, RS485)
}
message DeviceInfoResponse { message DeviceInfoResponse {
option (id) = 10; option (id) = 10;
option (source) = SOURCE_SERVER; option (source) = SOURCE_SERVER;
@@ -260,6 +278,9 @@ message DeviceInfoResponse {
// Indicates if Z-Wave proxy support is available and features supported // Indicates if Z-Wave proxy support is available and features supported
uint32 zwave_proxy_feature_flags = 23 [(field_ifdef) = "USE_ZWAVE_PROXY"]; uint32 zwave_proxy_feature_flags = 23 [(field_ifdef) = "USE_ZWAVE_PROXY"];
uint32 zwave_home_id = 24 [(field_ifdef) = "USE_ZWAVE_PROXY"]; uint32 zwave_home_id = 24 [(field_ifdef) = "USE_ZWAVE_PROXY"];
// Serial proxy instance metadata
repeated SerialProxyInfo serial_proxies = 25 [(field_ifdef) = "USE_SERIAL_PROXY", (fixed_array_size_define) = "SERIAL_PROXY_COUNT"];
} }
message ListEntitiesRequest { message ListEntitiesRequest {
@@ -1583,11 +1604,11 @@ message BluetoothLEAdvertisementResponse {
} }
message BluetoothLERawAdvertisement { message BluetoothLERawAdvertisement {
uint64 address = 1; uint64 address = 1 [(force) = true];
sint32 rssi = 2; sint32 rssi = 2 [(force) = true];
uint32 address_type = 3; uint32 address_type = 3;
bytes data = 4 [(fixed_array_size) = 62]; bytes data = 4 [(fixed_array_size) = 62, (force) = true];
} }
message BluetoothLERawAdvertisementsResponse { message BluetoothLERawAdvertisementsResponse {
@@ -2517,3 +2538,134 @@ message InfraredRFReceiveEvent {
fixed32 key = 2; // Key identifying the receiver instance fixed32 key = 2; // Key identifying the receiver instance
repeated sint32 timings = 3 [packed = true, (container_pointer_no_template) = "std::vector<int32_t>"]; // Raw timings in microseconds (zigzag-encoded): alternating mark/space periods repeated sint32 timings = 3 [packed = true, (container_pointer_no_template) = "std::vector<int32_t>"]; // Raw timings in microseconds (zigzag-encoded): alternating mark/space periods
} }
// ==================== SERIAL PROXY ====================
enum SerialProxyParity {
SERIAL_PROXY_PARITY_NONE = 0;
SERIAL_PROXY_PARITY_EVEN = 1;
SERIAL_PROXY_PARITY_ODD = 2;
}
// Configure UART parameters for a serial proxy instance
message SerialProxyConfigureRequest {
option (id) = 138;
option (source) = SOURCE_CLIENT;
option (ifdef) = "USE_SERIAL_PROXY";
uint32 instance = 1; // Instance index (0-based)
uint32 baudrate = 2; // Baud rate in bits per second
bool flow_control = 3; // Enable hardware flow control
SerialProxyParity parity = 4; // Parity setting
uint32 stop_bits = 5; // Number of stop bits (1 or 2)
uint32 data_size = 6; // Number of data bits (5-8)
}
// Data received from a serial device, forwarded to clients
message SerialProxyDataReceived {
option (id) = 139;
option (source) = SOURCE_SERVER;
option (ifdef) = "USE_SERIAL_PROXY";
option (no_delay) = true;
uint32 instance = 1; // Instance index (0-based)
bytes data = 2; // Raw data received from the serial device
}
// Write data to a serial device
message SerialProxyWriteRequest {
option (id) = 140;
option (source) = SOURCE_CLIENT;
option (ifdef) = "USE_SERIAL_PROXY";
option (no_delay) = true;
uint32 instance = 1; // Instance index (0-based)
bytes data = 2; // Raw data to write to the serial device
}
// Set modem control pin states (RTS and DTR)
message SerialProxySetModemPinsRequest {
option (id) = 141;
option (source) = SOURCE_CLIENT;
option (ifdef) = "USE_SERIAL_PROXY";
uint32 instance = 1; // Instance index (0-based)
uint32 line_states = 2; // Bitmask of SerialProxyLineStateFlags
}
// Request current modem control pin states
message SerialProxyGetModemPinsRequest {
option (id) = 142;
option (source) = SOURCE_CLIENT;
option (ifdef) = "USE_SERIAL_PROXY";
uint32 instance = 1; // Instance index (0-based)
}
// Response with current modem control pin states
message SerialProxyGetModemPinsResponse {
option (id) = 143;
option (source) = SOURCE_SERVER;
option (ifdef) = "USE_SERIAL_PROXY";
uint32 instance = 1; // Instance index (0-based)
uint32 line_states = 2; // Bitmask of SerialProxyLineStateFlags
}
enum SerialProxyRequestType {
SERIAL_PROXY_REQUEST_TYPE_SUBSCRIBE = 0; // Subscribe to receive data from this serial proxy instance
SERIAL_PROXY_REQUEST_TYPE_UNSUBSCRIBE = 1; // Unsubscribe from this serial proxy instance
SERIAL_PROXY_REQUEST_TYPE_FLUSH = 2; // Flush the serial port (block until all TX data is sent)
}
enum SerialProxyStatus {
SERIAL_PROXY_STATUS_OK = 0; // Completed successfully; TX drain confirmed
SERIAL_PROXY_STATUS_ASSUMED_SUCCESS = 1; // Platform cannot confirm TX drain; success assumed
SERIAL_PROXY_STATUS_ERROR = 2; // Driver or hardware error
SERIAL_PROXY_STATUS_TIMEOUT = 3; // Timed out before TX completed
SERIAL_PROXY_STATUS_NOT_SUPPORTED = 4; // Request type not supported by this instance
}
// Generic request message for simple serial proxy operations
message SerialProxyRequest {
option (id) = 144;
option (source) = SOURCE_CLIENT;
option (ifdef) = "USE_SERIAL_PROXY";
uint32 instance = 1; // Instance index (0-based)
SerialProxyRequestType type = 2; // Request type
}
// Response to a SerialProxyRequest (e.g. flush completion or failure)
message SerialProxyRequestResponse {
option (id) = 147;
option (source) = SOURCE_SERVER;
option (ifdef) = "USE_SERIAL_PROXY";
uint32 instance = 1; // Instance index (0-based)
SerialProxyRequestType type = 2; // Which request type this responds to
SerialProxyStatus status = 3; // Result status
string error_message = 4; // Additional detail on failure (optional)
}
// ==================== BLUETOOTH CONNECTION PARAMS ====================
message BluetoothSetConnectionParamsRequest {
option (id) = 145;
option (source) = SOURCE_CLIENT;
option (ifdef) = "USE_BLUETOOTH_PROXY";
uint64 address = 1;
uint32 min_interval = 2; // units of 1.25ms
uint32 max_interval = 3; // units of 1.25ms
uint32 latency = 4;
uint32 timeout = 5; // units of 10ms
}
message BluetoothSetConnectionParamsResponse {
option (id) = 146;
option (source) = SOURCE_SERVER;
option (ifdef) = "USE_BLUETOOTH_PROXY";
uint64 address = 1;
int32 error = 2;
}
+13
View File
@@ -0,0 +1,13 @@
#include "api_buffer.h"
namespace esphome::api {
void APIBuffer::grow_(size_t n) {
auto new_data = make_buffer(n);
if (this->size_)
std::memcpy(new_data.get(), this->data_.get(), this->size_);
this->data_ = std::move(new_data);
this->capacity_ = n;
}
} // namespace esphome::api
+67
View File
@@ -0,0 +1,67 @@
#pragma once
#include <cstdint>
#include <cstring>
#include <memory>
#include "esphome/core/defines.h"
#include "esphome/core/helpers.h"
namespace esphome::api {
/// Helper to use make_unique_for_overwrite where available (skips zero-fill),
/// falling back to make_unique on older GCC (ESP8266, LibreTiny).
inline std::unique_ptr<uint8_t[]> make_buffer(size_t n) {
#if defined(USE_ESP8266) || defined(USE_LIBRETINY)
return std::make_unique<uint8_t[]>(n);
#else
return std::make_unique_for_overwrite<uint8_t[]>(n);
#endif
}
/// Byte buffer that skips zero-initialization on resize().
///
/// std::vector<uint8_t>::resize() zero-fills new bytes via memset. For the
/// shared protobuf write buffer, every byte is overwritten by the encoder,
/// making the zero-fill pure waste. For the receive buffer, bytes are
/// overwritten by socket reads.
///
/// Designed for bulk clear/resize/overwrite patterns. grow_() allocates
/// exactly the requested size (no growth factor) since callers resize to
/// known sizes rather than appending incrementally.
///
/// Safe because: callers always write exactly the number of bytes they
/// resize for. In the protobuf write path, debug_check_bounds_ validates
/// writes in debug builds.
class APIBuffer {
public:
void clear() { this->size_ = 0; }
inline void reserve(size_t n) ESPHOME_ALWAYS_INLINE {
if (n > this->capacity_)
this->grow_(n);
}
inline void resize(size_t n) ESPHOME_ALWAYS_INLINE {
this->reserve(n);
this->size_ = n; // no zero-fill
}
uint8_t *data() { return this->data_.get(); }
const uint8_t *data() const { return this->data_.get(); }
size_t size() const { return this->size_; }
bool empty() const { return this->size_ == 0; }
uint8_t &operator[](size_t i) { return this->data_[i]; }
const uint8_t &operator[](size_t i) const { return this->data_[i]; }
/// Release all memory (equivalent to std::vector swap trick).
void release() {
this->data_.reset();
this->size_ = 0;
this->capacity_ = 0;
}
protected:
void grow_(size_t n);
std::unique_ptr<uint8_t[]> data_;
size_t size_{0};
size_t capacity_{0};
};
} // namespace esphome::api
+282 -137
View File
@@ -114,9 +114,10 @@ APIConnection::APIConnection(std::unique_ptr<socket::Socket> sock, APIServer *pa
this->helper_ = std::unique_ptr<APIFrameHelper>{new APIPlaintextFrameHelper(std::move(sock))}; this->helper_ = std::unique_ptr<APIFrameHelper>{new APIPlaintextFrameHelper(std::move(sock))};
} }
#elif defined(USE_API_PLAINTEXT) #elif defined(USE_API_PLAINTEXT)
this->helper_ = std::unique_ptr<APIFrameHelper>{new APIPlaintextFrameHelper(std::move(sock))}; this->helper_ = std::unique_ptr<APIPlaintextFrameHelper>{new APIPlaintextFrameHelper(std::move(sock))};
#elif defined(USE_API_NOISE) #elif defined(USE_API_NOISE)
this->helper_ = std::unique_ptr<APIFrameHelper>{new APINoiseFrameHelper(std::move(sock), parent->get_noise_ctx())}; this->helper_ =
std::unique_ptr<APINoiseFrameHelper>{new APINoiseFrameHelper(std::move(sock), parent->get_noise_ctx())};
#else #else
#error "No frame helper defined" #error "No frame helper defined"
#endif #endif
@@ -154,6 +155,18 @@ APIConnection::~APIConnection() {
voice_assistant::global_voice_assistant->client_subscription(this, false); voice_assistant::global_voice_assistant->client_subscription(this, false);
} }
#endif #endif
#ifdef USE_ZWAVE_PROXY
if (zwave_proxy::global_zwave_proxy != nullptr && zwave_proxy::global_zwave_proxy->get_api_connection() == this) {
zwave_proxy::global_zwave_proxy->zwave_proxy_request(this, enums::ZWAVE_PROXY_REQUEST_TYPE_UNSUBSCRIBE);
}
#endif
#ifdef USE_SERIAL_PROXY
for (auto *proxy : App.get_serial_proxies()) {
if (proxy->get_api_connection() == this) {
proxy->serial_proxy_request(this, enums::SERIAL_PROXY_REQUEST_TYPE_UNSUBSCRIBE);
}
}
#endif
} }
void APIConnection::destroy_active_iterator_() { void APIConnection::destroy_active_iterator_() {
@@ -274,7 +287,7 @@ void APIConnection::check_keepalive_(uint32_t now) {
// Only send ping if we're not disconnecting // Only send ping if we're not disconnecting
ESP_LOGVV(TAG, "Sending keepalive PING"); ESP_LOGVV(TAG, "Sending keepalive PING");
PingRequest req; PingRequest req;
this->flags_.sent_ping = this->send_message(req, PingRequest::MESSAGE_TYPE); this->flags_.sent_ping = this->send_message(req);
if (!this->flags_.sent_ping) { if (!this->flags_.sent_ping) {
// If we can't send the ping request directly (tx_buffer full), // If we can't send the ping request directly (tx_buffer full),
// schedule it at the front of the batch so it will be sent with priority // schedule it at the front of the batch so it will be sent with priority
@@ -335,7 +348,7 @@ bool APIConnection::send_disconnect_response_() {
this->log_client_(ESPHOME_LOG_LEVEL_DEBUG, LOG_STR("disconnected")); this->log_client_(ESPHOME_LOG_LEVEL_DEBUG, LOG_STR("disconnected"));
this->flags_.next_close = true; this->flags_.next_close = true;
DisconnectResponse resp; DisconnectResponse resp;
return this->send_message(resp, DisconnectResponse::MESSAGE_TYPE); return this->send_message(resp);
} }
void APIConnection::on_disconnect_response() { void APIConnection::on_disconnect_response() {
// Don't close socket here, let APIServer::loop() do it // Don't close socket here, let APIServer::loop() do it
@@ -343,56 +356,57 @@ void APIConnection::on_disconnect_response() {
this->flags_.remove = true; this->flags_.remove = true;
} }
// Encodes a message to the buffer and returns the total number of bytes used, uint16_t APIConnection::fill_and_encode_entity_state(EntityBase *entity, StateResponseProtoMessage &msg,
// including header and footer overhead. Returns 0 if the message doesn't fit. CalculateSizeFn size_fn, MessageEncodeFn encode_fn,
uint16_t APIConnection::encode_message_to_buffer(ProtoMessage &msg, uint8_t message_type, APIConnection *conn, APIConnection *conn, uint32_t remaining_size) {
uint32_t remaining_size) { msg.key = entity->get_object_id_hash();
#ifdef HAS_PROTO_MESSAGE_DUMP #ifdef USE_DEVICES
// If in log-only mode, just log and return msg.device_id = entity->get_device_id();
if (conn->flags_.log_only_mode) {
DumpBuffer dump_buf;
conn->log_send_message_(msg.message_name(), msg.dump_to(dump_buf));
return 1; // Return non-zero to indicate "success" for logging
}
#endif #endif
return encode_to_buffer(size_fn(&msg), encode_fn, &msg, conn, remaining_size);
}
// Calculate size uint16_t APIConnection::fill_and_encode_entity_info(EntityBase *entity, InfoResponseProtoMessage &msg,
uint32_t calculated_size = msg.calculated_size(); CalculateSizeFn size_fn, MessageEncodeFn encode_fn,
APIConnection *conn, uint32_t remaining_size) {
// Set common fields that are shared by all entity types
msg.key = entity->get_object_id_hash();
// Cache frame sizes to avoid repeated virtual calls // API 1.14+ clients compute object_id client-side from the entity name
const uint8_t header_padding = conn->helper_->frame_header_padding(); // For older clients, we must send object_id for backward compatibility
const uint8_t footer_size = conn->helper_->frame_footer_size(); // See: https://github.com/esphome/backlog/issues/76
// TODO: Remove this backward compat code before 2026.7.0 - all clients should support API 1.14 by then
// Calculate total size with padding for buffer allocation // Buffer must remain in scope until encode_to_buffer is called
size_t total_calculated_size = calculated_size + header_padding + footer_size; char object_id_buf[OBJECT_ID_MAX_LEN];
if (!conn->client_supports_api_version(1, 14)) {
// Check if it fits msg.object_id = entity->get_object_id_to(object_id_buf);
if (total_calculated_size > remaining_size) {
return 0; // Doesn't fit
} }
// Get buffer size after allocation (which includes header padding) if (entity->has_own_name()) {
std::vector<uint8_t> &shared_buf = conn->parent_->get_shared_buffer_ref(); msg.name = entity->get_name();
if (conn->flags_.batch_first_message) {
// First message - buffer already prepared by caller, just clear flag
conn->flags_.batch_first_message = false;
} else {
// Batch message second or later
// Add padding for previous message footer + this message header
size_t current_size = shared_buf.size();
shared_buf.reserve(current_size + total_calculated_size);
shared_buf.resize(current_size + footer_size + header_padding);
} }
// Pre-resize buffer to include payload, then encode through raw pointer // Set common EntityBase properties
size_t write_start = shared_buf.size(); #ifdef USE_ENTITY_ICON
shared_buf.resize(write_start + calculated_size); char icon_buf[MAX_ICON_LENGTH];
ProtoWriteBuffer buffer{&shared_buf, write_start}; msg.icon = StringRef(entity->get_icon_to(icon_buf));
msg.encode(buffer); #endif
msg.disabled_by_default = entity->is_disabled_by_default();
msg.entity_category = static_cast<enums::EntityCategory>(entity->get_entity_category());
#ifdef USE_DEVICES
msg.device_id = entity->get_device_id();
#endif
return encode_to_buffer(size_fn(&msg), encode_fn, &msg, conn, remaining_size);
}
// Return total size (header + payload + footer) uint16_t APIConnection::fill_and_encode_entity_info_with_device_class(EntityBase *entity, InfoResponseProtoMessage &msg,
return static_cast<uint16_t>(header_padding + calculated_size + footer_size); StringRef &device_class_field,
CalculateSizeFn size_fn,
MessageEncodeFn encode_fn, APIConnection *conn,
uint32_t remaining_size) {
char dc_buf[MAX_DEVICE_CLASS_LENGTH];
device_class_field = StringRef(entity->get_device_class_to(dc_buf));
return fill_and_encode_entity_info(entity, msg, size_fn, encode_fn, conn, remaining_size);
} }
#ifdef USE_BINARY_SENSOR #ifdef USE_BINARY_SENSOR
@@ -406,17 +420,14 @@ uint16_t APIConnection::try_send_binary_sensor_state(EntityBase *entity, APIConn
BinarySensorStateResponse resp; BinarySensorStateResponse resp;
resp.state = binary_sensor->state; resp.state = binary_sensor->state;
resp.missing_state = !binary_sensor->has_state(); resp.missing_state = !binary_sensor->has_state();
return fill_and_encode_entity_state(binary_sensor, resp, BinarySensorStateResponse::MESSAGE_TYPE, conn, return fill_and_encode_entity_state(binary_sensor, resp, conn, remaining_size);
remaining_size);
} }
uint16_t APIConnection::try_send_binary_sensor_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size) { uint16_t APIConnection::try_send_binary_sensor_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size) {
auto *binary_sensor = static_cast<binary_sensor::BinarySensor *>(entity); auto *binary_sensor = static_cast<binary_sensor::BinarySensor *>(entity);
ListEntitiesBinarySensorResponse msg; ListEntitiesBinarySensorResponse msg;
msg.device_class = binary_sensor->get_device_class_ref();
msg.is_status_binary_sensor = binary_sensor->is_status_binary_sensor(); msg.is_status_binary_sensor = binary_sensor->is_status_binary_sensor();
return fill_and_encode_entity_info(binary_sensor, msg, ListEntitiesBinarySensorResponse::MESSAGE_TYPE, conn, return fill_and_encode_entity_info_with_device_class(binary_sensor, msg, msg.device_class, conn, remaining_size);
remaining_size);
} }
#endif #endif
@@ -432,7 +443,7 @@ uint16_t APIConnection::try_send_cover_state(EntityBase *entity, APIConnection *
if (traits.get_supports_tilt()) if (traits.get_supports_tilt())
msg.tilt = cover->tilt; msg.tilt = cover->tilt;
msg.current_operation = static_cast<enums::CoverOperation>(cover->current_operation); msg.current_operation = static_cast<enums::CoverOperation>(cover->current_operation);
return fill_and_encode_entity_state(cover, msg, CoverStateResponse::MESSAGE_TYPE, conn, remaining_size); return fill_and_encode_entity_state(cover, msg, conn, remaining_size);
} }
uint16_t APIConnection::try_send_cover_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size) { uint16_t APIConnection::try_send_cover_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size) {
auto *cover = static_cast<cover::Cover *>(entity); auto *cover = static_cast<cover::Cover *>(entity);
@@ -442,8 +453,7 @@ uint16_t APIConnection::try_send_cover_info(EntityBase *entity, APIConnection *c
msg.supports_position = traits.get_supports_position(); msg.supports_position = traits.get_supports_position();
msg.supports_tilt = traits.get_supports_tilt(); msg.supports_tilt = traits.get_supports_tilt();
msg.supports_stop = traits.get_supports_stop(); msg.supports_stop = traits.get_supports_stop();
msg.device_class = cover->get_device_class_ref(); return fill_and_encode_entity_info_with_device_class(cover, msg, msg.device_class, conn, remaining_size);
return fill_and_encode_entity_info(cover, msg, ListEntitiesCoverResponse::MESSAGE_TYPE, conn, remaining_size);
} }
void APIConnection::on_cover_command_request(const CoverCommandRequest &msg) { void APIConnection::on_cover_command_request(const CoverCommandRequest &msg) {
ENTITY_COMMAND_MAKE_CALL(cover::Cover, cover, cover) ENTITY_COMMAND_MAKE_CALL(cover::Cover, cover, cover)
@@ -475,7 +485,7 @@ uint16_t APIConnection::try_send_fan_state(EntityBase *entity, APIConnection *co
msg.direction = static_cast<enums::FanDirection>(fan->direction); msg.direction = static_cast<enums::FanDirection>(fan->direction);
if (traits.supports_preset_modes() && fan->has_preset_mode()) if (traits.supports_preset_modes() && fan->has_preset_mode())
msg.preset_mode = fan->get_preset_mode(); msg.preset_mode = fan->get_preset_mode();
return fill_and_encode_entity_state(fan, msg, FanStateResponse::MESSAGE_TYPE, conn, remaining_size); return fill_and_encode_entity_state(fan, msg, conn, remaining_size);
} }
uint16_t APIConnection::try_send_fan_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size) { uint16_t APIConnection::try_send_fan_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size) {
auto *fan = static_cast<fan::Fan *>(entity); auto *fan = static_cast<fan::Fan *>(entity);
@@ -486,7 +496,7 @@ uint16_t APIConnection::try_send_fan_info(EntityBase *entity, APIConnection *con
msg.supports_direction = traits.supports_direction(); msg.supports_direction = traits.supports_direction();
msg.supported_speed_count = traits.supported_speed_count(); msg.supported_speed_count = traits.supported_speed_count();
msg.supported_preset_modes = &traits.supported_preset_modes(); msg.supported_preset_modes = &traits.supported_preset_modes();
return fill_and_encode_entity_info(fan, msg, ListEntitiesFanResponse::MESSAGE_TYPE, conn, remaining_size); return fill_and_encode_entity_info(fan, msg, conn, remaining_size);
} }
void APIConnection::on_fan_command_request(const FanCommandRequest &msg) { void APIConnection::on_fan_command_request(const FanCommandRequest &msg) {
ENTITY_COMMAND_MAKE_CALL(fan::Fan, fan, fan) ENTITY_COMMAND_MAKE_CALL(fan::Fan, fan, fan)
@@ -529,7 +539,7 @@ uint16_t APIConnection::try_send_light_state(EntityBase *entity, APIConnection *
if (light->supports_effects()) { if (light->supports_effects()) {
resp.effect = light->get_effect_name(); resp.effect = light->get_effect_name();
} }
return fill_and_encode_entity_state(light, resp, LightStateResponse::MESSAGE_TYPE, conn, remaining_size); return fill_and_encode_entity_state(light, resp, conn, remaining_size);
} }
uint16_t APIConnection::try_send_light_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size) { uint16_t APIConnection::try_send_light_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size) {
auto *light = static_cast<light::LightState *>(entity); auto *light = static_cast<light::LightState *>(entity);
@@ -554,7 +564,7 @@ uint16_t APIConnection::try_send_light_info(EntityBase *entity, APIConnection *c
} }
} }
msg.effects = &effects_list; msg.effects = &effects_list;
return fill_and_encode_entity_info(light, msg, ListEntitiesLightResponse::MESSAGE_TYPE, conn, remaining_size); return fill_and_encode_entity_info(light, msg, conn, remaining_size);
} }
void APIConnection::on_light_command_request(const LightCommandRequest &msg) { void APIConnection::on_light_command_request(const LightCommandRequest &msg) {
ENTITY_COMMAND_MAKE_CALL(light::LightState, light, light) ENTITY_COMMAND_MAKE_CALL(light::LightState, light, light)
@@ -599,7 +609,7 @@ uint16_t APIConnection::try_send_sensor_state(EntityBase *entity, APIConnection
SensorStateResponse resp; SensorStateResponse resp;
resp.state = sensor->state; resp.state = sensor->state;
resp.missing_state = !sensor->has_state(); resp.missing_state = !sensor->has_state();
return fill_and_encode_entity_state(sensor, resp, SensorStateResponse::MESSAGE_TYPE, conn, remaining_size); return fill_and_encode_entity_state(sensor, resp, conn, remaining_size);
} }
uint16_t APIConnection::try_send_sensor_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size) { uint16_t APIConnection::try_send_sensor_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size) {
@@ -608,9 +618,8 @@ uint16_t APIConnection::try_send_sensor_info(EntityBase *entity, APIConnection *
msg.unit_of_measurement = sensor->get_unit_of_measurement_ref(); msg.unit_of_measurement = sensor->get_unit_of_measurement_ref();
msg.accuracy_decimals = sensor->get_accuracy_decimals(); msg.accuracy_decimals = sensor->get_accuracy_decimals();
msg.force_update = sensor->get_force_update(); msg.force_update = sensor->get_force_update();
msg.device_class = sensor->get_device_class_ref();
msg.state_class = static_cast<enums::SensorStateClass>(sensor->get_state_class()); msg.state_class = static_cast<enums::SensorStateClass>(sensor->get_state_class());
return fill_and_encode_entity_info(sensor, msg, ListEntitiesSensorResponse::MESSAGE_TYPE, conn, remaining_size); return fill_and_encode_entity_info_with_device_class(sensor, msg, msg.device_class, conn, remaining_size);
} }
#endif #endif
@@ -623,15 +632,14 @@ uint16_t APIConnection::try_send_switch_state(EntityBase *entity, APIConnection
auto *a_switch = static_cast<switch_::Switch *>(entity); auto *a_switch = static_cast<switch_::Switch *>(entity);
SwitchStateResponse resp; SwitchStateResponse resp;
resp.state = a_switch->state; resp.state = a_switch->state;
return fill_and_encode_entity_state(a_switch, resp, SwitchStateResponse::MESSAGE_TYPE, conn, remaining_size); return fill_and_encode_entity_state(a_switch, resp, conn, remaining_size);
} }
uint16_t APIConnection::try_send_switch_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size) { uint16_t APIConnection::try_send_switch_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size) {
auto *a_switch = static_cast<switch_::Switch *>(entity); auto *a_switch = static_cast<switch_::Switch *>(entity);
ListEntitiesSwitchResponse msg; ListEntitiesSwitchResponse msg;
msg.assumed_state = a_switch->assumed_state(); msg.assumed_state = a_switch->assumed_state();
msg.device_class = a_switch->get_device_class_ref(); return fill_and_encode_entity_info_with_device_class(a_switch, msg, msg.device_class, conn, remaining_size);
return fill_and_encode_entity_info(a_switch, msg, ListEntitiesSwitchResponse::MESSAGE_TYPE, conn, remaining_size);
} }
void APIConnection::on_switch_command_request(const SwitchCommandRequest &msg) { void APIConnection::on_switch_command_request(const SwitchCommandRequest &msg) {
ENTITY_COMMAND_GET(switch_::Switch, a_switch, switch) ENTITY_COMMAND_GET(switch_::Switch, a_switch, switch)
@@ -655,14 +663,12 @@ uint16_t APIConnection::try_send_text_sensor_state(EntityBase *entity, APIConnec
TextSensorStateResponse resp; TextSensorStateResponse resp;
resp.state = StringRef(text_sensor->state); resp.state = StringRef(text_sensor->state);
resp.missing_state = !text_sensor->has_state(); resp.missing_state = !text_sensor->has_state();
return fill_and_encode_entity_state(text_sensor, resp, TextSensorStateResponse::MESSAGE_TYPE, conn, remaining_size); return fill_and_encode_entity_state(text_sensor, resp, conn, remaining_size);
} }
uint16_t APIConnection::try_send_text_sensor_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size) { uint16_t APIConnection::try_send_text_sensor_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size) {
auto *text_sensor = static_cast<text_sensor::TextSensor *>(entity); auto *text_sensor = static_cast<text_sensor::TextSensor *>(entity);
ListEntitiesTextSensorResponse msg; ListEntitiesTextSensorResponse msg;
msg.device_class = text_sensor->get_device_class_ref(); return fill_and_encode_entity_info_with_device_class(text_sensor, msg, msg.device_class, conn, remaining_size);
return fill_and_encode_entity_info(text_sensor, msg, ListEntitiesTextSensorResponse::MESSAGE_TYPE, conn,
remaining_size);
} }
#endif #endif
@@ -702,7 +708,7 @@ uint16_t APIConnection::try_send_climate_state(EntityBase *entity, APIConnection
resp.current_humidity = climate->current_humidity; resp.current_humidity = climate->current_humidity;
if (traits.has_feature_flags(climate::CLIMATE_SUPPORTS_TARGET_HUMIDITY)) if (traits.has_feature_flags(climate::CLIMATE_SUPPORTS_TARGET_HUMIDITY))
resp.target_humidity = climate->target_humidity; resp.target_humidity = climate->target_humidity;
return fill_and_encode_entity_state(climate, resp, ClimateStateResponse::MESSAGE_TYPE, conn, remaining_size); return fill_and_encode_entity_state(climate, resp, conn, remaining_size);
} }
uint16_t APIConnection::try_send_climate_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size) { uint16_t APIConnection::try_send_climate_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size) {
auto *climate = static_cast<climate::Climate *>(entity); auto *climate = static_cast<climate::Climate *>(entity);
@@ -729,7 +735,7 @@ uint16_t APIConnection::try_send_climate_info(EntityBase *entity, APIConnection
msg.supported_presets = &traits.get_supported_presets(); msg.supported_presets = &traits.get_supported_presets();
msg.supported_custom_presets = &traits.get_supported_custom_presets(); msg.supported_custom_presets = &traits.get_supported_custom_presets();
msg.supported_swing_modes = &traits.get_supported_swing_modes(); msg.supported_swing_modes = &traits.get_supported_swing_modes();
return fill_and_encode_entity_info(climate, msg, ListEntitiesClimateResponse::MESSAGE_TYPE, conn, remaining_size); return fill_and_encode_entity_info(climate, msg, conn, remaining_size);
} }
void APIConnection::on_climate_command_request(const ClimateCommandRequest &msg) { void APIConnection::on_climate_command_request(const ClimateCommandRequest &msg) {
ENTITY_COMMAND_MAKE_CALL(climate::Climate, climate, climate) ENTITY_COMMAND_MAKE_CALL(climate::Climate, climate, climate)
@@ -767,19 +773,18 @@ uint16_t APIConnection::try_send_number_state(EntityBase *entity, APIConnection
NumberStateResponse resp; NumberStateResponse resp;
resp.state = number->state; resp.state = number->state;
resp.missing_state = !number->has_state(); resp.missing_state = !number->has_state();
return fill_and_encode_entity_state(number, resp, NumberStateResponse::MESSAGE_TYPE, conn, remaining_size); return fill_and_encode_entity_state(number, resp, conn, remaining_size);
} }
uint16_t APIConnection::try_send_number_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size) { uint16_t APIConnection::try_send_number_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size) {
auto *number = static_cast<number::Number *>(entity); auto *number = static_cast<number::Number *>(entity);
ListEntitiesNumberResponse msg; ListEntitiesNumberResponse msg;
msg.unit_of_measurement = number->traits.get_unit_of_measurement_ref(); msg.unit_of_measurement = number->get_unit_of_measurement_ref();
msg.mode = static_cast<enums::NumberMode>(number->traits.get_mode()); msg.mode = static_cast<enums::NumberMode>(number->traits.get_mode());
msg.device_class = number->traits.get_device_class_ref();
msg.min_value = number->traits.get_min_value(); msg.min_value = number->traits.get_min_value();
msg.max_value = number->traits.get_max_value(); msg.max_value = number->traits.get_max_value();
msg.step = number->traits.get_step(); msg.step = number->traits.get_step();
return fill_and_encode_entity_info(number, msg, ListEntitiesNumberResponse::MESSAGE_TYPE, conn, remaining_size); return fill_and_encode_entity_info_with_device_class(number, msg, msg.device_class, conn, remaining_size);
} }
void APIConnection::on_number_command_request(const NumberCommandRequest &msg) { void APIConnection::on_number_command_request(const NumberCommandRequest &msg) {
ENTITY_COMMAND_MAKE_CALL(number::Number, number, number) ENTITY_COMMAND_MAKE_CALL(number::Number, number, number)
@@ -799,12 +804,12 @@ uint16_t APIConnection::try_send_date_state(EntityBase *entity, APIConnection *c
resp.year = date->year; resp.year = date->year;
resp.month = date->month; resp.month = date->month;
resp.day = date->day; resp.day = date->day;
return fill_and_encode_entity_state(date, resp, DateStateResponse::MESSAGE_TYPE, conn, remaining_size); return fill_and_encode_entity_state(date, resp, conn, remaining_size);
} }
uint16_t APIConnection::try_send_date_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size) { uint16_t APIConnection::try_send_date_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size) {
auto *date = static_cast<datetime::DateEntity *>(entity); auto *date = static_cast<datetime::DateEntity *>(entity);
ListEntitiesDateResponse msg; ListEntitiesDateResponse msg;
return fill_and_encode_entity_info(date, msg, ListEntitiesDateResponse::MESSAGE_TYPE, conn, remaining_size); return fill_and_encode_entity_info(date, msg, conn, remaining_size);
} }
void APIConnection::on_date_command_request(const DateCommandRequest &msg) { void APIConnection::on_date_command_request(const DateCommandRequest &msg) {
ENTITY_COMMAND_MAKE_CALL(datetime::DateEntity, date, date) ENTITY_COMMAND_MAKE_CALL(datetime::DateEntity, date, date)
@@ -824,12 +829,12 @@ uint16_t APIConnection::try_send_time_state(EntityBase *entity, APIConnection *c
resp.hour = time->hour; resp.hour = time->hour;
resp.minute = time->minute; resp.minute = time->minute;
resp.second = time->second; resp.second = time->second;
return fill_and_encode_entity_state(time, resp, TimeStateResponse::MESSAGE_TYPE, conn, remaining_size); return fill_and_encode_entity_state(time, resp, conn, remaining_size);
} }
uint16_t APIConnection::try_send_time_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size) { uint16_t APIConnection::try_send_time_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size) {
auto *time = static_cast<datetime::TimeEntity *>(entity); auto *time = static_cast<datetime::TimeEntity *>(entity);
ListEntitiesTimeResponse msg; ListEntitiesTimeResponse msg;
return fill_and_encode_entity_info(time, msg, ListEntitiesTimeResponse::MESSAGE_TYPE, conn, remaining_size); return fill_and_encode_entity_info(time, msg, conn, remaining_size);
} }
void APIConnection::on_time_command_request(const TimeCommandRequest &msg) { void APIConnection::on_time_command_request(const TimeCommandRequest &msg) {
ENTITY_COMMAND_MAKE_CALL(datetime::TimeEntity, time, time) ENTITY_COMMAND_MAKE_CALL(datetime::TimeEntity, time, time)
@@ -851,12 +856,12 @@ uint16_t APIConnection::try_send_datetime_state(EntityBase *entity, APIConnectio
ESPTime state = datetime->state_as_esptime(); ESPTime state = datetime->state_as_esptime();
resp.epoch_seconds = state.timestamp; resp.epoch_seconds = state.timestamp;
} }
return fill_and_encode_entity_state(datetime, resp, DateTimeStateResponse::MESSAGE_TYPE, conn, remaining_size); return fill_and_encode_entity_state(datetime, resp, conn, remaining_size);
} }
uint16_t APIConnection::try_send_datetime_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size) { uint16_t APIConnection::try_send_datetime_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size) {
auto *datetime = static_cast<datetime::DateTimeEntity *>(entity); auto *datetime = static_cast<datetime::DateTimeEntity *>(entity);
ListEntitiesDateTimeResponse msg; ListEntitiesDateTimeResponse msg;
return fill_and_encode_entity_info(datetime, msg, ListEntitiesDateTimeResponse::MESSAGE_TYPE, conn, remaining_size); return fill_and_encode_entity_info(datetime, msg, conn, remaining_size);
} }
void APIConnection::on_date_time_command_request(const DateTimeCommandRequest &msg) { void APIConnection::on_date_time_command_request(const DateTimeCommandRequest &msg) {
ENTITY_COMMAND_MAKE_CALL(datetime::DateTimeEntity, datetime, datetime) ENTITY_COMMAND_MAKE_CALL(datetime::DateTimeEntity, datetime, datetime)
@@ -875,7 +880,7 @@ uint16_t APIConnection::try_send_text_state(EntityBase *entity, APIConnection *c
TextStateResponse resp; TextStateResponse resp;
resp.state = StringRef(text->state); resp.state = StringRef(text->state);
resp.missing_state = !text->has_state(); resp.missing_state = !text->has_state();
return fill_and_encode_entity_state(text, resp, TextStateResponse::MESSAGE_TYPE, conn, remaining_size); return fill_and_encode_entity_state(text, resp, conn, remaining_size);
} }
uint16_t APIConnection::try_send_text_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size) { uint16_t APIConnection::try_send_text_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size) {
@@ -885,11 +890,11 @@ uint16_t APIConnection::try_send_text_info(EntityBase *entity, APIConnection *co
msg.min_length = text->traits.get_min_length(); msg.min_length = text->traits.get_min_length();
msg.max_length = text->traits.get_max_length(); msg.max_length = text->traits.get_max_length();
msg.pattern = text->traits.get_pattern_ref(); msg.pattern = text->traits.get_pattern_ref();
return fill_and_encode_entity_info(text, msg, ListEntitiesTextResponse::MESSAGE_TYPE, conn, remaining_size); return fill_and_encode_entity_info(text, msg, conn, remaining_size);
} }
void APIConnection::on_text_command_request(const TextCommandRequest &msg) { void APIConnection::on_text_command_request(const TextCommandRequest &msg) {
ENTITY_COMMAND_MAKE_CALL(text::Text, text, text) ENTITY_COMMAND_MAKE_CALL(text::Text, text, text)
call.set_value(msg.state); call.set_value(msg.state.c_str(), msg.state.size());
call.perform(); call.perform();
} }
#endif #endif
@@ -904,14 +909,14 @@ uint16_t APIConnection::try_send_select_state(EntityBase *entity, APIConnection
SelectStateResponse resp; SelectStateResponse resp;
resp.state = select->current_option(); resp.state = select->current_option();
resp.missing_state = !select->has_state(); resp.missing_state = !select->has_state();
return fill_and_encode_entity_state(select, resp, SelectStateResponse::MESSAGE_TYPE, conn, remaining_size); return fill_and_encode_entity_state(select, resp, conn, remaining_size);
} }
uint16_t APIConnection::try_send_select_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size) { uint16_t APIConnection::try_send_select_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size) {
auto *select = static_cast<select::Select *>(entity); auto *select = static_cast<select::Select *>(entity);
ListEntitiesSelectResponse msg; ListEntitiesSelectResponse msg;
msg.options = &select->traits.get_options(); msg.options = &select->traits.get_options();
return fill_and_encode_entity_info(select, msg, ListEntitiesSelectResponse::MESSAGE_TYPE, conn, remaining_size); return fill_and_encode_entity_info(select, msg, conn, remaining_size);
} }
void APIConnection::on_select_command_request(const SelectCommandRequest &msg) { void APIConnection::on_select_command_request(const SelectCommandRequest &msg) {
ENTITY_COMMAND_MAKE_CALL(select::Select, select, select) ENTITY_COMMAND_MAKE_CALL(select::Select, select, select)
@@ -924,8 +929,7 @@ void APIConnection::on_select_command_request(const SelectCommandRequest &msg) {
uint16_t APIConnection::try_send_button_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size) { uint16_t APIConnection::try_send_button_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size) {
auto *button = static_cast<button::Button *>(entity); auto *button = static_cast<button::Button *>(entity);
ListEntitiesButtonResponse msg; ListEntitiesButtonResponse msg;
msg.device_class = button->get_device_class_ref(); return fill_and_encode_entity_info_with_device_class(button, msg, msg.device_class, conn, remaining_size);
return fill_and_encode_entity_info(button, msg, ListEntitiesButtonResponse::MESSAGE_TYPE, conn, remaining_size);
} }
void esphome::api::APIConnection::on_button_command_request(const ButtonCommandRequest &msg) { void esphome::api::APIConnection::on_button_command_request(const ButtonCommandRequest &msg) {
ENTITY_COMMAND_GET(button::Button, button, button) ENTITY_COMMAND_GET(button::Button, button, button)
@@ -942,7 +946,7 @@ uint16_t APIConnection::try_send_lock_state(EntityBase *entity, APIConnection *c
auto *a_lock = static_cast<lock::Lock *>(entity); auto *a_lock = static_cast<lock::Lock *>(entity);
LockStateResponse resp; LockStateResponse resp;
resp.state = static_cast<enums::LockState>(a_lock->state); resp.state = static_cast<enums::LockState>(a_lock->state);
return fill_and_encode_entity_state(a_lock, resp, LockStateResponse::MESSAGE_TYPE, conn, remaining_size); return fill_and_encode_entity_state(a_lock, resp, conn, remaining_size);
} }
uint16_t APIConnection::try_send_lock_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size) { uint16_t APIConnection::try_send_lock_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size) {
@@ -951,7 +955,7 @@ uint16_t APIConnection::try_send_lock_info(EntityBase *entity, APIConnection *co
msg.assumed_state = a_lock->traits.get_assumed_state(); msg.assumed_state = a_lock->traits.get_assumed_state();
msg.supports_open = a_lock->traits.get_supports_open(); msg.supports_open = a_lock->traits.get_supports_open();
msg.requires_code = a_lock->traits.get_requires_code(); msg.requires_code = a_lock->traits.get_requires_code();
return fill_and_encode_entity_info(a_lock, msg, ListEntitiesLockResponse::MESSAGE_TYPE, conn, remaining_size); return fill_and_encode_entity_info(a_lock, msg, conn, remaining_size);
} }
void APIConnection::on_lock_command_request(const LockCommandRequest &msg) { void APIConnection::on_lock_command_request(const LockCommandRequest &msg) {
ENTITY_COMMAND_GET(lock::Lock, a_lock, lock) ENTITY_COMMAND_GET(lock::Lock, a_lock, lock)
@@ -979,17 +983,16 @@ uint16_t APIConnection::try_send_valve_state(EntityBase *entity, APIConnection *
ValveStateResponse resp; ValveStateResponse resp;
resp.position = valve->position; resp.position = valve->position;
resp.current_operation = static_cast<enums::ValveOperation>(valve->current_operation); resp.current_operation = static_cast<enums::ValveOperation>(valve->current_operation);
return fill_and_encode_entity_state(valve, resp, ValveStateResponse::MESSAGE_TYPE, conn, remaining_size); return fill_and_encode_entity_state(valve, resp, conn, remaining_size);
} }
uint16_t APIConnection::try_send_valve_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size) { uint16_t APIConnection::try_send_valve_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size) {
auto *valve = static_cast<valve::Valve *>(entity); auto *valve = static_cast<valve::Valve *>(entity);
ListEntitiesValveResponse msg; ListEntitiesValveResponse msg;
auto traits = valve->get_traits(); auto traits = valve->get_traits();
msg.device_class = valve->get_device_class_ref();
msg.assumed_state = traits.get_is_assumed_state(); msg.assumed_state = traits.get_is_assumed_state();
msg.supports_position = traits.get_supports_position(); msg.supports_position = traits.get_supports_position();
msg.supports_stop = traits.get_supports_stop(); msg.supports_stop = traits.get_supports_stop();
return fill_and_encode_entity_info(valve, msg, ListEntitiesValveResponse::MESSAGE_TYPE, conn, remaining_size); return fill_and_encode_entity_info_with_device_class(valve, msg, msg.device_class, conn, remaining_size);
} }
void APIConnection::on_valve_command_request(const ValveCommandRequest &msg) { void APIConnection::on_valve_command_request(const ValveCommandRequest &msg) {
ENTITY_COMMAND_MAKE_CALL(valve::Valve, valve, valve) ENTITY_COMMAND_MAKE_CALL(valve::Valve, valve, valve)
@@ -1015,7 +1018,7 @@ uint16_t APIConnection::try_send_media_player_state(EntityBase *entity, APIConne
resp.state = static_cast<enums::MediaPlayerState>(report_state); resp.state = static_cast<enums::MediaPlayerState>(report_state);
resp.volume = media_player->volume; resp.volume = media_player->volume;
resp.muted = media_player->is_muted(); resp.muted = media_player->is_muted();
return fill_and_encode_entity_state(media_player, resp, MediaPlayerStateResponse::MESSAGE_TYPE, conn, remaining_size); return fill_and_encode_entity_state(media_player, resp, conn, remaining_size);
} }
uint16_t APIConnection::try_send_media_player_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size) { uint16_t APIConnection::try_send_media_player_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size) {
auto *media_player = static_cast<media_player::MediaPlayer *>(entity); auto *media_player = static_cast<media_player::MediaPlayer *>(entity);
@@ -1032,8 +1035,7 @@ uint16_t APIConnection::try_send_media_player_info(EntityBase *entity, APIConnec
media_format.purpose = static_cast<enums::MediaPlayerFormatPurpose>(supported_format.purpose); media_format.purpose = static_cast<enums::MediaPlayerFormatPurpose>(supported_format.purpose);
media_format.sample_bytes = supported_format.sample_bytes; media_format.sample_bytes = supported_format.sample_bytes;
} }
return fill_and_encode_entity_info(media_player, msg, ListEntitiesMediaPlayerResponse::MESSAGE_TYPE, conn, return fill_and_encode_entity_info(media_player, msg, conn, remaining_size);
remaining_size);
} }
void APIConnection::on_media_player_command_request(const MediaPlayerCommandRequest &msg) { void APIConnection::on_media_player_command_request(const MediaPlayerCommandRequest &msg) {
ENTITY_COMMAND_MAKE_CALL(media_player::MediaPlayer, media_player, media_player) ENTITY_COMMAND_MAKE_CALL(media_player::MediaPlayer, media_player, media_player)
@@ -1074,7 +1076,7 @@ void APIConnection::try_send_camera_image_() {
msg.device_id = camera::Camera::instance()->get_device_id(); msg.device_id = camera::Camera::instance()->get_device_id();
#endif #endif
if (!this->send_message_impl(msg, CameraImageResponse::MESSAGE_TYPE)) { if (!this->send_message(msg)) {
return; // Send failed, try again later return; // Send failed, try again later
} }
this->image_reader_->consume_data(to_send); this->image_reader_->consume_data(to_send);
@@ -1100,7 +1102,7 @@ void APIConnection::set_camera_state(std::shared_ptr<camera::CameraImage> image)
uint16_t APIConnection::try_send_camera_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size) { uint16_t APIConnection::try_send_camera_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size) {
auto *camera = static_cast<camera::Camera *>(entity); auto *camera = static_cast<camera::Camera *>(entity);
ListEntitiesCameraResponse msg; ListEntitiesCameraResponse msg;
return fill_and_encode_entity_info(camera, msg, ListEntitiesCameraResponse::MESSAGE_TYPE, conn, remaining_size); return fill_and_encode_entity_info(camera, msg, conn, remaining_size);
} }
void APIConnection::on_camera_image_request(const CameraImageRequest &msg) { void APIConnection::on_camera_image_request(const CameraImageRequest &msg) {
if (camera::Camera::instance() == nullptr) if (camera::Camera::instance() == nullptr)
@@ -1198,6 +1200,9 @@ void APIConnection::on_bluetooth_scanner_set_mode_request(const BluetoothScanner
bluetooth_proxy::global_bluetooth_proxy->bluetooth_scanner_set_mode( bluetooth_proxy::global_bluetooth_proxy->bluetooth_scanner_set_mode(
msg.mode == enums::BluetoothScannerMode::BLUETOOTH_SCANNER_MODE_ACTIVE); msg.mode == enums::BluetoothScannerMode::BLUETOOTH_SCANNER_MODE_ACTIVE);
} }
void APIConnection::on_bluetooth_set_connection_params_request(const BluetoothSetConnectionParamsRequest &msg) {
bluetooth_proxy::global_bluetooth_proxy->bluetooth_set_connection_params(msg);
}
#endif #endif
#ifdef USE_VOICE_ASSISTANT #ifdef USE_VOICE_ASSISTANT
@@ -1255,7 +1260,7 @@ void APIConnection::on_voice_assistant_announce_request(const VoiceAssistantAnno
bool APIConnection::send_voice_assistant_get_configuration_response_(const VoiceAssistantConfigurationRequest &msg) { bool APIConnection::send_voice_assistant_get_configuration_response_(const VoiceAssistantConfigurationRequest &msg) {
VoiceAssistantConfigurationResponse resp; VoiceAssistantConfigurationResponse resp;
if (!this->check_voice_assistant_api_connection_()) { if (!this->check_voice_assistant_api_connection_()) {
return this->send_message(resp, VoiceAssistantConfigurationResponse::MESSAGE_TYPE); return this->send_message(resp);
} }
auto &config = voice_assistant::global_voice_assistant->get_configuration(); auto &config = voice_assistant::global_voice_assistant->get_configuration();
@@ -1287,7 +1292,7 @@ bool APIConnection::send_voice_assistant_get_configuration_response_(const Voice
resp.active_wake_words = &config.active_wake_words; resp.active_wake_words = &config.active_wake_words;
resp.max_active_wake_words = config.max_active_wake_words; resp.max_active_wake_words = config.max_active_wake_words;
return this->send_message(resp, VoiceAssistantConfigurationResponse::MESSAGE_TYPE); return this->send_message(resp);
} }
void APIConnection::on_voice_assistant_configuration_request(const VoiceAssistantConfigurationRequest &msg) { void APIConnection::on_voice_assistant_configuration_request(const VoiceAssistantConfigurationRequest &msg) {
if (!this->send_voice_assistant_get_configuration_response_(msg)) { if (!this->send_voice_assistant_get_configuration_response_(msg)) {
@@ -1322,8 +1327,7 @@ uint16_t APIConnection::try_send_alarm_control_panel_state(EntityBase *entity, A
auto *a_alarm_control_panel = static_cast<alarm_control_panel::AlarmControlPanel *>(entity); auto *a_alarm_control_panel = static_cast<alarm_control_panel::AlarmControlPanel *>(entity);
AlarmControlPanelStateResponse resp; AlarmControlPanelStateResponse resp;
resp.state = static_cast<enums::AlarmControlPanelState>(a_alarm_control_panel->get_state()); resp.state = static_cast<enums::AlarmControlPanelState>(a_alarm_control_panel->get_state());
return fill_and_encode_entity_state(a_alarm_control_panel, resp, AlarmControlPanelStateResponse::MESSAGE_TYPE, conn, return fill_and_encode_entity_state(a_alarm_control_panel, resp, conn, remaining_size);
remaining_size);
} }
uint16_t APIConnection::try_send_alarm_control_panel_info(EntityBase *entity, APIConnection *conn, uint16_t APIConnection::try_send_alarm_control_panel_info(EntityBase *entity, APIConnection *conn,
uint32_t remaining_size) { uint32_t remaining_size) {
@@ -1332,8 +1336,7 @@ uint16_t APIConnection::try_send_alarm_control_panel_info(EntityBase *entity, AP
msg.supported_features = a_alarm_control_panel->get_supported_features(); msg.supported_features = a_alarm_control_panel->get_supported_features();
msg.requires_code = a_alarm_control_panel->get_requires_code(); msg.requires_code = a_alarm_control_panel->get_requires_code();
msg.requires_code_to_arm = a_alarm_control_panel->get_requires_code_to_arm(); msg.requires_code_to_arm = a_alarm_control_panel->get_requires_code_to_arm();
return fill_and_encode_entity_info(a_alarm_control_panel, msg, ListEntitiesAlarmControlPanelResponse::MESSAGE_TYPE, return fill_and_encode_entity_info(a_alarm_control_panel, msg, conn, remaining_size);
conn, remaining_size);
} }
void APIConnection::on_alarm_control_panel_command_request(const AlarmControlPanelCommandRequest &msg) { void APIConnection::on_alarm_control_panel_command_request(const AlarmControlPanelCommandRequest &msg) {
ENTITY_COMMAND_MAKE_CALL(alarm_control_panel::AlarmControlPanel, a_alarm_control_panel, alarm_control_panel) ENTITY_COMMAND_MAKE_CALL(alarm_control_panel::AlarmControlPanel, a_alarm_control_panel, alarm_control_panel)
@@ -1360,7 +1363,7 @@ void APIConnection::on_alarm_control_panel_command_request(const AlarmControlPan
call.pending(); call.pending();
break; break;
} }
call.set_code(msg.code); call.set_code(msg.code.c_str(), msg.code.size());
call.perform(); call.perform();
} }
#endif #endif
@@ -1380,7 +1383,7 @@ uint16_t APIConnection::try_send_water_heater_state(EntityBase *entity, APIConne
resp.target_temperature_high = wh->get_target_temperature_high(); resp.target_temperature_high = wh->get_target_temperature_high();
resp.state = wh->get_state(); resp.state = wh->get_state();
return fill_and_encode_entity_state(wh, resp, WaterHeaterStateResponse::MESSAGE_TYPE, conn, remaining_size); return fill_and_encode_entity_state(wh, resp, conn, remaining_size);
} }
uint16_t APIConnection::try_send_water_heater_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size) { uint16_t APIConnection::try_send_water_heater_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size) {
auto *wh = static_cast<water_heater::WaterHeater *>(entity); auto *wh = static_cast<water_heater::WaterHeater *>(entity);
@@ -1391,7 +1394,7 @@ uint16_t APIConnection::try_send_water_heater_info(EntityBase *entity, APIConnec
msg.target_temperature_step = traits.get_target_temperature_step(); msg.target_temperature_step = traits.get_target_temperature_step();
msg.supported_modes = &traits.get_supported_modes(); msg.supported_modes = &traits.get_supported_modes();
msg.supported_features = traits.get_feature_flags(); msg.supported_features = traits.get_feature_flags();
return fill_and_encode_entity_info(wh, msg, ListEntitiesWaterHeaterResponse::MESSAGE_TYPE, conn, remaining_size); return fill_and_encode_entity_info(wh, msg, conn, remaining_size);
} }
void APIConnection::on_water_heater_command_request(const WaterHeaterCommandRequest &msg) { void APIConnection::on_water_heater_command_request(const WaterHeaterCommandRequest &msg) {
@@ -1427,15 +1430,14 @@ uint16_t APIConnection::try_send_event_response(event::Event *event, StringRef e
uint32_t remaining_size) { uint32_t remaining_size) {
EventResponse resp; EventResponse resp;
resp.event_type = event_type; resp.event_type = event_type;
return fill_and_encode_entity_state(event, resp, EventResponse::MESSAGE_TYPE, conn, remaining_size); return fill_and_encode_entity_state(event, resp, conn, remaining_size);
} }
uint16_t APIConnection::try_send_event_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size) { uint16_t APIConnection::try_send_event_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size) {
auto *event = static_cast<event::Event *>(entity); auto *event = static_cast<event::Event *>(entity);
ListEntitiesEventResponse msg; ListEntitiesEventResponse msg;
msg.device_class = event->get_device_class_ref();
msg.event_types = &event->get_event_types(); msg.event_types = &event->get_event_types();
return fill_and_encode_entity_info(event, msg, ListEntitiesEventResponse::MESSAGE_TYPE, conn, remaining_size); return fill_and_encode_entity_info_with_device_class(event, msg, msg.device_class, conn, remaining_size);
} }
#endif #endif
@@ -1452,9 +1454,90 @@ void APIConnection::on_infrared_rf_transmit_raw_timings_request(const InfraredRF
#endif #endif
} }
void APIConnection::send_infrared_rf_receive_event(const InfraredRFReceiveEvent &msg) { void APIConnection::send_infrared_rf_receive_event(const InfraredRFReceiveEvent &msg) { this->send_message(msg); }
this->send_message(msg, InfraredRFReceiveEvent::MESSAGE_TYPE); #endif
#ifdef USE_SERIAL_PROXY
void APIConnection::on_serial_proxy_configure_request(const SerialProxyConfigureRequest &msg) {
auto &proxies = App.get_serial_proxies();
if (msg.instance >= proxies.size()) {
ESP_LOGW(TAG, "Serial proxy instance %u out of range (max %u)", msg.instance,
static_cast<uint32_t>(proxies.size()));
return;
}
proxies[msg.instance]->configure(msg.baudrate, msg.flow_control, static_cast<uint8_t>(msg.parity), msg.stop_bits,
msg.data_size);
} }
void APIConnection::on_serial_proxy_write_request(const SerialProxyWriteRequest &msg) {
auto &proxies = App.get_serial_proxies();
if (msg.instance >= proxies.size()) {
ESP_LOGW(TAG, "Serial proxy instance %u out of range", msg.instance);
return;
}
proxies[msg.instance]->write_from_client(msg.data, msg.data_len);
}
void APIConnection::on_serial_proxy_set_modem_pins_request(const SerialProxySetModemPinsRequest &msg) {
auto &proxies = App.get_serial_proxies();
if (msg.instance >= proxies.size()) {
ESP_LOGW(TAG, "Serial proxy instance %u out of range", msg.instance);
return;
}
proxies[msg.instance]->set_modem_pins(msg.line_states);
}
void APIConnection::on_serial_proxy_get_modem_pins_request(const SerialProxyGetModemPinsRequest &msg) {
auto &proxies = App.get_serial_proxies();
if (msg.instance >= proxies.size()) {
ESP_LOGW(TAG, "Serial proxy instance %u out of range", msg.instance);
return;
}
SerialProxyGetModemPinsResponse resp{};
resp.instance = msg.instance;
resp.line_states = proxies[msg.instance]->get_modem_pins();
this->send_message(resp);
}
void APIConnection::on_serial_proxy_request(const SerialProxyRequest &msg) {
auto &proxies = App.get_serial_proxies();
if (msg.instance >= proxies.size()) {
ESP_LOGW(TAG, "Serial proxy instance %u out of range", msg.instance);
return;
}
switch (msg.type) {
case enums::SERIAL_PROXY_REQUEST_TYPE_SUBSCRIBE:
case enums::SERIAL_PROXY_REQUEST_TYPE_UNSUBSCRIBE:
proxies[msg.instance]->serial_proxy_request(this, msg.type);
break;
case enums::SERIAL_PROXY_REQUEST_TYPE_FLUSH: {
SerialProxyRequestResponse resp{};
resp.instance = msg.instance;
resp.type = enums::SERIAL_PROXY_REQUEST_TYPE_FLUSH;
switch (proxies[msg.instance]->flush_port()) {
case uart::FlushResult::SUCCESS:
resp.status = enums::SERIAL_PROXY_STATUS_OK;
break;
case uart::FlushResult::ASSUMED_SUCCESS:
resp.status = enums::SERIAL_PROXY_STATUS_ASSUMED_SUCCESS;
break;
case uart::FlushResult::TIMEOUT:
resp.status = enums::SERIAL_PROXY_STATUS_TIMEOUT;
break;
case uart::FlushResult::FAILED:
resp.status = enums::SERIAL_PROXY_STATUS_ERROR;
break;
}
this->send_message(resp);
break;
}
default:
ESP_LOGW(TAG, "Unknown serial proxy request type: %u", static_cast<uint32_t>(msg.type));
break;
}
}
void APIConnection::send_serial_proxy_data(const SerialProxyDataReceived &msg) { this->send_message(msg); }
#endif #endif
#ifdef USE_INFRARED #ifdef USE_INFRARED
@@ -1462,7 +1545,7 @@ uint16_t APIConnection::try_send_infrared_info(EntityBase *entity, APIConnection
auto *infrared = static_cast<infrared::Infrared *>(entity); auto *infrared = static_cast<infrared::Infrared *>(entity);
ListEntitiesInfraredResponse msg; ListEntitiesInfraredResponse msg;
msg.capabilities = infrared->get_capability_flags(); msg.capabilities = infrared->get_capability_flags();
return fill_and_encode_entity_info(infrared, msg, ListEntitiesInfraredResponse::MESSAGE_TYPE, conn, remaining_size); return fill_and_encode_entity_info(infrared, msg, conn, remaining_size);
} }
#endif #endif
@@ -1486,13 +1569,12 @@ uint16_t APIConnection::try_send_update_state(EntityBase *entity, APIConnection
resp.release_summary = StringRef(update->update_info.summary); resp.release_summary = StringRef(update->update_info.summary);
resp.release_url = StringRef(update->update_info.release_url); resp.release_url = StringRef(update->update_info.release_url);
} }
return fill_and_encode_entity_state(update, resp, UpdateStateResponse::MESSAGE_TYPE, conn, remaining_size); return fill_and_encode_entity_state(update, resp, conn, remaining_size);
} }
uint16_t APIConnection::try_send_update_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size) { uint16_t APIConnection::try_send_update_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size) {
auto *update = static_cast<update::UpdateEntity *>(entity); auto *update = static_cast<update::UpdateEntity *>(entity);
ListEntitiesUpdateResponse msg; ListEntitiesUpdateResponse msg;
msg.device_class = update->get_device_class_ref(); return fill_and_encode_entity_info_with_device_class(update, msg, msg.device_class, conn, remaining_size);
return fill_and_encode_entity_info(update, msg, ListEntitiesUpdateResponse::MESSAGE_TYPE, conn, remaining_size);
} }
void APIConnection::on_update_command_request(const UpdateCommandRequest &msg) { void APIConnection::on_update_command_request(const UpdateCommandRequest &msg) {
ENTITY_COMMAND_GET(update::UpdateEntity, update, update) ENTITY_COMMAND_GET(update::UpdateEntity, update, update)
@@ -1518,7 +1600,7 @@ bool APIConnection::try_send_log_message(int level, const char *tag, const char
SubscribeLogsResponse msg; SubscribeLogsResponse msg;
msg.level = static_cast<enums::LogLevel>(level); msg.level = static_cast<enums::LogLevel>(level);
msg.set_message(reinterpret_cast<const uint8_t *>(line), message_len); msg.set_message(reinterpret_cast<const uint8_t *>(line), message_len);
return this->send_message_impl(msg, SubscribeLogsResponse::MESSAGE_TYPE); return this->send_message(msg);
} }
void APIConnection::complete_authentication_() { void APIConnection::complete_authentication_() {
@@ -1575,16 +1657,16 @@ bool APIConnection::send_hello_response_(const HelloRequest &msg) {
// Auto-authenticate - password auth was removed in ESPHome 2026.1.0 // Auto-authenticate - password auth was removed in ESPHome 2026.1.0
this->complete_authentication_(); this->complete_authentication_();
return this->send_message(resp, HelloResponse::MESSAGE_TYPE); return this->send_message(resp);
} }
bool APIConnection::send_ping_response_() { bool APIConnection::send_ping_response_() {
PingResponse resp; PingResponse resp;
return this->send_message(resp, PingResponse::MESSAGE_TYPE); return this->send_message(resp);
} }
bool APIConnection::send_device_info_response_() { bool APIConnection::send_device_info_response_() {
DeviceInfoResponse resp{}; DeviceInfoResponse resp;
resp.name = StringRef(App.get_name()); resp.name = StringRef(App.get_name());
resp.friendly_name = StringRef(App.get_friendly_name()); resp.friendly_name = StringRef(App.get_friendly_name());
#ifdef USE_AREAS #ifdef USE_AREAS
@@ -1679,6 +1761,16 @@ bool APIConnection::send_device_info_response_() {
resp.zwave_proxy_feature_flags = zwave_proxy::global_zwave_proxy->get_feature_flags(); resp.zwave_proxy_feature_flags = zwave_proxy::global_zwave_proxy->get_feature_flags();
resp.zwave_home_id = zwave_proxy::global_zwave_proxy->get_home_id(); resp.zwave_home_id = zwave_proxy::global_zwave_proxy->get_home_id();
#endif #endif
#ifdef USE_SERIAL_PROXY
size_t serial_proxy_index = 0;
for (auto const &proxy : App.get_serial_proxies()) {
if (serial_proxy_index >= SERIAL_PROXY_COUNT)
break;
auto &info = resp.serial_proxies[serial_proxy_index++];
info.name = StringRef(proxy->get_name());
info.port_type = proxy->get_port_type();
}
#endif
#ifdef USE_API_NOISE #ifdef USE_API_NOISE
resp.api_encryption_supported = true; resp.api_encryption_supported = true;
#endif #endif
@@ -1704,7 +1796,7 @@ bool APIConnection::send_device_info_response_() {
} }
#endif #endif
return this->send_message(resp, DeviceInfoResponse::MESSAGE_TYPE); return this->send_message(resp);
} }
void APIConnection::on_hello_request(const HelloRequest &msg) { void APIConnection::on_hello_request(const HelloRequest &msg) {
if (!this->send_hello_response_(msg)) { if (!this->send_hello_response_(msg)) {
@@ -1804,7 +1896,7 @@ void APIConnection::send_execute_service_response(uint32_t call_id, bool success
resp.call_id = call_id; resp.call_id = call_id;
resp.success = success; resp.success = success;
resp.error_message = error_message; resp.error_message = error_message;
this->send_message(resp, ExecuteServiceResponse::MESSAGE_TYPE); this->send_message(resp);
} }
#ifdef USE_API_USER_DEFINED_ACTION_RESPONSES_JSON #ifdef USE_API_USER_DEFINED_ACTION_RESPONSES_JSON
void APIConnection::send_execute_service_response(uint32_t call_id, bool success, StringRef error_message, void APIConnection::send_execute_service_response(uint32_t call_id, bool success, StringRef error_message,
@@ -1815,7 +1907,7 @@ void APIConnection::send_execute_service_response(uint32_t call_id, bool success
resp.error_message = error_message; resp.error_message = error_message;
resp.response_data = response_data; resp.response_data = response_data;
resp.response_data_len = response_data_len; resp.response_data_len = response_data_len;
this->send_message(resp, ExecuteServiceResponse::MESSAGE_TYPE); this->send_message(resp);
} }
#endif // USE_API_USER_DEFINED_ACTION_RESPONSES_JSON #endif // USE_API_USER_DEFINED_ACTION_RESPONSES_JSON
#endif // USE_API_USER_DEFINED_ACTION_RESPONSES #endif // USE_API_USER_DEFINED_ACTION_RESPONSES
@@ -1854,7 +1946,7 @@ bool APIConnection::send_noise_encryption_set_key_response_(const NoiseEncryptio
resp.success = true; resp.success = true;
} }
return this->send_message(resp, NoiseEncryptionSetKeyResponse::MESSAGE_TYPE); return this->send_message(resp);
} }
void APIConnection::on_noise_encryption_set_key_request(const NoiseEncryptionSetKeyRequest &msg) { void APIConnection::on_noise_encryption_set_key_request(const NoiseEncryptionSetKeyRequest &msg) {
if (!this->send_noise_encryption_set_key_response_(msg)) { if (!this->send_noise_encryption_set_key_response_(msg)) {
@@ -1865,11 +1957,7 @@ void APIConnection::on_noise_encryption_set_key_request(const NoiseEncryptionSet
#ifdef USE_API_HOMEASSISTANT_STATES #ifdef USE_API_HOMEASSISTANT_STATES
void APIConnection::on_subscribe_home_assistant_states_request() { state_subs_at_ = 0; } void APIConnection::on_subscribe_home_assistant_states_request() { state_subs_at_ = 0; }
#endif #endif
bool APIConnection::try_to_clear_buffer(bool log_out_of_space) { bool APIConnection::try_to_clear_buffer_slow_(bool log_out_of_space) {
if (this->flags_.remove)
return false;
if (this->helper_->can_write_without_blocking())
return true;
delay(0); delay(0);
APIError err = this->helper_->loop(); APIError err = this->helper_->loop();
if (err != APIError::OK) { if (err != APIError::OK) {
@@ -1883,16 +1971,73 @@ bool APIConnection::try_to_clear_buffer(bool log_out_of_space) {
} }
return false; return false;
} }
bool APIConnection::send_message_impl(const ProtoMessage &msg, uint8_t message_type) { bool APIConnection::send_message_(uint32_t payload_size, uint8_t message_type, MessageEncodeFn encode_fn,
uint32_t payload_size = msg.calculated_size(); const void *msg) {
std::vector<uint8_t> &shared_buf = this->parent_->get_shared_buffer_ref(); #ifdef HAS_PROTO_MESSAGE_DUMP
// Skip dump for log messages (recursive logging risk) and camera frames (high-frequency noise)
if (message_type != SubscribeLogsResponse::MESSAGE_TYPE
#ifdef USE_CAMERA
&& message_type != CameraImageResponse::MESSAGE_TYPE
#endif
) {
auto *proto_msg = static_cast<const ProtoMessage *>(msg);
DumpBuffer dump_buf;
this->log_send_message_(proto_msg->message_name(), proto_msg->dump_to(dump_buf));
}
#endif
auto &shared_buf = this->parent_->get_shared_buffer_ref();
this->prepare_first_message_buffer(shared_buf, payload_size); this->prepare_first_message_buffer(shared_buf, payload_size);
size_t write_start = shared_buf.size(); size_t write_start = shared_buf.size();
shared_buf.resize(write_start + payload_size); shared_buf.resize(write_start + payload_size);
ProtoWriteBuffer buffer{&shared_buf, write_start}; ProtoWriteBuffer buffer{&shared_buf, write_start};
msg.encode(buffer); encode_fn(msg, buffer);
return this->send_buffer(ProtoWriteBuffer{&shared_buf}, message_type); return this->send_buffer(ProtoWriteBuffer{&shared_buf}, message_type);
} }
// Encodes a message to the buffer and returns the total number of bytes used,
// including header and footer overhead. Returns 0 if the message doesn't fit.
uint16_t APIConnection::encode_to_buffer(uint32_t calculated_size, MessageEncodeFn encode_fn, const void *msg,
APIConnection *conn, uint32_t remaining_size) {
#ifdef HAS_PROTO_MESSAGE_DUMP
if (conn->flags_.log_only_mode) {
auto *proto_msg = static_cast<const ProtoMessage *>(msg);
DumpBuffer dump_buf;
conn->log_send_message_(proto_msg->message_name(), proto_msg->dump_to(dump_buf));
return 1;
}
#endif
// Cache frame sizes to avoid repeated virtual calls
const uint8_t header_padding = conn->helper_->frame_header_padding();
const uint8_t footer_size = conn->helper_->frame_footer_size();
// Calculate total size with padding for buffer allocation
size_t total_calculated_size = calculated_size + header_padding + footer_size;
// Check if it fits
if (total_calculated_size > remaining_size)
return 0; // Doesn't fit
auto &shared_buf = conn->parent_->get_shared_buffer_ref();
if (conn->flags_.batch_first_message) {
// First message - buffer already prepared by caller, just clear flag
conn->flags_.batch_first_message = false;
} else {
// Batch message second or later
// Add padding for previous message footer + this message header
size_t current_size = shared_buf.size();
shared_buf.reserve(current_size + total_calculated_size);
shared_buf.resize(current_size + footer_size + header_padding);
}
// Pre-resize buffer to include payload, then encode through raw pointer
size_t write_start = shared_buf.size();
shared_buf.resize(write_start + calculated_size);
ProtoWriteBuffer buffer{&shared_buf, write_start};
encode_fn(msg, buffer);
// Return total size (header + payload + footer)
return static_cast<uint16_t>(header_padding + calculated_size + footer_size);
}
bool APIConnection::send_buffer(ProtoWriteBuffer buffer, uint8_t message_type) { bool APIConnection::send_buffer(ProtoWriteBuffer buffer, uint8_t message_type) {
const bool is_log_message = (message_type == SubscribeLogsResponse::MESSAGE_TYPE); const bool is_log_message = (message_type == SubscribeLogsResponse::MESSAGE_TYPE);
@@ -2039,7 +2184,7 @@ void APIConnection::process_batch_() {
// Separated from process_batch_() so the single-message fast path gets a minimal // Separated from process_batch_() so the single-message fast path gets a minimal
// stack frame without the MAX_MESSAGES_PER_BATCH * sizeof(MessageInfo) array. // stack frame without the MAX_MESSAGES_PER_BATCH * sizeof(MessageInfo) array.
void APIConnection::process_batch_multi_(std::vector<uint8_t> &shared_buf, size_t num_items, uint8_t header_padding, void APIConnection::process_batch_multi_(APIBuffer &shared_buf, size_t num_items, uint8_t header_padding,
uint8_t footer_size) { uint8_t footer_size) {
// Ensure MessageInfo remains trivially destructible for our placement new approach // Ensure MessageInfo remains trivially destructible for our placement new approach
static_assert(std::is_trivially_destructible<MessageInfo>::value, static_assert(std::is_trivially_destructible<MessageInfo>::value,
@@ -2251,17 +2396,17 @@ uint16_t APIConnection::dispatch_message_(const DeferredBatch::BatchItem &item,
uint16_t APIConnection::try_send_list_info_done(EntityBase *entity, APIConnection *conn, uint32_t remaining_size) { uint16_t APIConnection::try_send_list_info_done(EntityBase *entity, APIConnection *conn, uint32_t remaining_size) {
ListEntitiesDoneResponse resp; ListEntitiesDoneResponse resp;
return encode_message_to_buffer(resp, ListEntitiesDoneResponse::MESSAGE_TYPE, conn, remaining_size); return encode_message_to_buffer(resp, conn, remaining_size);
} }
uint16_t APIConnection::try_send_disconnect_request(EntityBase *entity, APIConnection *conn, uint32_t remaining_size) { uint16_t APIConnection::try_send_disconnect_request(EntityBase *entity, APIConnection *conn, uint32_t remaining_size) {
DisconnectRequest req; DisconnectRequest req;
return encode_message_to_buffer(req, DisconnectRequest::MESSAGE_TYPE, conn, remaining_size); return encode_message_to_buffer(req, conn, remaining_size);
} }
uint16_t APIConnection::try_send_ping_request(EntityBase *entity, APIConnection *conn, uint32_t remaining_size) { uint16_t APIConnection::try_send_ping_request(EntityBase *entity, APIConnection *conn, uint32_t remaining_size) {
PingRequest req; PingRequest req;
return encode_message_to_buffer(req, PingRequest::MESSAGE_TYPE, conn, remaining_size); return encode_message_to_buffer(req, conn, remaining_size);
} }
#ifdef USE_API_HOMEASSISTANT_STATES #ifdef USE_API_HOMEASSISTANT_STATES
@@ -2280,7 +2425,7 @@ void APIConnection::process_state_subscriptions_() {
resp.attribute = it.attribute != nullptr ? StringRef(it.attribute) : StringRef(""); resp.attribute = it.attribute != nullptr ? StringRef(it.attribute) : StringRef("");
resp.once = it.once; resp.once = it.once;
if (this->send_message(resp, SubscribeHomeAssistantStateResponse::MESSAGE_TYPE)) { if (this->send_message(resp)) {
this->state_subs_at_++; this->state_subs_at_++;
} }
} }
+106 -46
View File
@@ -3,6 +3,12 @@
#include "esphome/core/defines.h" #include "esphome/core/defines.h"
#ifdef USE_API #ifdef USE_API
#include "api_frame_helper.h" #include "api_frame_helper.h"
#ifdef USE_API_NOISE
#include "api_frame_helper_noise.h"
#endif
#ifdef USE_API_PLAINTEXT
#include "api_frame_helper_plaintext.h"
#endif
#include "api_pb2.h" #include "api_pb2.h"
#include "api_pb2_service.h" #include "api_pb2_service.h"
#include "api_server.h" #include "api_server.h"
@@ -123,7 +129,7 @@ class APIConnection final : public APIServerConnectionBase {
void send_homeassistant_action(const HomeassistantActionRequest &call) { void send_homeassistant_action(const HomeassistantActionRequest &call) {
if (!this->flags_.service_call_subscription) if (!this->flags_.service_call_subscription)
return; return;
this->send_message(call, HomeassistantActionRequest::MESSAGE_TYPE); this->send_message(call);
} }
#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES #ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES
void on_homeassistant_action_response(const HomeassistantActionResponse &msg) override; void on_homeassistant_action_response(const HomeassistantActionResponse &msg) override;
@@ -142,12 +148,13 @@ class APIConnection final : public APIServerConnectionBase {
void on_bluetooth_gatt_notify_request(const BluetoothGATTNotifyRequest &msg) override; void on_bluetooth_gatt_notify_request(const BluetoothGATTNotifyRequest &msg) override;
void on_subscribe_bluetooth_connections_free_request() override; void on_subscribe_bluetooth_connections_free_request() override;
void on_bluetooth_scanner_set_mode_request(const BluetoothScannerSetModeRequest &msg) override; void on_bluetooth_scanner_set_mode_request(const BluetoothScannerSetModeRequest &msg) override;
void on_bluetooth_set_connection_params_request(const BluetoothSetConnectionParamsRequest &msg) override;
#endif #endif
#ifdef USE_HOMEASSISTANT_TIME #ifdef USE_HOMEASSISTANT_TIME
void send_time_request() { void send_time_request() {
GetTimeRequest req; GetTimeRequest req;
this->send_message(req, GetTimeRequest::MESSAGE_TYPE); this->send_message(req);
} }
#endif #endif
@@ -182,6 +189,15 @@ class APIConnection final : public APIServerConnectionBase {
void send_infrared_rf_receive_event(const InfraredRFReceiveEvent &msg); void send_infrared_rf_receive_event(const InfraredRFReceiveEvent &msg);
#endif #endif
#ifdef USE_SERIAL_PROXY
void on_serial_proxy_configure_request(const SerialProxyConfigureRequest &msg) override;
void on_serial_proxy_write_request(const SerialProxyWriteRequest &msg) override;
void on_serial_proxy_set_modem_pins_request(const SerialProxySetModemPinsRequest &msg) override;
void on_serial_proxy_get_modem_pins_request(const SerialProxyGetModemPinsRequest &msg) override;
void on_serial_proxy_request(const SerialProxyRequest &msg) override;
void send_serial_proxy_data(const SerialProxyDataReceived &msg);
#endif
#ifdef USE_EVENT #ifdef USE_EVENT
void send_event(event::Event *event); void send_event(event::Event *event);
#endif #endif
@@ -247,6 +263,7 @@ class APIConnection final : public APIServerConnectionBase {
return static_cast<ConnectionState>(this->flags_.connection_state) == ConnectionState::CONNECTED || return static_cast<ConnectionState>(this->flags_.connection_state) == ConnectionState::CONNECTED ||
this->is_authenticated(); this->is_authenticated();
} }
bool is_marked_for_removal() const { return this->flags_.remove; }
uint8_t get_log_subscription_level() const { return this->flags_.log_subscription; } uint8_t get_log_subscription_level() const { return this->flags_.log_subscription; }
// Get client API version for feature detection // Get client API version for feature detection
@@ -257,9 +274,21 @@ class APIConnection final : public APIServerConnectionBase {
void on_fatal_error() override; void on_fatal_error() override;
void on_no_setup_connection() override; void on_no_setup_connection() override;
bool send_message_impl(const ProtoMessage &msg, uint8_t message_type) override;
void prepare_first_message_buffer(std::vector<uint8_t> &shared_buf, size_t header_padding, size_t total_size) { // Function pointer type for type-erased message encoding
using MessageEncodeFn = void (*)(const void *, ProtoWriteBuffer &);
// Function pointer type for type-erased size calculation
using CalculateSizeFn = uint32_t (*)(const void *);
template<typename T> bool send_message(const T &msg) {
if constexpr (T::ESTIMATED_SIZE == 0) {
return this->send_message_(0, T::MESSAGE_TYPE, &encode_msg_noop, &msg);
} else {
return this->send_message_(msg.calculate_size(), T::MESSAGE_TYPE, &proto_encode_msg<T>, &msg);
}
}
void prepare_first_message_buffer(APIBuffer &shared_buf, size_t header_padding, size_t total_size) {
shared_buf.clear(); shared_buf.clear();
// Reserve space for header padding + message + footer // Reserve space for header padding + message + footer
// - Header padding: space for protocol headers (7 bytes for Noise, 6 for Plaintext) // - Header padding: space for protocol headers (7 bytes for Noise, 6 for Plaintext)
@@ -270,13 +299,19 @@ class APIConnection final : public APIServerConnectionBase {
} }
// Convenience overload - computes frame overhead internally // Convenience overload - computes frame overhead internally
void prepare_first_message_buffer(std::vector<uint8_t> &shared_buf, size_t payload_size) { void prepare_first_message_buffer(APIBuffer &shared_buf, size_t payload_size) {
const uint8_t header_padding = this->helper_->frame_header_padding(); const uint8_t header_padding = this->helper_->frame_header_padding();
const uint8_t footer_size = this->helper_->frame_footer_size(); const uint8_t footer_size = this->helper_->frame_footer_size();
this->prepare_first_message_buffer(shared_buf, header_padding, payload_size + header_padding + footer_size); this->prepare_first_message_buffer(shared_buf, header_padding, payload_size + header_padding + footer_size);
} }
bool try_to_clear_buffer(bool log_out_of_space); bool try_to_clear_buffer(bool log_out_of_space) {
if (this->flags_.remove)
return false;
if (this->helper_->can_write_without_blocking())
return true;
return this->try_to_clear_buffer_slow_(log_out_of_space);
}
bool send_buffer(ProtoWriteBuffer buffer, uint8_t message_type) override; bool send_buffer(ProtoWriteBuffer buffer, uint8_t message_type) override;
const char *get_name() const { return this->helper_->get_client_name(); } const char *get_name() const { return this->helper_->get_client_name(); }
@@ -286,6 +321,8 @@ class APIConnection final : public APIServerConnectionBase {
} }
protected: protected:
bool try_to_clear_buffer_slow_(bool log_out_of_space);
// Helper function to handle authentication completion // Helper function to handle authentication completion
void complete_authentication_(); void complete_authentication_();
@@ -312,50 +349,67 @@ class APIConnection final : public APIServerConnectionBase {
void process_state_subscriptions_(); void process_state_subscriptions_();
#endif #endif
// Non-template helper to encode any ProtoMessage // Size thunk — converts void* back to concrete type for direct calculate_size() call
static uint16_t encode_message_to_buffer(ProtoMessage &msg, uint8_t message_type, APIConnection *conn, template<typename T> static uint32_t calc_size(const void *msg) {
uint32_t remaining_size); return static_cast<const T *>(msg)->calculate_size();
// Helper to fill entity state base and encode message
static uint16_t fill_and_encode_entity_state(EntityBase *entity, StateResponseProtoMessage &msg, uint8_t message_type,
APIConnection *conn, uint32_t remaining_size) {
msg.key = entity->get_object_id_hash();
#ifdef USE_DEVICES
msg.device_id = entity->get_device_id();
#endif
return encode_message_to_buffer(msg, message_type, conn, remaining_size);
} }
// Helper to fill entity info base and encode message // Shared no-op encode thunk for empty messages (ESTIMATED_SIZE == 0)
static uint16_t fill_and_encode_entity_info(EntityBase *entity, InfoResponseProtoMessage &msg, uint8_t message_type, static void encode_msg_noop(const void *, ProtoWriteBuffer &) {}
APIConnection *conn, uint32_t remaining_size) {
// Set common fields that are shared by all entity types
msg.key = entity->get_object_id_hash();
// API 1.14+ clients compute object_id client-side from the entity name // Non-template buffer management for send_message
// For older clients, we must send object_id for backward compatibility bool send_message_(uint32_t payload_size, uint8_t message_type, MessageEncodeFn encode_fn, const void *msg);
// See: https://github.com/esphome/backlog/issues/76
// TODO: Remove this backward compat code before 2026.7.0 - all clients should support API 1.14 by then // Non-template buffer management for batch encoding
// Buffer must remain in scope until encode_message_to_buffer is called static uint16_t encode_to_buffer(uint32_t calculated_size, MessageEncodeFn encode_fn, const void *msg,
char object_id_buf[OBJECT_ID_MAX_LEN]; APIConnection *conn, uint32_t remaining_size);
if (!conn->client_supports_api_version(1, 14)) {
msg.object_id = entity->get_object_id_to(object_id_buf); // Thin template wrapper — computes size, delegates buffer work to non-template helper
template<typename T> static uint16_t encode_message_to_buffer(T &msg, APIConnection *conn, uint32_t remaining_size) {
if constexpr (T::ESTIMATED_SIZE == 0) {
return encode_to_buffer(0, &encode_msg_noop, &msg, conn, remaining_size);
} else {
return encode_to_buffer(msg.calculate_size(), &proto_encode_msg<T>, &msg, conn, remaining_size);
} }
}
if (entity->has_own_name()) { // Non-template core — fills state fields and encodes
msg.name = entity->get_name(); static uint16_t fill_and_encode_entity_state(EntityBase *entity, StateResponseProtoMessage &msg,
} CalculateSizeFn size_fn, MessageEncodeFn encode_fn, APIConnection *conn,
uint32_t remaining_size);
// Set common EntityBase properties // Thin template wrapper
#ifdef USE_ENTITY_ICON template<typename T>
msg.icon = entity->get_icon_ref(); static uint16_t fill_and_encode_entity_state(EntityBase *entity, T &msg, APIConnection *conn,
#endif uint32_t remaining_size) {
msg.disabled_by_default = entity->is_disabled_by_default(); return fill_and_encode_entity_state(entity, msg, &calc_size<T>, &proto_encode_msg<T>, conn, remaining_size);
msg.entity_category = static_cast<enums::EntityCategory>(entity->get_entity_category()); }
#ifdef USE_DEVICES
msg.device_id = entity->get_device_id(); // Non-template core — fills info fields, allocates buffers, and encodes
#endif static uint16_t fill_and_encode_entity_info(EntityBase *entity, InfoResponseProtoMessage &msg,
return encode_message_to_buffer(msg, message_type, conn, remaining_size); CalculateSizeFn size_fn, MessageEncodeFn encode_fn, APIConnection *conn,
uint32_t remaining_size);
// Thin template wrapper
template<typename T>
static uint16_t fill_and_encode_entity_info(EntityBase *entity, T &msg, APIConnection *conn,
uint32_t remaining_size) {
return fill_and_encode_entity_info(entity, msg, &calc_size<T>, &proto_encode_msg<T>, conn, remaining_size);
}
// Non-template core — fills device_class, then delegates to fill_and_encode_entity_info
static uint16_t fill_and_encode_entity_info_with_device_class(EntityBase *entity, InfoResponseProtoMessage &msg,
StringRef &device_class_field, CalculateSizeFn size_fn,
MessageEncodeFn encode_fn, APIConnection *conn,
uint32_t remaining_size);
// Thin template wrapper
template<typename T>
static uint16_t fill_and_encode_entity_info_with_device_class(EntityBase *entity, T &msg,
StringRef &device_class_field, APIConnection *conn,
uint32_t remaining_size) {
return fill_and_encode_entity_info_with_device_class(entity, msg, device_class_field, &calc_size<T>,
&proto_encode_msg<T>, conn, remaining_size);
} }
#ifdef USE_VOICE_ASSISTANT #ifdef USE_VOICE_ASSISTANT
@@ -489,7 +543,13 @@ class APIConnection final : public APIServerConnectionBase {
// === Optimal member ordering for 32-bit systems === // === Optimal member ordering for 32-bit systems ===
// Group 1: Pointers (4 bytes each on 32-bit) // Group 1: Pointers (4 bytes each on 32-bit)
#if defined(USE_API_NOISE) && defined(USE_API_PLAINTEXT)
std::unique_ptr<APIFrameHelper> helper_; std::unique_ptr<APIFrameHelper> helper_;
#elif defined(USE_API_NOISE)
std::unique_ptr<APINoiseFrameHelper> helper_;
#elif defined(USE_API_PLAINTEXT)
std::unique_ptr<APIPlaintextFrameHelper> helper_;
#endif
APIServer *parent_; APIServer *parent_;
// Group 2: Iterator union (saves ~16 bytes vs separate iterators) // Group 2: Iterator union (saves ~16 bytes vs separate iterators)
@@ -627,8 +687,8 @@ class APIConnection final : public APIServerConnectionBase {
bool schedule_batch_(); bool schedule_batch_();
void process_batch_(); void process_batch_();
void process_batch_multi_(std::vector<uint8_t> &shared_buf, size_t num_items, uint8_t header_padding, void process_batch_multi_(APIBuffer &shared_buf, size_t num_items, uint8_t header_padding, uint8_t footer_size)
uint8_t footer_size) __attribute__((noinline)); __attribute__((noinline));
void clear_batch_() { void clear_batch_() {
this->deferred_batch_.clear(); this->deferred_batch_.clear();
this->flags_.batch_scheduled = false; this->flags_.batch_scheduled = false;
+14 -7
View File
@@ -5,10 +5,10 @@
#include <memory> #include <memory>
#include <span> #include <span>
#include <utility> #include <utility>
#include <vector>
#include "esphome/core/defines.h" #include "esphome/core/defines.h"
#ifdef USE_API #ifdef USE_API
#include "esphome/components/api/api_buffer.h"
#include "esphome/components/socket/socket.h" #include "esphome/components/socket/socket.h"
#include "esphome/core/application.h" #include "esphome/core/application.h"
#include "esphome/core/log.h" #include "esphome/core/log.h"
@@ -178,8 +178,7 @@ class APIFrameHelper {
// rx_buf_len_ tracks bytes read so far; if non-zero, we're mid-frame // rx_buf_len_ tracks bytes read so far; if non-zero, we're mid-frame
// and clearing would lose partially received data. // and clearing would lose partially received data.
if (this->rx_buf_len_ == 0) { if (this->rx_buf_len_ == 0) {
// Use swap trick since shrink_to_fit() is non-binding and may be ignored this->rx_buf_.release();
std::vector<uint8_t>().swap(this->rx_buf_);
} }
} }
@@ -206,9 +205,6 @@ class APIFrameHelper {
// Common socket write error handling // Common socket write error handling
APIError handle_socket_write_error_(); 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);
// Socket ownership (4 bytes on 32-bit, 8 bytes on 64-bit) // Socket ownership (4 bytes on 32-bit, 8 bytes on 64-bit)
std::unique_ptr<socket::Socket> socket_; std::unique_ptr<socket::Socket> socket_;
@@ -232,9 +228,20 @@ class APIFrameHelper {
EXPLICIT_REJECT = 8, // Noise only EXPLICIT_REJECT = 8, // Noise only
}; };
// Fast inline state check for read_packet/write_protobuf_messages hot path.
// Returns OK only in DATA state; maps CLOSED/FAILED to BAD_STATE and any
// other intermediate state to WOULD_BLOCK.
inline APIError ESPHOME_ALWAYS_INLINE check_data_state_() const {
if (this->state_ == State::DATA)
return APIError::OK;
if (this->state_ == State::CLOSED || this->state_ == State::FAILED)
return APIError::BAD_STATE;
return APIError::WOULD_BLOCK;
}
// Containers (size varies, but typically 12+ bytes on 32-bit) // Containers (size varies, but typically 12+ bytes on 32-bit)
std::array<std::unique_ptr<SendBuffer>, API_MAX_SEND_QUEUE> tx_buf_; std::array<std::unique_ptr<SendBuffer>, API_MAX_SEND_QUEUE> tx_buf_;
std::vector<uint8_t> rx_buf_; APIBuffer rx_buf_;
// Client name buffer - stores name from Hello message or initial peername // Client name buffer - stores name from Hello message or initial peername
char client_name_[CLIENT_INFO_NAME_MAX_LEN]{}; char client_name_[CLIENT_INFO_NAME_MAX_LEN]{};
@@ -207,9 +207,7 @@ APIError APINoiseFrameHelper::try_read_frame_() {
// During handshake, rx_buf_.size() is used in prologue construction, so // During handshake, rx_buf_.size() is used in prologue construction, so
// the buffer must be exactly msg_size to avoid prologue mismatch.) // the buffer must be exactly msg_size to avoid prologue mismatch.)
uint16_t alloc_size = msg_size + (is_data ? RX_BUF_NULL_TERMINATOR : 0); uint16_t alloc_size = msg_size + (is_data ? RX_BUF_NULL_TERMINATOR : 0);
if (this->rx_buf_.size() != alloc_size) { this->rx_buf_.resize(alloc_size);
this->rx_buf_.resize(alloc_size);
}
if (rx_buf_len_ < msg_size) { if (rx_buf_len_ < msg_size) {
// more data to read // more data to read
@@ -269,7 +267,7 @@ APIError APINoiseFrameHelper::state_action_() {
} }
if (state_ == State::SERVER_HELLO) { if (state_ == State::SERVER_HELLO) {
// send server hello // send server hello
const std::string &name = App.get_name(); const auto &name = App.get_name();
char mac[MAC_ADDRESS_BUFFER_SIZE]; char mac[MAC_ADDRESS_BUFFER_SIZE];
get_mac_address_into_buffer(mac); get_mac_address_into_buffer(mac);
@@ -375,6 +373,7 @@ void APINoiseFrameHelper::send_explicit_handshake_reject_(const LogString *reaso
#ifdef USE_STORE_LOG_STR_IN_FLASH #ifdef USE_STORE_LOG_STR_IN_FLASH
// On ESP8266 with flash strings, we need to use PROGMEM-aware functions // On ESP8266 with flash strings, we need to use PROGMEM-aware functions
size_t reason_len = strlen_P(reinterpret_cast<PGM_P>(reason)); size_t reason_len = strlen_P(reinterpret_cast<PGM_P>(reason));
reason_len = std::min(reason_len, sizeof(data) - 1);
if (reason_len > 0) { if (reason_len > 0) {
memcpy_P(data + 1, reinterpret_cast<PGM_P>(reason), reason_len); memcpy_P(data + 1, reinterpret_cast<PGM_P>(reason), reason_len);
} }
@@ -382,6 +381,7 @@ void APINoiseFrameHelper::send_explicit_handshake_reject_(const LogString *reaso
// Normal memory access // Normal memory access
const char *reason_str = LOG_STR_ARG(reason); const char *reason_str = LOG_STR_ARG(reason);
size_t reason_len = strlen(reason_str); size_t reason_len = strlen(reason_str);
reason_len = std::min(reason_len, sizeof(data) - 1);
if (reason_len > 0) { if (reason_len > 0) {
// NOLINTNEXTLINE(bugprone-not-null-terminated-result) - binary protocol, not a C string // NOLINTNEXTLINE(bugprone-not-null-terminated-result) - binary protocol, not a C string
std::memcpy(data + 1, reason_str, reason_len); std::memcpy(data + 1, reason_str, reason_len);
@@ -397,14 +397,9 @@ void APINoiseFrameHelper::send_explicit_handshake_reject_(const LogString *reaso
state_ = orig_state; state_ = orig_state;
} }
APIError APINoiseFrameHelper::read_packet(ReadPacketBuffer *buffer) { APIError APINoiseFrameHelper::read_packet(ReadPacketBuffer *buffer) {
APIError aerr = this->state_action_(); APIError aerr = this->check_data_state_();
if (aerr != APIError::OK) { if (aerr != APIError::OK)
return aerr; return aerr;
}
if (this->state_ != State::DATA) {
return APIError::WOULD_BLOCK;
}
aerr = this->try_read_frame_(); aerr = this->try_read_frame_();
if (aerr != APIError::OK) if (aerr != APIError::OK)
@@ -461,14 +456,9 @@ APIError APINoiseFrameHelper::write_protobuf_packet(uint8_t type, ProtoWriteBuff
} }
APIError APINoiseFrameHelper::write_protobuf_messages(ProtoWriteBuffer buffer, std::span<const MessageInfo> messages) { APIError APINoiseFrameHelper::write_protobuf_messages(ProtoWriteBuffer buffer, std::span<const MessageInfo> messages) {
APIError aerr = state_action_(); APIError aerr = this->check_data_state_();
if (aerr != APIError::OK) { if (aerr != APIError::OK)
return aerr; return aerr;
}
if (state_ != State::DATA) {
return APIError::WOULD_BLOCK;
}
if (messages.empty()) { if (messages.empty()) {
return APIError::OK; return APIError::OK;
@@ -579,8 +569,7 @@ APIError APINoiseFrameHelper::init_handshake_() {
if (aerr != APIError::OK) if (aerr != APIError::OK)
return aerr; return aerr;
// set_prologue copies it into handshakestate, so we can get rid of it now // set_prologue copies it into handshakestate, so we can get rid of it now
// Use swap idiom to actually release memory (= {} only clears size, not capacity) prologue_.release();
std::vector<uint8_t>().swap(prologue_);
err = noise_handshakestate_start(handshake_); err = noise_handshakestate_start(handshake_);
aerr = handle_noise_error_(err, LOG_STR("noise_handshakestate_start"), APIError::HANDSHAKESTATE_SETUP_FAILED); aerr = handle_noise_error_(err, LOG_STR("noise_handshakestate_start"), APIError::HANDSHAKESTATE_SETUP_FAILED);
@@ -43,8 +43,8 @@ class APINoiseFrameHelper final : public APIFrameHelper {
// Reference to noise context (4 bytes on 32-bit) // Reference to noise context (4 bytes on 32-bit)
APINoiseContext &ctx_; APINoiseContext &ctx_;
// Vector (12 bytes on 32-bit) // Buffer for noise handshake prologue (released after handshake)
std::vector<uint8_t> prologue_; APIBuffer prologue_;
// NoiseProtocolId (size depends on implementation) // NoiseProtocolId (size depends on implementation)
NoiseProtocolId nid_; NoiseProtocolId nid_;
@@ -128,46 +128,44 @@ APIError APIPlaintextFrameHelper::try_read_frame_() {
// Skip indicator byte at position 0 // Skip indicator byte at position 0
uint8_t varint_pos = 1; 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); // rx_header_buf_pos_ >= 3 and varint_pos == 1, so len >= 2
auto msg_size_varint = ProtoVarInt::parse_non_empty(&rx_header_buf_[varint_pos], rx_header_buf_pos_ - varint_pos);
if (!msg_size_varint.has_value()) { if (!msg_size_varint.has_value()) {
// not enough data there yet // not enough data there yet
continue; continue;
} }
if (msg_size_varint->as_uint32() > MAX_MESSAGE_SIZE) { if (msg_size_varint.value > MAX_MESSAGE_SIZE) {
state_ = State::FAILED; state_ = State::FAILED;
HELPER_LOG("Bad packet: message size %" PRIu32 " exceeds maximum %u", msg_size_varint->as_uint32(), HELPER_LOG("Bad packet: message size %" PRIu32 " exceeds maximum %u",
MAX_MESSAGE_SIZE); static_cast<uint32_t>(msg_size_varint.value), MAX_MESSAGE_SIZE);
return APIError::BAD_DATA_PACKET; return APIError::BAD_DATA_PACKET;
} }
rx_header_parsed_len_ = msg_size_varint->as_uint16(); rx_header_parsed_len_ = static_cast<uint16_t>(msg_size_varint.value);
// Move to next varint position // Move to next varint position
varint_pos += consumed; varint_pos += msg_size_varint.consumed;
auto msg_type_varint = ProtoVarInt::parse(&rx_header_buf_[varint_pos], rx_header_buf_pos_ - varint_pos, &consumed); auto msg_type_varint = ProtoVarInt::parse(&rx_header_buf_[varint_pos], rx_header_buf_pos_ - varint_pos);
if (!msg_type_varint.has_value()) { if (!msg_type_varint.has_value()) {
// not enough data there yet // not enough data there yet
continue; continue;
} }
if (msg_type_varint->as_uint32() > std::numeric_limits<uint16_t>::max()) { if (msg_type_varint.value > std::numeric_limits<uint16_t>::max()) {
state_ = State::FAILED; state_ = State::FAILED;
HELPER_LOG("Bad packet: message type %" PRIu32 " exceeds maximum %u", msg_type_varint->as_uint32(), HELPER_LOG("Bad packet: message type %" PRIu32 " exceeds maximum %u",
std::numeric_limits<uint16_t>::max()); static_cast<uint32_t>(msg_type_varint.value), std::numeric_limits<uint16_t>::max());
return APIError::BAD_DATA_PACKET; return APIError::BAD_DATA_PACKET;
} }
rx_header_parsed_type_ = msg_type_varint->as_uint16(); rx_header_parsed_type_ = static_cast<uint16_t>(msg_type_varint.value);
rx_header_parsed_ = true; rx_header_parsed_ = true;
} }
// header reading done // header reading done
// Reserve space for body (+ null terminator so protobuf StringRef fields // Reserve space for body (+ null terminator so protobuf StringRef fields
// can be safely null-terminated in-place after decode) // can be safely null-terminated in-place after decode)
if (this->rx_buf_.size() != this->rx_header_parsed_len_ + RX_BUF_NULL_TERMINATOR) { this->rx_buf_.resize(this->rx_header_parsed_len_ + RX_BUF_NULL_TERMINATOR);
this->rx_buf_.resize(this->rx_header_parsed_len_ + RX_BUF_NULL_TERMINATOR);
}
if (rx_buf_len_ < rx_header_parsed_len_) { if (rx_buf_len_ < rx_header_parsed_len_) {
// more data to read // more data to read
@@ -195,11 +193,11 @@ APIError APIPlaintextFrameHelper::try_read_frame_() {
} }
APIError APIPlaintextFrameHelper::read_packet(ReadPacketBuffer *buffer) { APIError APIPlaintextFrameHelper::read_packet(ReadPacketBuffer *buffer) {
if (this->state_ != State::DATA) { APIError aerr = this->check_data_state_();
return APIError::WOULD_BLOCK; if (aerr != APIError::OK)
} return aerr;
APIError aerr = this->try_read_frame_(); aerr = this->try_read_frame_();
if (aerr != APIError::OK) { if (aerr != APIError::OK) {
if (aerr == APIError::BAD_INDICATOR) { if (aerr == APIError::BAD_INDICATOR) {
// Make sure to tell the remote that we don't // Make sure to tell the remote that we don't
@@ -244,9 +242,9 @@ APIError APIPlaintextFrameHelper::write_protobuf_packet(uint8_t type, ProtoWrite
APIError APIPlaintextFrameHelper::write_protobuf_messages(ProtoWriteBuffer buffer, APIError APIPlaintextFrameHelper::write_protobuf_messages(ProtoWriteBuffer buffer,
std::span<const MessageInfo> messages) { std::span<const MessageInfo> messages) {
if (state_ != State::DATA) { APIError aerr = this->check_data_state_();
return APIError::BAD_STATE; if (aerr != APIError::OK)
} return aerr;
if (messages.empty()) { if (messages.empty()) {
return APIError::OK; return APIError::OK;
+6
View File
@@ -90,4 +90,10 @@ extend google.protobuf.FieldOptions {
// - uint16_t <field>_length_{0}; // - uint16_t <field>_length_{0};
// - uint16_t <field>_count_{0}; // - uint16_t <field>_count_{0};
optional bool packed_buffer = 50015 [default=false]; optional bool packed_buffer = 50015 [default=false];
// force: Always encode this field, even when its value equals the proto3 default.
// Skips the zero/empty check in calculate_size() and encode(), using the _force
// variant of the calc_ method. Use on fields that are almost always non-default
// to eliminate dead branches on hot paths.
optional bool force = 50016 [default=false];
} }
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+141
View File
@@ -100,6 +100,18 @@ static void dump_bytes_field(DumpBuffer &out, const char *field_name, const uint
out.append(hex_buf).append("\n"); out.append(hex_buf).append("\n");
} }
template<> const char *proto_enum_to_string<enums::SerialProxyPortType>(enums::SerialProxyPortType value) {
switch (value) {
case enums::SERIAL_PROXY_PORT_TYPE_TTL:
return "SERIAL_PROXY_PORT_TYPE_TTL";
case enums::SERIAL_PROXY_PORT_TYPE_RS232:
return "SERIAL_PROXY_PORT_TYPE_RS232";
case enums::SERIAL_PROXY_PORT_TYPE_RS485:
return "SERIAL_PROXY_PORT_TYPE_RS485";
default:
return "UNKNOWN";
}
}
template<> const char *proto_enum_to_string<enums::EntityCategory>(enums::EntityCategory value) { template<> const char *proto_enum_to_string<enums::EntityCategory>(enums::EntityCategory value) {
switch (value) { switch (value) {
case enums::ENTITY_CATEGORY_NONE: case enums::ENTITY_CATEGORY_NONE:
@@ -752,6 +764,48 @@ template<> const char *proto_enum_to_string<enums::ZWaveProxyRequestType>(enums:
} }
} }
#endif #endif
#ifdef USE_SERIAL_PROXY
template<> const char *proto_enum_to_string<enums::SerialProxyParity>(enums::SerialProxyParity value) {
switch (value) {
case enums::SERIAL_PROXY_PARITY_NONE:
return "SERIAL_PROXY_PARITY_NONE";
case enums::SERIAL_PROXY_PARITY_EVEN:
return "SERIAL_PROXY_PARITY_EVEN";
case enums::SERIAL_PROXY_PARITY_ODD:
return "SERIAL_PROXY_PARITY_ODD";
default:
return "UNKNOWN";
}
}
template<> const char *proto_enum_to_string<enums::SerialProxyRequestType>(enums::SerialProxyRequestType value) {
switch (value) {
case enums::SERIAL_PROXY_REQUEST_TYPE_SUBSCRIBE:
return "SERIAL_PROXY_REQUEST_TYPE_SUBSCRIBE";
case enums::SERIAL_PROXY_REQUEST_TYPE_UNSUBSCRIBE:
return "SERIAL_PROXY_REQUEST_TYPE_UNSUBSCRIBE";
case enums::SERIAL_PROXY_REQUEST_TYPE_FLUSH:
return "SERIAL_PROXY_REQUEST_TYPE_FLUSH";
default:
return "UNKNOWN";
}
}
template<> const char *proto_enum_to_string<enums::SerialProxyStatus>(enums::SerialProxyStatus value) {
switch (value) {
case enums::SERIAL_PROXY_STATUS_OK:
return "SERIAL_PROXY_STATUS_OK";
case enums::SERIAL_PROXY_STATUS_ASSUMED_SUCCESS:
return "SERIAL_PROXY_STATUS_ASSUMED_SUCCESS";
case enums::SERIAL_PROXY_STATUS_ERROR:
return "SERIAL_PROXY_STATUS_ERROR";
case enums::SERIAL_PROXY_STATUS_TIMEOUT:
return "SERIAL_PROXY_STATUS_TIMEOUT";
case enums::SERIAL_PROXY_STATUS_NOT_SUPPORTED:
return "SERIAL_PROXY_STATUS_NOT_SUPPORTED";
default:
return "UNKNOWN";
}
}
#endif
const char *HelloRequest::dump_to(DumpBuffer &out) const { const char *HelloRequest::dump_to(DumpBuffer &out) const {
MessageDumpHelper helper(out, "HelloRequest"); MessageDumpHelper helper(out, "HelloRequest");
@@ -801,6 +855,14 @@ const char *DeviceInfo::dump_to(DumpBuffer &out) const {
return out.c_str(); return out.c_str();
} }
#endif #endif
#ifdef USE_SERIAL_PROXY
const char *SerialProxyInfo::dump_to(DumpBuffer &out) const {
MessageDumpHelper helper(out, "SerialProxyInfo");
dump_field(out, "name", this->name);
dump_field(out, "port_type", static_cast<enums::SerialProxyPortType>(this->port_type));
return out.c_str();
}
#endif
const char *DeviceInfoResponse::dump_to(DumpBuffer &out) const { const char *DeviceInfoResponse::dump_to(DumpBuffer &out) const {
MessageDumpHelper helper(out, "DeviceInfoResponse"); MessageDumpHelper helper(out, "DeviceInfoResponse");
dump_field(out, "name", this->name); dump_field(out, "name", this->name);
@@ -861,6 +923,13 @@ const char *DeviceInfoResponse::dump_to(DumpBuffer &out) const {
#endif #endif
#ifdef USE_ZWAVE_PROXY #ifdef USE_ZWAVE_PROXY
dump_field(out, "zwave_home_id", this->zwave_home_id); dump_field(out, "zwave_home_id", this->zwave_home_id);
#endif
#ifdef USE_SERIAL_PROXY
for (const auto &it : this->serial_proxies) {
out.append(" serial_proxies: ");
it.dump_to(out);
out.append("\n");
}
#endif #endif
return out.c_str(); return out.c_str();
} }
@@ -2509,6 +2578,78 @@ const char *InfraredRFReceiveEvent::dump_to(DumpBuffer &out) const {
return out.c_str(); return out.c_str();
} }
#endif #endif
#ifdef USE_SERIAL_PROXY
const char *SerialProxyConfigureRequest::dump_to(DumpBuffer &out) const {
MessageDumpHelper helper(out, "SerialProxyConfigureRequest");
dump_field(out, "instance", this->instance);
dump_field(out, "baudrate", this->baudrate);
dump_field(out, "flow_control", this->flow_control);
dump_field(out, "parity", static_cast<enums::SerialProxyParity>(this->parity));
dump_field(out, "stop_bits", this->stop_bits);
dump_field(out, "data_size", this->data_size);
return out.c_str();
}
const char *SerialProxyDataReceived::dump_to(DumpBuffer &out) const {
MessageDumpHelper helper(out, "SerialProxyDataReceived");
dump_field(out, "instance", this->instance);
dump_bytes_field(out, "data", this->data_ptr_, this->data_len_);
return out.c_str();
}
const char *SerialProxyWriteRequest::dump_to(DumpBuffer &out) const {
MessageDumpHelper helper(out, "SerialProxyWriteRequest");
dump_field(out, "instance", this->instance);
dump_bytes_field(out, "data", this->data, this->data_len);
return out.c_str();
}
const char *SerialProxySetModemPinsRequest::dump_to(DumpBuffer &out) const {
MessageDumpHelper helper(out, "SerialProxySetModemPinsRequest");
dump_field(out, "instance", this->instance);
dump_field(out, "line_states", this->line_states);
return out.c_str();
}
const char *SerialProxyGetModemPinsRequest::dump_to(DumpBuffer &out) const {
MessageDumpHelper helper(out, "SerialProxyGetModemPinsRequest");
dump_field(out, "instance", this->instance);
return out.c_str();
}
const char *SerialProxyGetModemPinsResponse::dump_to(DumpBuffer &out) const {
MessageDumpHelper helper(out, "SerialProxyGetModemPinsResponse");
dump_field(out, "instance", this->instance);
dump_field(out, "line_states", this->line_states);
return out.c_str();
}
const char *SerialProxyRequest::dump_to(DumpBuffer &out) const {
MessageDumpHelper helper(out, "SerialProxyRequest");
dump_field(out, "instance", this->instance);
dump_field(out, "type", static_cast<enums::SerialProxyRequestType>(this->type));
return out.c_str();
}
const char *SerialProxyRequestResponse::dump_to(DumpBuffer &out) const {
MessageDumpHelper helper(out, "SerialProxyRequestResponse");
dump_field(out, "instance", this->instance);
dump_field(out, "type", static_cast<enums::SerialProxyRequestType>(this->type));
dump_field(out, "status", static_cast<enums::SerialProxyStatus>(this->status));
dump_field(out, "error_message", this->error_message);
return out.c_str();
}
#endif
#ifdef USE_BLUETOOTH_PROXY
const char *BluetoothSetConnectionParamsRequest::dump_to(DumpBuffer &out) const {
MessageDumpHelper helper(out, "BluetoothSetConnectionParamsRequest");
dump_field(out, "address", this->address);
dump_field(out, "min_interval", this->min_interval);
dump_field(out, "max_interval", this->max_interval);
dump_field(out, "latency", this->latency);
dump_field(out, "timeout", this->timeout);
return out.c_str();
}
const char *BluetoothSetConnectionParamsResponse::dump_to(DumpBuffer &out) const {
MessageDumpHelper helper(out, "BluetoothSetConnectionParamsResponse");
dump_field(out, "address", this->address);
dump_field(out, "error", this->error);
return out.c_str();
}
#endif
} // namespace esphome::api } // namespace esphome::api
@@ -634,6 +634,72 @@ void APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type,
this->on_infrared_rf_transmit_raw_timings_request(msg); this->on_infrared_rf_transmit_raw_timings_request(msg);
break; break;
} }
#endif
#ifdef USE_SERIAL_PROXY
case SerialProxyConfigureRequest::MESSAGE_TYPE: {
SerialProxyConfigureRequest msg;
msg.decode(msg_data, msg_size);
#ifdef HAS_PROTO_MESSAGE_DUMP
this->log_receive_message_(LOG_STR("on_serial_proxy_configure_request"), msg);
#endif
this->on_serial_proxy_configure_request(msg);
break;
}
#endif
#ifdef USE_SERIAL_PROXY
case SerialProxyWriteRequest::MESSAGE_TYPE: {
SerialProxyWriteRequest msg;
msg.decode(msg_data, msg_size);
#ifdef HAS_PROTO_MESSAGE_DUMP
this->log_receive_message_(LOG_STR("on_serial_proxy_write_request"), msg);
#endif
this->on_serial_proxy_write_request(msg);
break;
}
#endif
#ifdef USE_SERIAL_PROXY
case SerialProxySetModemPinsRequest::MESSAGE_TYPE: {
SerialProxySetModemPinsRequest msg;
msg.decode(msg_data, msg_size);
#ifdef HAS_PROTO_MESSAGE_DUMP
this->log_receive_message_(LOG_STR("on_serial_proxy_set_modem_pins_request"), msg);
#endif
this->on_serial_proxy_set_modem_pins_request(msg);
break;
}
#endif
#ifdef USE_SERIAL_PROXY
case SerialProxyGetModemPinsRequest::MESSAGE_TYPE: {
SerialProxyGetModemPinsRequest msg;
msg.decode(msg_data, msg_size);
#ifdef HAS_PROTO_MESSAGE_DUMP
this->log_receive_message_(LOG_STR("on_serial_proxy_get_modem_pins_request"), msg);
#endif
this->on_serial_proxy_get_modem_pins_request(msg);
break;
}
#endif
#ifdef USE_SERIAL_PROXY
case SerialProxyRequest::MESSAGE_TYPE: {
SerialProxyRequest msg;
msg.decode(msg_data, msg_size);
#ifdef HAS_PROTO_MESSAGE_DUMP
this->log_receive_message_(LOG_STR("on_serial_proxy_request"), msg);
#endif
this->on_serial_proxy_request(msg);
break;
}
#endif
#ifdef USE_BLUETOOTH_PROXY
case BluetoothSetConnectionParamsRequest::MESSAGE_TYPE: {
BluetoothSetConnectionParamsRequest msg;
msg.decode(msg_data, msg_size);
#ifdef HAS_PROTO_MESSAGE_DUMP
this->log_receive_message_(LOG_STR("on_bluetooth_set_connection_params_request"), msg);
#endif
this->on_bluetooth_set_connection_params_request(msg);
break;
}
#endif #endif
default: default:
break; break;
+22 -8
View File
@@ -19,14 +19,6 @@ class APIServerConnectionBase : public ProtoService {
public: public:
#endif #endif
bool send_message(const ProtoMessage &msg, uint8_t message_type) {
#ifdef HAS_PROTO_MESSAGE_DUMP
DumpBuffer dump_buf;
this->log_send_message_(msg.message_name(), msg.dump_to(dump_buf));
#endif
return this->send_message_impl(msg, message_type);
}
virtual void on_hello_request(const HelloRequest &value){}; virtual void on_hello_request(const HelloRequest &value){};
virtual void on_disconnect_request(){}; virtual void on_disconnect_request(){};
@@ -224,6 +216,28 @@ class APIServerConnectionBase : public ProtoService {
virtual void on_infrared_rf_transmit_raw_timings_request(const InfraredRFTransmitRawTimingsRequest &value){}; virtual void on_infrared_rf_transmit_raw_timings_request(const InfraredRFTransmitRawTimingsRequest &value){};
#endif #endif
#ifdef USE_SERIAL_PROXY
virtual void on_serial_proxy_configure_request(const SerialProxyConfigureRequest &value){};
#endif
#ifdef USE_SERIAL_PROXY
virtual void on_serial_proxy_write_request(const SerialProxyWriteRequest &value){};
#endif
#ifdef USE_SERIAL_PROXY
virtual void on_serial_proxy_set_modem_pins_request(const SerialProxySetModemPinsRequest &value){};
#endif
#ifdef USE_SERIAL_PROXY
virtual void on_serial_proxy_get_modem_pins_request(const SerialProxyGetModemPinsRequest &value){};
#endif
#ifdef USE_SERIAL_PROXY
virtual void on_serial_proxy_request(const SerialProxyRequest &value){};
#endif
#ifdef USE_BLUETOOTH_PROXY
virtual void on_bluetooth_set_connection_params_request(const BluetoothSetConnectionParamsRequest &value){};
#endif
protected: protected:
void read_message(uint32_t msg_size, uint32_t msg_type, const uint8_t *msg_data) override; void read_message(uint32_t msg_size, uint32_t msg_type, const uint8_t *msg_data) override;
}; };
+5 -9
View File
@@ -359,11 +359,11 @@ void APIServer::on_update(update::UpdateEntity *obj) {
#endif #endif
#ifdef USE_ZWAVE_PROXY #ifdef USE_ZWAVE_PROXY
void APIServer::on_zwave_proxy_request(const esphome::api::ProtoMessage &msg) { void APIServer::on_zwave_proxy_request(const ZWaveProxyRequest &msg) {
// We could add code to manage a second subscription type, but, since this message type is // We could add code to manage a second subscription type, but, since this message type is
// very infrequent and small, we simply send it to all clients // very infrequent and small, we simply send it to all clients
for (auto &c : this->clients_) for (auto &c : this->clients_)
c->send_message(msg, api::ZWaveProxyRequest::MESSAGE_TYPE); c->send_message(msg);
} }
#endif #endif
@@ -531,7 +531,7 @@ bool APIServer::update_noise_psk_(const SavedNoisePsk &new_psk, const LogString
this->set_noise_psk(active_psk); this->set_noise_psk(active_psk);
for (auto &c : this->clients_) { for (auto &c : this->clients_) {
DisconnectRequest req; DisconnectRequest req;
c->send_message(req, DisconnectRequest::MESSAGE_TYPE); c->send_message(req);
} }
}); });
} }
@@ -582,11 +582,7 @@ void APIServer::request_time() {
} }
#endif #endif
bool APIServer::is_connected(bool state_subscription_only) const { bool APIServer::is_connected_with_state_subscription() const {
if (!state_subscription_only) {
return !this->clients_.empty();
}
for (const auto &client : this->clients_) { for (const auto &client : this->clients_) {
if (client->flags_.state_subscription) { if (client->flags_.state_subscription) {
return true; return true;
@@ -631,7 +627,7 @@ void APIServer::on_shutdown() {
// Send disconnect requests to all connected clients // Send disconnect requests to all connected clients
for (auto &c : this->clients_) { for (auto &c : this->clients_) {
DisconnectRequest req; DisconnectRequest req;
if (!c->send_message(req, DisconnectRequest::MESSAGE_TYPE)) { if (!c->send_message(req)) {
// If we can't send the disconnect request directly (tx_buffer full), // If we can't send the disconnect request directly (tx_buffer full),
// schedule it at the front of the batch so it will be sent with priority // schedule it at the front of the batch so it will be sent with priority
c->schedule_message_front_(nullptr, DisconnectRequest::MESSAGE_TYPE, DisconnectRequest::ESTIMATED_SIZE); c->schedule_message_front_(nullptr, DisconnectRequest::MESSAGE_TYPE, DisconnectRequest::ESTIMATED_SIZE);
+11 -6
View File
@@ -2,6 +2,7 @@
#include "esphome/core/defines.h" #include "esphome/core/defines.h"
#ifdef USE_API #ifdef USE_API
#include "api_buffer.h"
#include "api_noise_context.h" #include "api_noise_context.h"
#include "api_pb2.h" #include "api_pb2.h"
#include "api_pb2_service.h" #include "api_pb2_service.h"
@@ -65,7 +66,7 @@ class APIServer : public Component,
void set_max_connections(uint8_t max_connections) { this->max_connections_ = max_connections; } void set_max_connections(uint8_t max_connections) { this->max_connections_ = max_connections; }
// Get reference to shared buffer for API connections // Get reference to shared buffer for API connections
std::vector<uint8_t> &get_shared_buffer_ref() { return shared_write_buffer_; } APIBuffer &get_shared_buffer_ref() { return shared_write_buffer_; }
#ifdef USE_API_NOISE #ifdef USE_API_NOISE
bool save_noise_psk(psk_t psk, bool make_active = true); bool save_noise_psk(psk_t psk, bool make_active = true);
@@ -179,13 +180,14 @@ class APIServer : public Component,
void on_update(update::UpdateEntity *obj) override; void on_update(update::UpdateEntity *obj) override;
#endif #endif
#ifdef USE_ZWAVE_PROXY #ifdef USE_ZWAVE_PROXY
void on_zwave_proxy_request(const esphome::api::ProtoMessage &msg); void on_zwave_proxy_request(const ZWaveProxyRequest &msg);
#endif #endif
#ifdef USE_IR_RF #ifdef USE_IR_RF
void send_infrared_rf_receive_event(uint32_t device_id, uint32_t key, const std::vector<int32_t> *timings); void send_infrared_rf_receive_event(uint32_t device_id, uint32_t key, const std::vector<int32_t> *timings);
#endif #endif
bool is_connected(bool state_subscription_only = false) const; bool is_connected() const { return !this->clients_.empty(); }
bool is_connected_with_state_subscription() const;
#ifdef USE_API_HOMEASSISTANT_STATES #ifdef USE_API_HOMEASSISTANT_STATES
struct HomeAssistantStateSubscription { struct HomeAssistantStateSubscription {
@@ -257,7 +259,7 @@ class APIServer : public Component,
} }
void socket_failed_(const LogString *msg); void socket_failed_(const LogString *msg);
// Pointers and pointer-like types first (4 bytes each) // Pointers and pointer-like types first (4 bytes each)
socket::Socket *socket_{nullptr}; socket::ListenSocket *socket_{nullptr};
#ifdef USE_API_CLIENT_CONNECTED_TRIGGER #ifdef USE_API_CLIENT_CONNECTED_TRIGGER
Trigger<std::string, std::string> client_connected_trigger_; Trigger<std::string, std::string> client_connected_trigger_;
#endif #endif
@@ -275,7 +277,7 @@ class APIServer : public Component,
// Not pre-allocated: all send paths call prepare_first_message_buffer() which // Not pre-allocated: all send paths call prepare_first_message_buffer() which
// reserves the exact needed size. Pre-allocating here would cause heap fragmentation // reserves the exact needed size. Pre-allocating here would cause heap fragmentation
// since the buffer would almost always reallocate on first use. // since the buffer would almost always reallocate on first use.
std::vector<uint8_t> shared_write_buffer_; APIBuffer shared_write_buffer_;
#ifdef USE_API_HOMEASSISTANT_STATES #ifdef USE_API_HOMEASSISTANT_STATES
std::vector<HomeAssistantStateSubscription> state_subs_; std::vector<HomeAssistantStateSubscription> state_subs_;
#endif #endif
@@ -323,7 +325,10 @@ template<typename... Ts> class APIConnectedCondition : public Condition<Ts...> {
TEMPLATABLE_VALUE(bool, state_subscription_only) TEMPLATABLE_VALUE(bool, state_subscription_only)
public: public:
bool check(const Ts &...x) override { bool check(const Ts &...x) override {
return global_api_server->is_connected(this->state_subscription_only_.value(x...)); if (this->state_subscription_only_.value(x...)) {
return global_api_server->is_connected_with_state_subscription();
}
return global_api_server->is_connected();
} }
}; };
+43 -3
View File
@@ -130,6 +130,20 @@ template<typename... Ts> class HomeAssistantServiceCallAction : public Action<Ts
this->add_kv_(this->variables_, key, std::forward<V>(value)); this->add_kv_(this->variables_, key, std::forward<V>(value));
} }
#ifdef USE_ESP8266
// On ESP8266, ESPHOME_F() returns __FlashStringHelper* (PROGMEM pointer).
// Store as const char* — populate_service_map copies from PROGMEM at play() time.
template<typename V> void add_data(const __FlashStringHelper *key, V &&value) {
this->add_kv_(this->data_, reinterpret_cast<const char *>(key), std::forward<V>(value));
}
template<typename V> void add_data_template(const __FlashStringHelper *key, V &&value) {
this->add_kv_(this->data_template_, reinterpret_cast<const char *>(key), std::forward<V>(value));
}
template<typename V> void add_variable(const __FlashStringHelper *key, V &&value) {
this->add_kv_(this->variables_, reinterpret_cast<const char *>(key), std::forward<V>(value));
}
#endif
#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES #ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES
template<typename T> void set_response_template(T response_template) { template<typename T> void set_response_template(T response_template) {
this->response_template_ = response_template; this->response_template_ = response_template;
@@ -221,7 +235,32 @@ template<typename... Ts> class HomeAssistantServiceCallAction : public Action<Ts
Ts... x) { Ts... x) {
dest.init(source.size()); dest.init(source.size());
// Count non-static strings to allocate exact storage needed #ifdef USE_ESP8266
// On ESP8266, all static strings from codegen are FLASH_STRING (PROGMEM),
// so is_static_string() is always false — the zero-copy STATIC_STRING fast
// path from the non-ESP8266 branch cannot trigger. We copy all keys and
// values unconditionally: keys via _P functions (may be in PROGMEM), values
// via value() which handles FLASH_STRING internally.
value_storage.init(source.size() * 2);
for (auto &it : source) {
auto &kv = dest.emplace_back();
// Key: copy from possible PROGMEM
{
size_t key_len = strlen_P(it.key);
value_storage.push_back(std::string(key_len, '\0'));
memcpy_P(value_storage.back().data(), it.key, key_len);
kv.key = StringRef(value_storage.back());
}
// Value: value() handles FLASH_STRING via _P functions internally
value_storage.push_back(it.value.value(x...));
kv.value = StringRef(value_storage.back());
}
#else
// On non-ESP8266, strings are directly readable from flash-mapped memory.
// Count non-static strings to allocate exact storage needed.
size_t lambda_count = 0; size_t lambda_count = 0;
for (const auto &it : source) { for (const auto &it : source) {
if (!it.value.is_static_string()) { if (!it.value.is_static_string()) {
@@ -235,14 +274,15 @@ template<typename... Ts> class HomeAssistantServiceCallAction : public Action<Ts
kv.key = StringRef(it.key); kv.key = StringRef(it.key);
if (it.value.is_static_string()) { if (it.value.is_static_string()) {
// Static string from YAML - zero allocation // Static string — pointer directly readable, zero allocation
kv.value = StringRef(it.value.get_static_string()); kv.value = StringRef(it.value.get_static_string());
} else { } else {
// Lambda evaluation - store result, reference it // Lambda — evaluate and store result
value_storage.push_back(it.value.value(x...)); value_storage.push_back(it.value.value(x...));
kv.value = StringRef(value_storage.back()); kv.value = StringRef(value_storage.back());
} }
} }
#endif
} }
APIServer *parent_; APIServer *parent_;
+1 -1
View File
@@ -94,7 +94,7 @@ ListEntitiesIterator::ListEntitiesIterator(APIConnection *client) : client_(clie
#ifdef USE_API_USER_DEFINED_ACTIONS #ifdef USE_API_USER_DEFINED_ACTIONS
bool ListEntitiesIterator::on_service(UserServiceDescriptor *service) { bool ListEntitiesIterator::on_service(UserServiceDescriptor *service) {
auto resp = service->encode_list_service_response(); auto resp = service->encode_list_service_response();
return this->client_->send_message(resp, ListEntitiesServicesResponse::MESSAGE_TYPE); return this->client_->send_message(resp);
} }
#endif #endif
+129 -29
View File
@@ -1,5 +1,6 @@
#include "proto.h" #include "proto.h"
#include <cinttypes> #include <cinttypes>
#include <cstring>
#include "esphome/core/helpers.h" #include "esphome/core/helpers.h"
#include "esphome/core/log.h" #include "esphome/core/log.h"
@@ -7,20 +8,52 @@ namespace esphome::api {
static const char *const TAG = "api.proto"; static const char *const TAG = "api.proto";
uint32_t ProtoSize::varint_slow(uint32_t value) { return varint_wide(value); }
void ProtoWriteBuffer::encode_varint_raw_slow_(uint32_t value) {
do {
this->debug_check_bounds_(1);
*this->pos_++ = static_cast<uint8_t>(value | 0x80);
value >>= 7;
} while (value > 0x7F);
this->debug_check_bounds_(1);
*this->pos_++ = static_cast<uint8_t>(value);
}
ProtoVarIntResult ProtoVarInt::parse_slow(const uint8_t *buffer, uint32_t len) {
// Multi-byte varint: first byte already checked to have high bit set
uint32_t result32 = buffer[0] & 0x7F;
#ifdef USE_API_VARINT64 #ifdef USE_API_VARINT64
optional<ProtoVarInt> ProtoVarInt::parse_wide(const uint8_t *buffer, uint32_t len, uint32_t *consumed, uint32_t limit = std::min(len, uint32_t(4));
uint32_t result32) { #else
uint32_t limit = std::min(len, uint32_t(5));
#endif
for (uint32_t i = 1; i < limit; i++) {
uint8_t val = buffer[i];
result32 |= uint32_t(val & 0x7F) << (i * 7);
if ((val & 0x80) == 0) {
return {result32, i + 1};
}
}
#ifdef USE_API_VARINT64
return parse_wide(buffer, len, result32);
#else
return {0, PROTO_VARINT_PARSE_FAILED};
#endif
}
#ifdef USE_API_VARINT64
ProtoVarIntResult ProtoVarInt::parse_wide(const uint8_t *buffer, uint32_t len, uint32_t result32) {
uint64_t result64 = result32; uint64_t result64 = result32;
uint32_t limit = std::min(len, uint32_t(10)); uint32_t limit = std::min(len, uint32_t(10));
for (uint32_t i = 4; i < limit; i++) { for (uint32_t i = 4; i < limit; i++) {
uint8_t val = buffer[i]; uint8_t val = buffer[i];
result64 |= uint64_t(val & 0x7F) << (i * 7); result64 |= uint64_t(val & 0x7F) << (i * 7);
if ((val & 0x80) == 0) { if ((val & 0x80) == 0) {
*consumed = i + 1; return {result64, i + 1};
return ProtoVarInt(result64);
} }
} }
return {}; return {0, PROTO_VARINT_PARSE_FAILED};
} }
#endif #endif
@@ -30,18 +63,16 @@ uint32_t ProtoDecodableMessage::count_repeated_field(const uint8_t *buffer, size
const uint8_t *end = buffer + length; const uint8_t *end = buffer + length;
while (ptr < end) { while (ptr < end) {
uint32_t consumed; // Parse field header (tag) - ptr < end guarantees len >= 1
auto res = ProtoVarInt::parse_non_empty(ptr, end - ptr);
// Parse field header (tag)
auto res = ProtoVarInt::parse(ptr, end - ptr, &consumed);
if (!res.has_value()) { if (!res.has_value()) {
break; // Invalid data, stop counting break; // Invalid data, stop counting
} }
uint32_t tag = res->as_uint32(); uint32_t tag = static_cast<uint32_t>(res.value);
uint32_t field_type = tag & WIRE_TYPE_MASK; uint32_t field_type = tag & WIRE_TYPE_MASK;
uint32_t field_id = tag >> 3; uint32_t field_id = tag >> 3;
ptr += consumed; ptr += res.consumed;
// Count if this is the target field // Count if this is the target field
if (field_id == target_field_id) { if (field_id == target_field_id) {
@@ -51,20 +82,20 @@ uint32_t ProtoDecodableMessage::count_repeated_field(const uint8_t *buffer, size
// Skip field data based on wire type // Skip field data based on wire type
switch (field_type) { switch (field_type) {
case WIRE_TYPE_VARINT: { // VarInt - parse and skip case WIRE_TYPE_VARINT: { // VarInt - parse and skip
res = ProtoVarInt::parse(ptr, end - ptr, &consumed); res = ProtoVarInt::parse(ptr, end - ptr);
if (!res.has_value()) { if (!res.has_value()) {
return count; // Invalid data, return what we have return count; // Invalid data, return what we have
} }
ptr += consumed; ptr += res.consumed;
break; break;
} }
case WIRE_TYPE_LENGTH_DELIMITED: { // Length-delimited - parse length and skip data case WIRE_TYPE_LENGTH_DELIMITED: { // Length-delimited - parse length and skip data
res = ProtoVarInt::parse(ptr, end - ptr, &consumed); res = ProtoVarInt::parse(ptr, end - ptr);
if (!res.has_value()) { if (!res.has_value()) {
return count; return count;
} }
uint32_t field_length = res->as_uint32(); uint32_t field_length = static_cast<uint32_t>(res.value);
ptr += consumed; ptr += res.consumed;
if (field_length > static_cast<size_t>(end - ptr)) { if (field_length > static_cast<size_t>(end - ptr)) {
return count; // Out of bounds return count; // Out of bounds
} }
@@ -87,6 +118,76 @@ uint32_t ProtoDecodableMessage::count_repeated_field(const uint8_t *buffer, size
return count; return count;
} }
// Single-pass encode for repeated submessage elements (non-template core).
// Writes field tag, reserves 1 byte for length varint, encodes the submessage body,
// then backpatches the actual length. For the common case (body < 128 bytes), this is
// just a single byte write with no memmove — all current repeated submessage types
// (BLE advertisements at ~47B, GATT descriptors at ~24B, service args, etc.) take
// this fast path.
//
// The memmove fallback for body >= 128 bytes exists only for correctness (e.g., a GATT
// characteristic with many descriptors). It is safe because calculate_size() already
// reserved space for the full multi-byte varint — the shift fills that reserved space:
//
// calculate_size() allocates per element: tag + varint_size(body) + body_size
//
// After encode, before memmove (1 byte reserved, body written):
// [tag][__][body ..... body][??]
// ^ ^-- unused byte (v2 space from calculate_size)
// len_pos
//
// After memmove(body_start+1, body_start, body_size):
// [tag][__][__][body ..... body]
// ^ ^-- body shifted forward, fills v2 space exactly
// len_pos
//
// After writing 2-byte varint at len_pos:
// [tag][v1][v2][body ..... body]
// ^-- pos_ = element end, within buffer
void ProtoWriteBuffer::encode_sub_message(uint32_t field_id, const void *value,
void (*encode_fn)(const void *, ProtoWriteBuffer &)) {
this->encode_field_raw(field_id, 2);
// Reserve 1 byte for length varint (optimistic: submessage < 128 bytes)
uint8_t *len_pos = this->pos_;
this->debug_check_bounds_(1);
this->pos_++;
uint8_t *body_start = this->pos_;
encode_fn(value, *this);
uint32_t body_size = static_cast<uint32_t>(this->pos_ - body_start);
if (body_size < 128) [[likely]] {
// Common case: 1-byte varint, just backpatch
*len_pos = static_cast<uint8_t>(body_size);
return;
}
// Compute extra bytes needed for varint beyond the 1 already reserved
uint8_t extra = ProtoSize::varint(body_size) - 1;
// Shift body forward to make room for the extra varint bytes
this->debug_check_bounds_(extra);
std::memmove(body_start + extra, body_start, body_size);
uint8_t *end = this->pos_ + extra;
// Write the full varint at len_pos
this->pos_ = len_pos;
this->encode_varint_raw(body_size);
this->pos_ = end;
}
// Non-template core for encode_optional_sub_message.
void ProtoWriteBuffer::encode_optional_sub_message(uint32_t field_id, uint32_t nested_size, const void *value,
void (*encode_fn)(const void *, ProtoWriteBuffer &)) {
if (nested_size == 0)
return;
this->encode_field_raw(field_id, 2);
this->encode_varint_raw(nested_size);
#ifdef ESPHOME_DEBUG_API
uint8_t *start = this->pos_;
encode_fn(value, *this);
if (static_cast<uint32_t>(this->pos_ - start) != nested_size)
this->debug_check_encode_size_(field_id, nested_size, this->pos_ - start);
#else
encode_fn(value, *this);
#endif
}
#ifdef ESPHOME_DEBUG_API #ifdef ESPHOME_DEBUG_API
void ProtoWriteBuffer::debug_check_bounds_(size_t bytes, const char *caller) { void ProtoWriteBuffer::debug_check_bounds_(size_t bytes, const char *caller) {
if (this->pos_ + bytes > this->buffer_->data() + this->buffer_->size()) { if (this->pos_ + bytes > this->buffer_->data() + this->buffer_->size()) {
@@ -107,41 +208,40 @@ void ProtoDecodableMessage::decode(const uint8_t *buffer, size_t length) {
const uint8_t *end = buffer + length; const uint8_t *end = buffer + length;
while (ptr < end) { while (ptr < end) {
uint32_t consumed; // Parse field header - ptr < end guarantees len >= 1
auto res = ProtoVarInt::parse_non_empty(ptr, end - ptr);
// Parse field header
auto res = ProtoVarInt::parse(ptr, end - ptr, &consumed);
if (!res.has_value()) { if (!res.has_value()) {
ESP_LOGV(TAG, "Invalid field start at offset %ld", (long) (ptr - buffer)); ESP_LOGV(TAG, "Invalid field start at offset %ld", (long) (ptr - buffer));
return; return;
} }
uint32_t tag = res->as_uint32(); uint32_t tag = static_cast<uint32_t>(res.value);
uint32_t field_type = tag & WIRE_TYPE_MASK; uint32_t field_type = tag & WIRE_TYPE_MASK;
uint32_t field_id = tag >> 3; uint32_t field_id = tag >> 3;
ptr += consumed; ptr += res.consumed;
switch (field_type) { switch (field_type) {
case WIRE_TYPE_VARINT: { // VarInt case WIRE_TYPE_VARINT: { // VarInt
res = ProtoVarInt::parse(ptr, end - ptr, &consumed); res = ProtoVarInt::parse(ptr, end - ptr);
if (!res.has_value()) { if (!res.has_value()) {
ESP_LOGV(TAG, "Invalid VarInt at offset %ld", (long) (ptr - buffer)); ESP_LOGV(TAG, "Invalid VarInt at offset %ld", (long) (ptr - buffer));
return; return;
} }
if (!this->decode_varint(field_id, *res)) { if (!this->decode_varint(field_id, res.value)) {
ESP_LOGV(TAG, "Cannot decode VarInt field %" PRIu32 " with value %" PRIu32 "!", field_id, res->as_uint32()); ESP_LOGV(TAG, "Cannot decode VarInt field %" PRIu32 " with value %" PRIu64 "!", field_id,
static_cast<uint64_t>(res.value));
} }
ptr += consumed; ptr += res.consumed;
break; break;
} }
case WIRE_TYPE_LENGTH_DELIMITED: { // Length-delimited case WIRE_TYPE_LENGTH_DELIMITED: { // Length-delimited
res = ProtoVarInt::parse(ptr, end - ptr, &consumed); res = ProtoVarInt::parse(ptr, end - ptr);
if (!res.has_value()) { if (!res.has_value()) {
ESP_LOGV(TAG, "Invalid Length Delimited at offset %ld", (long) (ptr - buffer)); ESP_LOGV(TAG, "Invalid Length Delimited at offset %ld", (long) (ptr - buffer));
return; return;
} }
uint32_t field_length = res->as_uint32(); uint32_t field_length = static_cast<uint32_t>(res.value);
ptr += consumed; ptr += res.consumed;
if (field_length > static_cast<size_t>(end - ptr)) { if (field_length > static_cast<size_t>(end - ptr)) {
ESP_LOGV(TAG, "Out-of-bounds Length Delimited at offset %ld", (long) (ptr - buffer)); ESP_LOGV(TAG, "Out-of-bounds Length Delimited at offset %ld", (long) (ptr - buffer));
return; return;
+158 -459
View File
@@ -1,6 +1,7 @@
#pragma once #pragma once
#include "api_pb2_defines.h" #include "api_pb2_defines.h"
#include "api_buffer.h"
#include "esphome/core/component.h" #include "esphome/core/component.h"
#include "esphome/core/helpers.h" #include "esphome/core/helpers.h"
#include "esphome/core/log.h" #include "esphome/core/log.h"
@@ -98,94 +99,60 @@ inline void encode_varint_to_buffer(uint32_t val, uint8_t *buffer) {
* within the same function scope where temporaries are created. * within the same function scope where temporaries are created.
*/ */
/// Representation of a VarInt - in ProtoBuf should be 64bit but we only use 32bit /// Type used for decoded varint values - uint64_t when BLE needs 64-bit addresses, uint32_t otherwise
#ifdef USE_API_VARINT64
using proto_varint_value_t = uint64_t;
#else
using proto_varint_value_t = uint32_t;
#endif
/// Sentinel value for consumed field indicating parse failure
inline constexpr uint32_t PROTO_VARINT_PARSE_FAILED = 0;
/// Result of parsing a varint: value + number of bytes consumed.
/// consumed == PROTO_VARINT_PARSE_FAILED indicates parse failure (not enough data or invalid).
struct ProtoVarIntResult {
proto_varint_value_t value;
uint32_t consumed; // PROTO_VARINT_PARSE_FAILED = parse failed
constexpr bool has_value() const { return this->consumed != PROTO_VARINT_PARSE_FAILED; }
};
/// Static varint parsing methods for the protobuf wire format.
class ProtoVarInt { class ProtoVarInt {
public: public:
ProtoVarInt() : value_(0) {} /// Parse a varint from buffer. Caller must ensure len >= 1.
explicit ProtoVarInt(uint64_t value) : value_(value) {} /// Returns result with consumed=0 on failure (truncated multi-byte varint).
static inline ProtoVarIntResult ESPHOME_ALWAYS_INLINE parse_non_empty(const uint8_t *buffer, uint32_t len) {
/// Parse a varint from buffer. consumed must be a valid pointer (not null).
static optional<ProtoVarInt> parse(const uint8_t *buffer, uint32_t len, uint32_t *consumed) {
#ifdef ESPHOME_DEBUG_API #ifdef ESPHOME_DEBUG_API
assert(consumed != nullptr); assert(len > 0);
#endif #endif
if (len == 0)
return {};
// Fast path: single-byte varints (0-127) are the most common case // Fast path: single-byte varints (0-127) are the most common case
// (booleans, small enums, field tags). Avoid loop overhead entirely. // (booleans, small enums, field tags, small message sizes/types).
if ((buffer[0] & 0x80) == 0) { if ((buffer[0] & 0x80) == 0) [[likely]]
*consumed = 1; return {buffer[0], 1};
return ProtoVarInt(buffer[0]); return parse_slow(buffer, len);
} }
// 32-bit phase: process remaining bytes with native 32-bit shifts.
// Without USE_API_VARINT64: cover bytes 1-4 (shifts 7, 14, 21, 28) — the uint32_t /// Parse a varint from buffer (safe for empty buffers).
// shift at byte 4 (shift by 28) may lose bits 32-34, but those are always zero for valid uint32 values. /// Returns result with consumed=0 on failure (empty buffer or truncated varint).
// With USE_API_VARINT64: cover bytes 1-3 (shifts 7, 14, 21) so parse_wide handles static inline ProtoVarIntResult ESPHOME_ALWAYS_INLINE parse(const uint8_t *buffer, uint32_t len) {
// byte 4+ with full 64-bit arithmetic (avoids truncating values > UINT32_MAX). if (len == 0)
uint32_t result32 = buffer[0] & 0x7F; return {0, PROTO_VARINT_PARSE_FAILED};
#ifdef USE_API_VARINT64 return parse_non_empty(buffer, len);
uint32_t limit = std::min(len, uint32_t(4));
#else
uint32_t limit = std::min(len, uint32_t(5));
#endif
for (uint32_t i = 1; i < limit; i++) {
uint8_t val = buffer[i];
result32 |= uint32_t(val & 0x7F) << (i * 7);
if ((val & 0x80) == 0) {
*consumed = i + 1;
return ProtoVarInt(result32);
}
}
// 64-bit phase for remaining bytes (BLE addresses etc.)
#ifdef USE_API_VARINT64
return parse_wide(buffer, len, consumed, result32);
#else
return {};
#endif
} }
#ifdef USE_API_VARINT64
protected: protected:
// Slow path for multi-byte varints (>= 128), outlined to keep fast path small
static ProtoVarIntResult parse_slow(const uint8_t *buffer, uint32_t len) __attribute__((noinline));
#ifdef USE_API_VARINT64
/// Continue parsing varint bytes 4-9 with 64-bit arithmetic. /// Continue parsing varint bytes 4-9 with 64-bit arithmetic.
/// Separated to keep 64-bit shift code (__ashldi3 on 32-bit platforms) out of the common path. static ProtoVarIntResult parse_wide(const uint8_t *buffer, uint32_t len, uint32_t result32) __attribute__((noinline));
static optional<ProtoVarInt> parse_wide(const uint8_t *buffer, uint32_t len, uint32_t *consumed, uint32_t result32)
__attribute__((noinline));
public:
#endif
constexpr uint16_t as_uint16() const { return this->value_; }
constexpr uint32_t as_uint32() const { return this->value_; }
constexpr bool as_bool() const { return this->value_; }
constexpr int32_t as_int32() const {
// Not ZigZag encoded
return static_cast<int32_t>(this->value_);
}
constexpr int32_t as_sint32() const {
// with ZigZag encoding
return decode_zigzag32(static_cast<uint32_t>(this->value_));
}
#ifdef USE_API_VARINT64
constexpr uint64_t as_uint64() const { return this->value_; }
constexpr int64_t as_int64() const {
// Not ZigZag encoded
return static_cast<int64_t>(this->value_);
}
constexpr int64_t as_sint64() const {
// with ZigZag encoding
return decode_zigzag64(this->value_);
}
#endif
protected:
#ifdef USE_API_VARINT64
uint64_t value_;
#else
uint32_t value_;
#endif #endif
}; };
// Forward declarations for decode_to_message, encode_message and encode_packed_sint32 // Forward declarations for decode_to_message and related encoding helpers
class ProtoDecodableMessage; class ProtoDecodableMessage;
class ProtoMessage; class ProtoMessage;
class ProtoSize; class ProtoSize;
@@ -237,17 +204,15 @@ class Proto32Bit {
class ProtoWriteBuffer { class ProtoWriteBuffer {
public: public:
ProtoWriteBuffer(std::vector<uint8_t> *buffer) : buffer_(buffer), pos_(buffer->data() + buffer->size()) {} ProtoWriteBuffer(APIBuffer *buffer) : buffer_(buffer), pos_(buffer->data() + buffer->size()) {}
ProtoWriteBuffer(std::vector<uint8_t> *buffer, size_t write_pos) ProtoWriteBuffer(APIBuffer *buffer, size_t write_pos) : buffer_(buffer), pos_(buffer->data() + write_pos) {}
: buffer_(buffer), pos_(buffer->data() + write_pos) {} inline void ESPHOME_ALWAYS_INLINE encode_varint_raw(uint32_t value) {
void encode_varint_raw(uint32_t value) { if (value < 128) [[likely]] {
while (value > 0x7F) {
this->debug_check_bounds_(1); this->debug_check_bounds_(1);
*this->pos_++ = static_cast<uint8_t>(value | 0x80); *this->pos_++ = static_cast<uint8_t>(value);
value >>= 7; return;
} }
this->debug_check_bounds_(1); this->encode_varint_raw_slow_(value);
*this->pos_++ = static_cast<uint8_t>(value);
} }
void encode_varint_raw_64(uint64_t value) { void encode_varint_raw_64(uint64_t value) {
while (value > 0x7F) { while (value > 0x7F) {
@@ -363,11 +328,24 @@ class ProtoWriteBuffer {
} }
/// Encode a packed repeated sint32 field (zero-copy from vector) /// Encode a packed repeated sint32 field (zero-copy from vector)
void encode_packed_sint32(uint32_t field_id, const std::vector<int32_t> &values); void encode_packed_sint32(uint32_t field_id, const std::vector<int32_t> &values);
/// Encode a nested message field (force=true for repeated, false for singular) /// Single-pass encode for repeated submessage elements.
void encode_message(uint32_t field_id, const ProtoMessage &value, bool force = true); /// Thin template wrapper; all buffer work is in the non-template core.
std::vector<uint8_t> *get_buffer() const { return buffer_; } template<typename T> void encode_sub_message(uint32_t field_id, const T &value);
/// Encode an optional singular submessage field — skips if empty.
/// Thin template wrapper; all buffer work is in the non-template core.
template<typename T> void encode_optional_sub_message(uint32_t field_id, const T &value);
// Non-template core for encode_sub_message — backpatch approach.
void encode_sub_message(uint32_t field_id, const void *value, void (*encode_fn)(const void *, ProtoWriteBuffer &));
// Non-template core for encode_optional_sub_message.
void encode_optional_sub_message(uint32_t field_id, uint32_t nested_size, const void *value,
void (*encode_fn)(const void *, ProtoWriteBuffer &));
APIBuffer *get_buffer() const { return buffer_; }
protected: protected:
// Slow path for encode_varint_raw values >= 128, outlined to keep fast path small
void encode_varint_raw_slow_(uint32_t value) __attribute__((noinline));
#ifdef ESPHOME_DEBUG_API #ifdef ESPHOME_DEBUG_API
void debug_check_bounds_(size_t bytes, const char *caller = __builtin_FUNCTION()); void debug_check_bounds_(size_t bytes, const char *caller = __builtin_FUNCTION());
void debug_check_encode_size_(uint32_t field_id, uint32_t expected, ptrdiff_t actual); void debug_check_encode_size_(uint32_t field_id, uint32_t expected, ptrdiff_t actual);
@@ -375,7 +353,7 @@ class ProtoWriteBuffer {
void debug_check_bounds_([[maybe_unused]] size_t bytes) {} void debug_check_bounds_([[maybe_unused]] size_t bytes) {}
#endif #endif
std::vector<uint8_t> *buffer_; APIBuffer *buffer_;
uint8_t *pos_; uint8_t *pos_;
}; };
@@ -452,17 +430,21 @@ class DumpBuffer {
class ProtoMessage { class ProtoMessage {
public: public:
virtual ~ProtoMessage() = default; // Non-virtual defaults for messages with no fields.
// Default implementation for messages with no fields // Concrete message classes hide these with their own implementations.
virtual void encode(ProtoWriteBuffer &buffer) const {} // All call sites use templates to preserve the concrete type, so virtual
// Default implementation for messages with no fields // dispatch is not needed. This eliminates per-message vtable entries for
virtual void calculate_size(ProtoSize &size) const {} // encode/calculate_size, saving ~1.3 KB of flash across all message types.
// Convenience: calculate and return size directly (defined after ProtoSize) void encode(ProtoWriteBuffer &buffer) const {}
uint32_t calculated_size() const; uint32_t calculate_size() const { return 0; }
#ifdef HAS_PROTO_MESSAGE_DUMP #ifdef HAS_PROTO_MESSAGE_DUMP
virtual const char *dump_to(DumpBuffer &out) const = 0; virtual const char *dump_to(DumpBuffer &out) const = 0;
virtual const char *message_name() const { return "unknown"; } virtual const char *message_name() const { return "unknown"; }
#endif #endif
protected:
// Non-virtual destructor is protected to prevent polymorphic deletion.
~ProtoMessage() = default;
}; };
// Base class for messages that support decoding // Base class for messages that support decoding
@@ -482,63 +464,44 @@ class ProtoDecodableMessage : public ProtoMessage {
static uint32_t count_repeated_field(const uint8_t *buffer, size_t length, uint32_t target_field_id); static uint32_t count_repeated_field(const uint8_t *buffer, size_t length, uint32_t target_field_id);
protected: protected:
virtual bool decode_varint(uint32_t field_id, ProtoVarInt value) { return false; } ~ProtoDecodableMessage() = default;
virtual bool decode_varint(uint32_t field_id, proto_varint_value_t value) { return false; }
virtual bool decode_length(uint32_t field_id, ProtoLengthDelimited value) { return false; } virtual bool decode_length(uint32_t field_id, ProtoLengthDelimited value) { return false; }
virtual bool decode_32bit(uint32_t field_id, Proto32Bit value) { return false; } virtual bool decode_32bit(uint32_t field_id, Proto32Bit value) { return false; }
// NOTE: decode_64bit removed - wire type 1 not supported // NOTE: decode_64bit removed - wire type 1 not supported
}; };
class ProtoSize { class ProtoSize {
private:
uint32_t total_size_ = 0;
public: public:
/**
* @brief ProtoSize class for Protocol Buffer serialization size calculation
*
* This class provides methods to calculate the exact byte counts needed
* for encoding various Protocol Buffer field types. The class now uses an
* object-based approach to reduce parameter passing overhead while keeping
* varint calculation methods static for external use.
*
* Implements Protocol Buffer encoding size calculation according to:
* https://protobuf.dev/programming-guides/encoding/
*
* Key features:
* - Object-based approach reduces flash usage by eliminating parameter passing
* - Early-return optimization for zero/default values
* - Static varint methods for external callers
* - Specialized handling for different field types according to protobuf spec
*/
ProtoSize() = default;
uint32_t get_size() const { return total_size_; }
/** /**
* @brief Calculates the size in bytes needed to encode a uint32_t value as a varint * @brief Calculates the size in bytes needed to encode a uint32_t value as a varint
* *
* @param value The uint32_t value to calculate size for * @param value The uint32_t value to calculate size for
* @return The number of bytes needed to encode the value * @return The number of bytes needed to encode the value
*/ */
static constexpr uint32_t varint(uint32_t value) { static constexpr inline uint32_t ESPHOME_ALWAYS_INLINE varint(uint32_t value) {
// Optimized varint size calculation using leading zeros if (value < 128) [[likely]]
// Each 7 bits requires one byte in the varint encoding return 1; // Fast path: 7 bits, most common case
if (value < 128) if (__builtin_is_constant_evaluated())
return 1; // 7 bits, common case for small values return varint_wide(value);
return varint_slow(value);
// For larger values, count bytes needed based on the position of the highest bit set
if (value < 16384) {
return 2; // 14 bits
} else if (value < 2097152) {
return 3; // 21 bits
} else if (value < 268435456) {
return 4; // 28 bits
} else {
return 5; // 32 bits (maximum for uint32_t)
}
} }
private:
// Slow path for varint >= 128, outlined to keep fast path small
static uint32_t varint_slow(uint32_t value) __attribute__((noinline));
// Shared cascade for values >= 128 (used by both constexpr and noinline paths)
static constexpr inline uint32_t ESPHOME_ALWAYS_INLINE varint_wide(uint32_t value) {
if (value < 16384)
return 2;
if (value < 2097152)
return 3;
if (value < 268435456)
return 4;
return 5;
}
public:
/** /**
* @brief Calculates the size in bytes needed to encode a uint64_t value as a varint * @brief Calculates the size in bytes needed to encode a uint64_t value as a varint
* *
@@ -611,320 +574,77 @@ class ProtoSize {
return varint(tag); return varint(tag);
} }
/** // Static methods that RETURN size contribution (no ProtoSize object needed).
* @brief Common parameters for all add_*_field methods // Used by generated calculate_size() methods to accumulate into a plain uint32_t register.
* static constexpr uint32_t calc_int32(uint32_t field_id_size, int32_t value) {
* All add_*_field methods follow these common patterns: return value ? field_id_size + (value < 0 ? 10 : varint(static_cast<uint32_t>(value))) : 0;
* * @param field_id_size Pre-calculated size of the field ID in bytes
* @param value The value to calculate size for (type varies)
* @param force Whether to calculate size even if the value is default/zero/empty
*
* Each method follows this implementation pattern:
* 1. Skip calculation if value is default (0, false, empty) and not forced
* 2. Calculate the size based on the field's encoding rules
* 3. Add the field_id_size + calculated value size to total_size
*/
/**
* @brief Calculates and adds the size of an int32 field to the total message size
*/
inline void add_int32(uint32_t field_id_size, int32_t value) {
if (value != 0) {
add_int32_force(field_id_size, value);
}
} }
static constexpr uint32_t calc_int32_force(uint32_t field_id_size, int32_t value) {
/** return field_id_size + (value < 0 ? 10 : varint(static_cast<uint32_t>(value)));
* @brief Calculates and adds the size of an int32 field to the total message size (force version)
*/
inline void add_int32_force(uint32_t field_id_size, int32_t value) {
// Always calculate size when forced
// Negative values are encoded as 10-byte varints in protobuf
total_size_ += field_id_size + (value < 0 ? 10 : varint(static_cast<uint32_t>(value)));
} }
static constexpr uint32_t calc_uint32(uint32_t field_id_size, uint32_t value) {
/** return value ? field_id_size + varint(value) : 0;
* @brief Calculates and adds the size of a uint32 field to the total message size
*/
inline void add_uint32(uint32_t field_id_size, uint32_t value) {
if (value != 0) {
add_uint32_force(field_id_size, value);
}
} }
static constexpr uint32_t calc_uint32_force(uint32_t field_id_size, uint32_t value) {
/** return field_id_size + varint(value);
* @brief Calculates and adds the size of a uint32 field to the total message size (force version)
*/
inline void add_uint32_force(uint32_t field_id_size, uint32_t value) {
// Always calculate size when force is true
total_size_ += field_id_size + varint(value);
} }
static constexpr uint32_t calc_bool(uint32_t field_id_size, bool value) { return value ? field_id_size + 1 : 0; }
/** static constexpr uint32_t calc_bool_force(uint32_t field_id_size) { return field_id_size + 1; }
* @brief Calculates and adds the size of a boolean field to the total message size static constexpr uint32_t calc_float(uint32_t field_id_size, float value) {
*/ return value != 0.0f ? field_id_size + 4 : 0;
inline void add_bool(uint32_t field_id_size, bool value) {
if (value) {
// Boolean fields always use 1 byte when true
total_size_ += field_id_size + 1;
}
} }
static constexpr uint32_t calc_fixed32(uint32_t field_id_size, uint32_t value) {
/** return value ? field_id_size + 4 : 0;
* @brief Calculates and adds the size of a boolean field to the total message size (force version)
*/
inline void add_bool_force(uint32_t field_id_size, bool value) {
// Always calculate size when force is true
// Boolean fields always use 1 byte
total_size_ += field_id_size + 1;
} }
static constexpr uint32_t calc_sfixed32(uint32_t field_id_size, int32_t value) {
/** return value ? field_id_size + 4 : 0;
* @brief Calculates and adds the size of a float field to the total message size
*/
inline void add_float(uint32_t field_id_size, float value) {
if (value != 0.0f) {
total_size_ += field_id_size + 4;
}
} }
static constexpr uint32_t calc_sint32(uint32_t field_id_size, int32_t value) {
// NOTE: add_double_field removed - wire type 1 (64-bit: double) not supported return value ? field_id_size + varint(encode_zigzag32(value)) : 0;
// to reduce overhead on embedded systems
/**
* @brief Calculates and adds the size of a fixed32 field to the total message size
*/
inline void add_fixed32(uint32_t field_id_size, uint32_t value) {
if (value != 0) {
total_size_ += field_id_size + 4;
}
} }
static constexpr uint32_t calc_sint32_force(uint32_t field_id_size, int32_t value) {
// NOTE: add_fixed64_field removed - wire type 1 (64-bit: fixed64) not supported return field_id_size + varint(encode_zigzag32(value));
// to reduce overhead on embedded systems
/**
* @brief Calculates and adds the size of a sfixed32 field to the total message size
*/
inline void add_sfixed32(uint32_t field_id_size, int32_t value) {
if (value != 0) {
total_size_ += field_id_size + 4;
}
} }
static constexpr uint32_t calc_int64(uint32_t field_id_size, int64_t value) {
// NOTE: add_sfixed64_field removed - wire type 1 (64-bit: sfixed64) not supported return value ? field_id_size + varint(value) : 0;
// to reduce overhead on embedded systems
/**
* @brief Calculates and adds the size of a sint32 field to the total message size
*
* Sint32 fields use ZigZag encoding, which is more efficient for negative values.
*/
inline void add_sint32(uint32_t field_id_size, int32_t value) {
if (value != 0) {
add_sint32_force(field_id_size, value);
}
} }
static constexpr uint32_t calc_int64_force(uint32_t field_id_size, int64_t value) {
/** return field_id_size + varint(value);
* @brief Calculates and adds the size of a sint32 field to the total message size (force version)
*
* Sint32 fields use ZigZag encoding, which is more efficient for negative values.
*/
inline void add_sint32_force(uint32_t field_id_size, int32_t value) {
// Always calculate size when force is true
// ZigZag encoding for sint32
total_size_ += field_id_size + varint(encode_zigzag32(value));
} }
static constexpr uint32_t calc_uint64(uint32_t field_id_size, uint64_t value) {
/** return value ? field_id_size + varint(value) : 0;
* @brief Calculates and adds the size of an int64 field to the total message size
*/
inline void add_int64(uint32_t field_id_size, int64_t value) {
if (value != 0) {
add_int64_force(field_id_size, value);
}
} }
static constexpr uint32_t calc_uint64_force(uint32_t field_id_size, uint64_t value) {
/** return field_id_size + varint(value);
* @brief Calculates and adds the size of an int64 field to the total message size (force version)
*/
inline void add_int64_force(uint32_t field_id_size, int64_t value) {
// Always calculate size when force is true
total_size_ += field_id_size + varint(value);
} }
static constexpr uint32_t calc_length(uint32_t field_id_size, size_t len) {
/** return len ? field_id_size + varint(static_cast<uint32_t>(len)) + static_cast<uint32_t>(len) : 0;
* @brief Calculates and adds the size of a uint64 field to the total message size
*/
inline void add_uint64(uint32_t field_id_size, uint64_t value) {
if (value != 0) {
add_uint64_force(field_id_size, value);
}
} }
static constexpr uint32_t calc_length_force(uint32_t field_id_size, size_t len) {
/** return field_id_size + varint(static_cast<uint32_t>(len)) + static_cast<uint32_t>(len);
* @brief Calculates and adds the size of a uint64 field to the total message size (force version)
*/
inline void add_uint64_force(uint32_t field_id_size, uint64_t value) {
// Always calculate size when force is true
total_size_ += field_id_size + varint(value);
} }
static constexpr uint32_t calc_sint64(uint32_t field_id_size, int64_t value) {
// NOTE: sint64 support functions (add_sint64_field, add_sint64_field_force) removed return value ? field_id_size + varint(encode_zigzag64(value)) : 0;
// sint64 type is not supported by ESPHome API to reduce overhead on embedded systems
/**
* @brief Calculates and adds the size of a length-delimited field (string/bytes) to the total message size
*/
inline void add_length(uint32_t field_id_size, size_t len) {
if (len != 0) {
add_length_force(field_id_size, len);
}
} }
static constexpr uint32_t calc_sint64_force(uint32_t field_id_size, int64_t value) {
/** return field_id_size + varint(encode_zigzag64(value));
* @brief Calculates and adds the size of a length-delimited field (string/bytes) to the total message size (repeated
* field version)
*/
inline void add_length_force(uint32_t field_id_size, size_t len) {
// Always calculate size when force is true
// Field ID + length varint + data bytes
total_size_ += field_id_size + varint(static_cast<uint32_t>(len)) + static_cast<uint32_t>(len);
} }
static constexpr uint32_t calc_fixed64(uint32_t field_id_size, uint64_t value) {
/** return value ? field_id_size + 8 : 0;
* @brief Adds a pre-calculated size directly to the total
*
* This is used when we can calculate the total size by multiplying the number
* of elements by the bytes per element (for repeated fixed-size types like float, fixed32, etc.)
*
* @param size The pre-calculated total size to add
*/
inline void add_precalculated_size(uint32_t size) { total_size_ += size; }
/**
* @brief Calculates and adds the size of a nested message field to the total message size
*
* This helper function directly updates the total_size reference if the nested size
* is greater than zero.
*
* @param nested_size The pre-calculated size of the nested message
*/
inline void add_message_field(uint32_t field_id_size, uint32_t nested_size) {
if (nested_size != 0) {
add_message_field_force(field_id_size, nested_size);
}
} }
static constexpr uint32_t calc_sfixed64(uint32_t field_id_size, int64_t value) {
/** return value ? field_id_size + 8 : 0;
* @brief Calculates and adds the size of a nested message field to the total message size (force version)
*
* @param nested_size The pre-calculated size of the nested message
*/
inline void add_message_field_force(uint32_t field_id_size, uint32_t nested_size) {
// Always calculate size when force is true
// Field ID + length varint + nested message content
total_size_ += field_id_size + varint(nested_size) + nested_size;
} }
static constexpr uint32_t calc_message(uint32_t field_id_size, uint32_t nested_size) {
/** return nested_size ? field_id_size + varint(nested_size) + nested_size : 0;
* @brief Calculates and adds the size of a nested message field to the total message size
*
* This version takes a ProtoMessage object, calculates its size internally,
* and updates the total_size reference. This eliminates the need for a temporary variable
* at the call site.
*
* @param message The nested message object
*/
inline void add_message_object(uint32_t field_id_size, const ProtoMessage &message) {
// Calculate nested message size by creating a temporary ProtoSize
ProtoSize nested_calc;
message.calculate_size(nested_calc);
uint32_t nested_size = nested_calc.get_size();
// Use the base implementation with the calculated nested_size
add_message_field(field_id_size, nested_size);
} }
static constexpr uint32_t calc_message_force(uint32_t field_id_size, uint32_t nested_size) {
/** return field_id_size + varint(nested_size) + nested_size;
* @brief Calculates and adds the size of a nested message field to the total message size (force version)
*
* @param message The nested message object
*/
inline void add_message_object_force(uint32_t field_id_size, const ProtoMessage &message) {
// Calculate nested message size by creating a temporary ProtoSize
ProtoSize nested_calc;
message.calculate_size(nested_calc);
uint32_t nested_size = nested_calc.get_size();
// Use the base implementation with the calculated nested_size
add_message_field_force(field_id_size, nested_size);
}
/**
* @brief Calculates and adds the sizes of all messages in a repeated field to the total message size
*
* This helper processes a vector of message objects, calculating the size for each message
* and adding it to the total size.
*
* @tparam MessageType The type of the nested messages in the vector
* @param messages Vector of message objects
*/
template<typename MessageType>
inline void add_repeated_message(uint32_t field_id_size, const std::vector<MessageType> &messages) {
// Skip if the vector is empty
if (!messages.empty()) {
// Use the force version for all messages in the repeated field
for (const auto &message : messages) {
add_message_object_force(field_id_size, message);
}
}
}
/**
* @brief Calculates and adds the sizes of all messages in a repeated field to the total message size (FixedVector
* version)
*
* @tparam MessageType The type of the nested messages in the FixedVector
* @param messages FixedVector of message objects
*/
template<typename MessageType>
inline void add_repeated_message(uint32_t field_id_size, const FixedVector<MessageType> &messages) {
// Skip if the fixed vector is empty
if (!messages.empty()) {
// Use the force version for all messages in the repeated field
for (const auto &message : messages) {
add_message_object_force(field_id_size, message);
}
}
}
/**
* @brief Calculate size of a packed repeated sint32 field
*/
inline void add_packed_sint32(uint32_t field_id_size, const std::vector<int32_t> &values) {
if (values.empty())
return;
size_t packed_size = 0;
for (int value : values) {
packed_size += varint(encode_zigzag32(value));
}
// field_id + length varint + packed data
total_size_ += field_id_size + varint(static_cast<uint32_t>(packed_size)) + static_cast<uint32_t>(packed_size);
} }
}; };
// Implementation of methods that depend on ProtoSize being fully defined // Implementation of methods that depend on ProtoSize being fully defined
inline uint32_t ProtoMessage::calculated_size() const {
ProtoSize size;
this->calculate_size(size);
return size.get_size();
}
// Implementation of encode_packed_sint32 - must be after ProtoSize is defined // Implementation of encode_packed_sint32 - must be after ProtoSize is defined
inline void ProtoWriteBuffer::encode_packed_sint32(uint32_t field_id, const std::vector<int32_t> &values) { inline void ProtoWriteBuffer::encode_packed_sint32(uint32_t field_id, const std::vector<int32_t> &values) {
if (values.empty()) if (values.empty())
@@ -944,32 +664,19 @@ inline void ProtoWriteBuffer::encode_packed_sint32(uint32_t field_id, const std:
} }
} }
// Implementation of encode_message - must be after ProtoMessage is defined // Encode thunk — converts void* back to concrete type for direct encode() call
inline void ProtoWriteBuffer::encode_message(uint32_t field_id, const ProtoMessage &value, bool force) { template<typename T> void proto_encode_msg(const void *msg, ProtoWriteBuffer &buf) {
// Calculate the message size first static_cast<const T *>(msg)->encode(buf);
ProtoSize msg_size; }
value.calculate_size(msg_size);
uint32_t msg_length_bytes = msg_size.get_size();
// Skip empty singular messages (matches add_message_field which skips when nested_size == 0) // Thin template wrapper; delegates to non-template core in proto.cpp.
// Repeated messages (force=true) are always encoded since an empty item is meaningful template<typename T> inline void ProtoWriteBuffer::encode_sub_message(uint32_t field_id, const T &value) {
if (msg_length_bytes == 0 && !force) this->encode_sub_message(field_id, &value, &proto_encode_msg<T>);
return; }
this->encode_field_raw(field_id, 2); // type 2: Length-delimited message // Thin template wrapper; delegates to non-template core.
template<typename T> inline void ProtoWriteBuffer::encode_optional_sub_message(uint32_t field_id, const T &value) {
// Write the length varint directly through pos_ this->encode_optional_sub_message(field_id, value.calculate_size(), &value, &proto_encode_msg<T>);
this->encode_varint_raw(msg_length_bytes);
// Encode nested message - pos_ advances directly through the reference
#ifdef ESPHOME_DEBUG_API
uint8_t *start = this->pos_;
value.encode(*this);
if (static_cast<uint32_t>(this->pos_ - start) != msg_length_bytes)
this->debug_check_encode_size_(field_id, msg_length_bytes, this->pos_ - start);
#else
value.encode(*this);
#endif
} }
// Implementation of decode_to_message - must be after ProtoDecodableMessage is defined // Implementation of decode_to_message - must be after ProtoDecodableMessage is defined
@@ -988,14 +695,6 @@ class ProtoService {
virtual void on_no_setup_connection() = 0; virtual void on_no_setup_connection() = 0;
virtual bool send_buffer(ProtoWriteBuffer buffer, uint8_t message_type) = 0; virtual bool send_buffer(ProtoWriteBuffer buffer, uint8_t message_type) = 0;
virtual void read_message(uint32_t msg_size, uint32_t msg_type, const uint8_t *msg_data) = 0; virtual void read_message(uint32_t msg_size, uint32_t msg_type, const uint8_t *msg_data) = 0;
/**
* Send a protobuf message by calculating its size, allocating a buffer, encoding, and sending.
* This is the implementation method - callers should use send_message() which adds logging.
* @param msg The protobuf message to send.
* @param message_type The message type identifier.
* @return True if the message was sent successfully, false otherwise.
*/
virtual bool send_message_impl(const ProtoMessage &msg, uint8_t message_type) = 0;
// Authentication helper methods // Authentication helper methods
inline bool check_connection_setup_() { inline bool check_connection_setup_() {
+1 -1
View File
@@ -9,6 +9,7 @@ from esphome.const import (
CONF_ID, CONF_ID,
CONF_POWER_MODE, CONF_POWER_MODE,
CONF_RANGE, CONF_RANGE,
CONF_WATCHDOG,
) )
CODEOWNERS = ["@ammmze"] CODEOWNERS = ["@ammmze"]
@@ -57,7 +58,6 @@ FAST_FILTER = {
CONF_RAW_ANGLE = "raw_angle" CONF_RAW_ANGLE = "raw_angle"
CONF_RAW_POSITION = "raw_position" CONF_RAW_POSITION = "raw_position"
CONF_WATCHDOG = "watchdog"
CONF_SLOW_FILTER = "slow_filter" CONF_SLOW_FILTER = "slow_filter"
CONF_FAST_FILTER = "fast_filter" CONF_FAST_FILTER = "fast_filter"
CONF_START_POSITION = "start_position" CONF_START_POSITION = "start_position"
@@ -23,7 +23,6 @@ AS5600Sensor = as5600_ns.class_("AS5600Sensor", sensor.Sensor, cg.PollingCompone
CONF_RAW_ANGLE = "raw_angle" CONF_RAW_ANGLE = "raw_angle"
CONF_RAW_POSITION = "raw_position" CONF_RAW_POSITION = "raw_position"
CONF_WATCHDOG = "watchdog"
CONF_SLOW_FILTER = "slow_filter" CONF_SLOW_FILTER = "slow_filter"
CONF_FAST_FILTER = "fast_filter" CONF_FAST_FILTER = "fast_filter"
CONF_PWM_FREQUENCY = "pwm_frequency" CONF_PWM_FREQUENCY = "pwm_frequency"
+2
View File
@@ -89,6 +89,7 @@ AT581XSettingsAction = at581x_ns.class_("AT581XSettingsAction", automation.Actio
cv.Required(CONF_ID): cv.use_id(AT581XComponent), cv.Required(CONF_ID): cv.use_id(AT581XComponent),
} }
), ),
synchronous=True,
) )
async def at581x_reset_to_code(config, action_id, template_arg, args): async def at581x_reset_to_code(config, action_id, template_arg, args):
var = cg.new_Pvariable(action_id, template_arg) var = cg.new_Pvariable(action_id, template_arg)
@@ -160,6 +161,7 @@ RADAR_SETTINGS_SCHEMA = cv.Schema(
"at581x.settings", "at581x.settings",
AT581XSettingsAction, AT581XSettingsAction,
RADAR_SETTINGS_SCHEMA, RADAR_SETTINGS_SCHEMA,
synchronous=True,
) )
async def at581x_settings_to_code(config, action_id, template_arg, args): async def at581x_settings_to_code(config, action_id, template_arg, args):
var = cg.new_Pvariable(action_id, template_arg) var = cg.new_Pvariable(action_id, template_arg)
+5
View File
@@ -135,6 +135,11 @@ bool AT581XComponent::i2c_write_config() {
} }
// Set gain // Set gain
if (this->gain_ < 0 || static_cast<size_t>(this->gain_) >= ARRAY_SIZE(GAIN5C_TABLE) ||
static_cast<size_t>(this->gain_ >> 1) >= ARRAY_SIZE(GAIN63_TABLE)) {
ESP_LOGE(TAG, "AT581X gain index out of range: %d", this->gain_);
return false;
}
if (!this->i2c_write_reg(GAIN_ADDR_TABLE[0], GAIN5C_TABLE[this->gain_]) || if (!this->i2c_write_reg(GAIN_ADDR_TABLE[0], GAIN5C_TABLE[this->gain_]) ||
!this->i2c_write_reg(GAIN_ADDR_TABLE[1], GAIN63_TABLE[this->gain_ >> 1])) { !this->i2c_write_reg(GAIN_ADDR_TABLE[1], GAIN63_TABLE[this->gain_ >> 1])) {
ESP_LOGE(TAG, "Failed to write AT581X gain registers"); ESP_LOGE(TAG, "Failed to write AT581X gain registers");
@@ -61,6 +61,10 @@ optional<ParseResult> ATCMiThermometer::parse_header_(const esp32_ble_tracker::S
} }
auto raw = service_data.data; auto raw = service_data.data;
if (raw.size() < 13) {
ESP_LOGVV(TAG, "parse_header_(): service data too short (%zu).", raw.size());
return {};
}
static uint8_t last_frame_count = 0; static uint8_t last_frame_count = 0;
if (last_frame_count == raw[12]) { if (last_frame_count == raw[12]) {
+1 -1
View File
@@ -197,7 +197,7 @@ float ATM90E26Component::get_reactive_power_() {
float ATM90E26Component::get_power_factor_() { float ATM90E26Component::get_power_factor_() {
const uint16_t val = this->read16_(ATM90E26_REGISTER_POWERF); // signed const uint16_t val = this->read16_(ATM90E26_REGISTER_POWERF); // signed
if (val & 0x8000) { if (val & 0x8000) {
return -(val & 0x7FF) / 1000.0f; return -(val & 0x7FFF) / 1000.0f;
} else { } else {
return val / 1000.0f; return val / 1000.0f;
} }
+2 -2
View File
@@ -619,7 +619,7 @@ void ATM90E32Component::run_gain_calibrations() {
ESP_LOGW(TAG, "[CALIBRATION][%s] Phase %s - Skipping voltage calibration: measured voltage is 0.", cs, ESP_LOGW(TAG, "[CALIBRATION][%s] Phase %s - Skipping voltage calibration: measured voltage is 0.", cs,
phase_labels[phase]); phase_labels[phase]);
} else { } else {
uint32_t new_voltage_gain = static_cast<uint16_t>((ref_voltage / measured_voltage) * current_voltage_gain); uint32_t new_voltage_gain = static_cast<uint32_t>((ref_voltage / measured_voltage) * current_voltage_gain);
if (new_voltage_gain == 0) { if (new_voltage_gain == 0) {
ESP_LOGW(TAG, "[CALIBRATION][%s] Phase %s - Voltage gain would be 0. Check reference and measured voltage.", cs, ESP_LOGW(TAG, "[CALIBRATION][%s] Phase %s - Voltage gain would be 0. Check reference and measured voltage.", cs,
phase_labels[phase]); phase_labels[phase]);
@@ -644,7 +644,7 @@ void ATM90E32Component::run_gain_calibrations() {
ESP_LOGW(TAG, "[CALIBRATION][%s] Phase %s - Skipping current calibration: measured current is 0.", cs, ESP_LOGW(TAG, "[CALIBRATION][%s] Phase %s - Skipping current calibration: measured current is 0.", cs,
phase_labels[phase]); phase_labels[phase]);
} else { } else {
uint32_t new_current_gain = static_cast<uint16_t>((ref_current / measured_current) * current_current_gain); uint32_t new_current_gain = static_cast<uint32_t>((ref_current / measured_current) * current_current_gain);
if (new_current_gain == 0) { if (new_current_gain == 0) {
ESP_LOGW(TAG, "[CALIBRATION][%s] Phase %s - Current gain would be 0. Check reference and measured current.", cs, ESP_LOGW(TAG, "[CALIBRATION][%s] Phase %s - Current gain would be 0. Check reference and measured current.", cs,
phase_labels[phase]); phase_labels[phase]);
+56
View File
@@ -1,5 +1,9 @@
#include "audio.h" #include "audio.h"
#include "esphome/core/helpers.h"
#include <cstring>
namespace esphome { namespace esphome {
namespace audio { namespace audio {
@@ -58,6 +62,58 @@ const char *audio_file_type_to_string(AudioFileType file_type) {
} }
} }
AudioFileType detect_audio_file_type(const char *content_type, const char *url) {
// Try Content-Type header first
if (content_type != nullptr && content_type[0] != '\0') {
#ifdef USE_AUDIO_MP3_SUPPORT
if (strcasecmp(content_type, "mp3") == 0 || strcasecmp(content_type, "audio/mp3") == 0 ||
strcasecmp(content_type, "audio/mpeg") == 0) {
return AudioFileType::MP3;
}
#endif
if (strcasecmp(content_type, "audio/wav") == 0) {
return AudioFileType::WAV;
}
#ifdef USE_AUDIO_FLAC_SUPPORT
if (strcasecmp(content_type, "audio/flac") == 0 || strcasecmp(content_type, "audio/x-flac") == 0) {
return AudioFileType::FLAC;
}
#endif
#ifdef USE_AUDIO_OPUS_SUPPORT
// Match "audio/ogg" with a codecs parameter containing "opus"
// Valid forms: audio/ogg;codecs=opus, audio/ogg; codecs="opus", etc.
// Plain "audio/ogg" without opus is not matched (almost always Ogg Vorbis)
if (strncasecmp(content_type, "audio/ogg", 9) == 0 && strcasestr(content_type + 9, "opus") != nullptr) {
return AudioFileType::OPUS;
}
#endif
}
// Fallback to URL extension
if (url != nullptr && url[0] != '\0') {
if (str_endswith_ignore_case(url, ".wav")) {
return AudioFileType::WAV;
}
#ifdef USE_AUDIO_MP3_SUPPORT
if (str_endswith_ignore_case(url, ".mp3")) {
return AudioFileType::MP3;
}
#endif
#ifdef USE_AUDIO_FLAC_SUPPORT
if (str_endswith_ignore_case(url, ".flac")) {
return AudioFileType::FLAC;
}
#endif
#ifdef USE_AUDIO_OPUS_SUPPORT
if (str_endswith_ignore_case(url, ".opus")) {
return AudioFileType::OPUS;
}
#endif
}
return AudioFileType::NONE;
}
void scale_audio_samples(const int16_t *audio_samples, int16_t *output_buffer, int16_t scale_factor, void scale_audio_samples(const int16_t *audio_samples, int16_t *output_buffer, int16_t scale_factor,
size_t samples_to_scale) { size_t samples_to_scale) {
// Note the assembly dsps_mulc function has audio glitches if the input and output buffers are the same. // Note the assembly dsps_mulc function has audio glitches if the input and output buffers are the same.
+7
View File
@@ -130,6 +130,13 @@ struct AudioFile {
/// @return const char pointer to the readable file type /// @return const char pointer to the readable file type
const char *audio_file_type_to_string(AudioFileType file_type); const char *audio_file_type_to_string(AudioFileType file_type);
/// @brief Detect audio file type from a Content-Type header value and/or URL extension.
/// Tries Content-Type first, then falls back to URL extension. Either parameter may be null.
/// @param content_type Content-Type header value (may be null or empty)
/// @param url URL to inspect for file extension (may be null or empty)
/// @return The detected AudioFileType, or NONE if unknown
AudioFileType detect_audio_file_type(const char *content_type, const char *url);
/// @brief Scales Q15 fixed point audio samples. Scales in place if audio_samples == output_buffer. /// @brief Scales Q15 fixed point audio samples. Scales in place if audio_samples == output_buffer.
/// @param audio_samples PCM int16 audio samples /// @param audio_samples PCM int16 audio samples
/// @param output_buffer Buffer to store the scaled samples /// @param output_buffer Buffer to store the scaled samples
+3 -47
View File
@@ -185,26 +185,8 @@ esp_err_t AudioReader::start(const std::string &uri, AudioFileType &file_type) {
return err; return err;
} }
if (str_endswith_ignore_case(url, ".wav")) { file_type = detect_audio_file_type(nullptr, url);
file_type = AudioFileType::WAV; if (file_type == AudioFileType::NONE) {
}
#ifdef USE_AUDIO_MP3_SUPPORT
else if (str_endswith_ignore_case(url, ".mp3")) {
file_type = AudioFileType::MP3;
}
#endif
#ifdef USE_AUDIO_FLAC_SUPPORT
else if (str_endswith_ignore_case(url, ".flac")) {
file_type = AudioFileType::FLAC;
}
#endif
#ifdef USE_AUDIO_OPUS_SUPPORT
else if (str_endswith_ignore_case(url, ".opus")) {
file_type = AudioFileType::OPUS;
}
#endif
else {
file_type = AudioFileType::NONE;
this->cleanup_connection_(); this->cleanup_connection_();
return ESP_ERR_NOT_SUPPORTED; return ESP_ERR_NOT_SUPPORTED;
} }
@@ -232,32 +214,6 @@ AudioReaderState AudioReader::read() {
return AudioReaderState::FAILED; return AudioReaderState::FAILED;
} }
AudioFileType AudioReader::get_audio_type(const char *content_type) {
#ifdef USE_AUDIO_MP3_SUPPORT
if (strcasecmp(content_type, "mp3") == 0 || strcasecmp(content_type, "audio/mp3") == 0 ||
strcasecmp(content_type, "audio/mpeg") == 0) {
return AudioFileType::MP3;
}
#endif
if (strcasecmp(content_type, "audio/wav") == 0) {
return AudioFileType::WAV;
}
#ifdef USE_AUDIO_FLAC_SUPPORT
if (strcasecmp(content_type, "audio/flac") == 0 || strcasecmp(content_type, "audio/x-flac") == 0) {
return AudioFileType::FLAC;
}
#endif
#ifdef USE_AUDIO_OPUS_SUPPORT
// Match "audio/ogg" with a codecs parameter containing "opus"
// Valid forms: audio/ogg;codecs=opus, audio/ogg; codecs="opus", etc.
// Plain "audio/ogg" without a codecs parameter is not matched, as those are almost always Ogg Vorbis streams
if (strncasecmp(content_type, "audio/ogg", 9) == 0 && strcasestr(content_type + 9, "opus") != nullptr) {
return AudioFileType::OPUS;
}
#endif
return AudioFileType::NONE;
}
esp_err_t AudioReader::http_event_handler(esp_http_client_event_t *evt) { esp_err_t AudioReader::http_event_handler(esp_http_client_event_t *evt) {
// Based on https://github.com/maroc81/WeatherLily/tree/main/main/net accessed 20241224 // Based on https://github.com/maroc81/WeatherLily/tree/main/main/net accessed 20241224
AudioReader *this_reader = (AudioReader *) evt->user_data; AudioReader *this_reader = (AudioReader *) evt->user_data;
@@ -265,7 +221,7 @@ esp_err_t AudioReader::http_event_handler(esp_http_client_event_t *evt) {
switch (evt->event_id) { switch (evt->event_id) {
case HTTP_EVENT_ON_HEADER: case HTTP_EVENT_ON_HEADER:
if (strcasecmp(evt->header_key, "Content-Type") == 0) { if (strcasecmp(evt->header_key, "Content-Type") == 0) {
this_reader->audio_file_type_ = get_audio_type(evt->header_value); this_reader->audio_file_type_ = detect_audio_file_type(evt->header_value, nullptr);
} }
break; break;
default: default:
-5
View File
@@ -58,11 +58,6 @@ class AudioReader {
/// @brief Monitors the http client events to attempt determining the file type from the Content-Type header /// @brief Monitors the http client events to attempt determining the file type from the Content-Type header
static esp_err_t http_event_handler(esp_http_client_event_t *evt); static esp_err_t http_event_handler(esp_http_client_event_t *evt);
/// @brief Determines the audio file type from the http header's Content-Type key
/// @param content_type string with the Content-Type key
/// @return AudioFileType of the url, if it can be determined. If not, return AudioFileType::NONE.
static AudioFileType get_audio_type(const char *content_type);
AudioReaderState file_read_(); AudioReaderState file_read_();
AudioReaderState http_read_(); AudioReaderState http_read_();
+4 -1
View File
@@ -23,7 +23,10 @@ SET_MIC_GAIN_ACTION_SCHEMA = cv.maybe_simple_value(
@automation.register_action( @automation.register_action(
"audio_adc.set_mic_gain", SetMicGainAction, SET_MIC_GAIN_ACTION_SCHEMA "audio_adc.set_mic_gain",
SetMicGainAction,
SET_MIC_GAIN_ACTION_SCHEMA,
synchronous=True,
) )
async def audio_adc_set_mic_gain_to_code(config, action_id, template_arg, args): async def audio_adc_set_mic_gain_to_code(config, action_id, template_arg, args):
paren = await cg.get_variable(config[CONF_ID]) paren = await cg.get_variable(config[CONF_ID])
+10 -3
View File
@@ -31,15 +31,22 @@ SET_VOLUME_ACTION_SCHEMA = cv.maybe_simple_value(
) )
@automation.register_action("audio_dac.mute_off", MuteOffAction, MUTE_ACTION_SCHEMA) @automation.register_action(
@automation.register_action("audio_dac.mute_on", MuteOnAction, MUTE_ACTION_SCHEMA) "audio_dac.mute_off", MuteOffAction, MUTE_ACTION_SCHEMA, synchronous=True
)
@automation.register_action(
"audio_dac.mute_on", MuteOnAction, MUTE_ACTION_SCHEMA, synchronous=True
)
async def audio_dac_mute_action_to_code(config, action_id, template_arg, args): async def audio_dac_mute_action_to_code(config, action_id, template_arg, args):
paren = await cg.get_variable(config[CONF_ID]) paren = await cg.get_variable(config[CONF_ID])
return cg.new_Pvariable(action_id, template_arg, paren) return cg.new_Pvariable(action_id, template_arg, paren)
@automation.register_action( @automation.register_action(
"audio_dac.set_volume", SetVolumeAction, SET_VOLUME_ACTION_SCHEMA "audio_dac.set_volume",
SetVolumeAction,
SET_VOLUME_ACTION_SCHEMA,
synchronous=True,
) )
async def audio_dac_set_volume_to_code(config, action_id, template_arg, args): async def audio_dac_set_volume_to_code(config, action_id, template_arg, args):
paren = await cg.get_variable(config[CONF_ID]) paren = await cg.get_variable(config[CONF_ID])
+255
View File
@@ -0,0 +1,255 @@
from dataclasses import dataclass, field
import hashlib
import logging
from pathlib import Path
import puremagic
from esphome import external_files
import esphome.codegen as cg
from esphome.components import audio
import esphome.config_validation as cv
from esphome.const import (
CONF_FILE,
CONF_ID,
CONF_PATH,
CONF_RAW_DATA_ID,
CONF_TYPE,
CONF_URL,
)
from esphome.core import CORE, ID, HexInt
from esphome.cpp_generator import MockObj
from esphome.external_files import download_content
from esphome.types import ConfigType
_LOGGER = logging.getLogger(__name__)
CODEOWNERS = ["@kahrendt"]
AUTO_LOAD = ["audio"]
DOMAIN = "audio_file"
audio_file_ns = cg.esphome_ns.namespace("audio_file")
TYPE_LOCAL = "local"
TYPE_WEB = "web"
@dataclass
class AudioFileData:
file_ids: dict[str, ID] = field(default_factory=dict)
file_cache: dict[str, tuple[bytes, MockObj]] = field(default_factory=dict)
def _get_data() -> AudioFileData:
if DOMAIN not in CORE.data:
CORE.data[DOMAIN] = AudioFileData()
return CORE.data[DOMAIN]
def get_audio_file_ids() -> dict[str, ID]:
"""Get all registered audio file IDs for cross-component access."""
return _get_data().file_ids
def _compute_local_file_path(value: ConfigType) -> Path:
url = value[CONF_URL]
h = hashlib.new("sha256")
h.update(url.encode())
key = h.hexdigest()[:8]
base_dir = external_files.compute_local_file_dir(DOMAIN)
_LOGGER.debug("_compute_local_file_path: base_dir=%s", base_dir / key)
return base_dir / key
def _download_web_file(value: ConfigType) -> ConfigType:
url = value[CONF_URL]
path = _compute_local_file_path(value)
download_content(url, path)
_LOGGER.debug("download_web_file: path=%s", path)
return value
def _file_schema(value: ConfigType | str) -> ConfigType:
if isinstance(value, str):
return _validate_file_shorthand(value)
return TYPED_FILE_SCHEMA(value)
def _validate_file_shorthand(value: str) -> ConfigType:
value = cv.string_strict(value)
if value.startswith("http://") or value.startswith("https://"):
return _file_schema(
{
CONF_TYPE: TYPE_WEB,
CONF_URL: value,
}
)
return _file_schema(
{
CONF_TYPE: TYPE_LOCAL,
CONF_PATH: value,
}
)
def read_audio_file_and_type(file_config: ConfigType) -> tuple[bytes, MockObj]:
"""Read an audio file and determine its type. Used by this component and media_source platform."""
conf_file = file_config[CONF_FILE]
file_source = conf_file[CONF_TYPE]
if file_source == TYPE_LOCAL:
path = CORE.relative_config_path(conf_file[CONF_PATH])
elif file_source == TYPE_WEB:
path = _compute_local_file_path(conf_file)
else:
raise cv.Invalid("Unsupported file source")
with open(path, "rb") as f:
data = f.read()
try:
file_type: str = puremagic.from_string(data)
file_type = file_type.removeprefix(".")
except puremagic.PureError as e:
raise cv.Invalid(
f"Unable to determine audio file type of '{path}'. "
f"Try re-encoding the file into a supported format. Details: {e}"
)
media_file_type = audio.AUDIO_FILE_TYPE_ENUM["NONE"]
if file_type == "wav":
media_file_type = audio.AUDIO_FILE_TYPE_ENUM["WAV"]
elif file_type in ("mp3", "mpeg", "mpga"):
media_file_type = audio.AUDIO_FILE_TYPE_ENUM["MP3"]
elif file_type == "flac":
media_file_type = audio.AUDIO_FILE_TYPE_ENUM["FLAC"]
elif (
file_type == "ogg"
and len(data) >= 36
and data.startswith(b"OggS")
and data[28:36] == b"OpusHead"
):
media_file_type = audio.AUDIO_FILE_TYPE_ENUM["OPUS"]
return data, media_file_type
LOCAL_SCHEMA = cv.Schema(
{
cv.Required(CONF_PATH): cv.file_,
}
)
WEB_SCHEMA = cv.All(
{
cv.Required(CONF_URL): cv.url,
},
_download_web_file,
)
TYPED_FILE_SCHEMA = cv.typed_schema(
{
TYPE_LOCAL: LOCAL_SCHEMA,
TYPE_WEB: WEB_SCHEMA,
},
)
MEDIA_FILE_TYPE_SCHEMA = cv.Schema(
{
cv.Required(CONF_ID): cv.declare_id(audio.AudioFile),
cv.Required(CONF_FILE): _file_schema,
cv.GenerateID(CONF_RAW_DATA_ID): cv.declare_id(cg.uint8),
}
)
MAX_FILE_SIZE = 5 * 1024 * 1024 # 5 MB
def _validate_supported_local_file(config: list[ConfigType]) -> list[ConfigType]:
for file_config in config:
data, media_file_type = read_audio_file_and_type(file_config)
if len(data) > MAX_FILE_SIZE:
file_info = file_config.get(CONF_FILE, {})
source = (
file_info.get(CONF_PATH) or file_info.get(CONF_URL) or "unknown source"
)
raise cv.Invalid(
f"Audio file {source!r} is too large ({len(data)} bytes, max {MAX_FILE_SIZE} bytes)"
)
if str(media_file_type) == str(audio.AUDIO_FILE_TYPE_ENUM["NONE"]):
file_info = file_config.get(CONF_FILE, {})
source = (
file_info.get(CONF_PATH) or file_info.get(CONF_URL) or "unknown source"
)
raise cv.Invalid(
f"Unsupported media file from {source!r} (detected type: {media_file_type})"
)
# Cache the file data so to_code() doesn't need to re-read it
_get_data().file_cache[str(file_config[CONF_ID])] = (data, media_file_type)
media_file_type_str = str(media_file_type)
if media_file_type_str == str(audio.AUDIO_FILE_TYPE_ENUM["FLAC"]):
audio.request_flac_support()
elif media_file_type_str == str(audio.AUDIO_FILE_TYPE_ENUM["MP3"]):
audio.request_mp3_support()
elif media_file_type_str == str(audio.AUDIO_FILE_TYPE_ENUM["OPUS"]):
audio.request_opus_support()
return config
CONFIG_SCHEMA = cv.All(
cv.only_on_esp32,
cv.ensure_list(MEDIA_FILE_TYPE_SCHEMA),
_validate_supported_local_file,
)
async def to_code(config: list[ConfigType]) -> None:
cache = _get_data().file_cache
for file_config in config:
file_id = str(file_config[CONF_ID])
data, media_file_type = cache[file_id]
rhs = [HexInt(x) for x in data]
prog_arr = cg.progmem_array(file_config[CONF_RAW_DATA_ID], rhs)
media_files_struct = cg.StructInitializer(
audio.AudioFile,
(
"data",
prog_arr,
),
(
"length",
len(rhs),
),
(
"file_type",
media_file_type,
),
)
cg.new_Pvariable(
file_config[CONF_ID],
media_files_struct,
)
# Store file ID for cross-component access
_get_data().file_ids[file_id] = file_config[CONF_ID]
# Register all files in the shared C++ registry
cg.add_define("AUDIO_FILE_MAX_FILES", len(config))
for file_config in config:
file_id = str(file_config[CONF_ID])
file_var = await cg.get_variable(file_config[CONF_ID])
cg.add(audio_file_ns.add_named_audio_file(file_var, file_id))
@@ -0,0 +1,28 @@
#pragma once
#include "esphome/core/defines.h"
#ifdef AUDIO_FILE_MAX_FILES
#include "esphome/components/audio/audio.h"
#include "esphome/core/helpers.h"
namespace esphome::audio_file {
struct NamedAudioFile {
audio::AudioFile *file;
const char *file_id;
};
inline StaticVector<NamedAudioFile, AUDIO_FILE_MAX_FILES>
named_audio_files; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
inline void add_named_audio_file(audio::AudioFile *file, const char *file_id) {
named_audio_files.push_back({file, file_id});
}
inline const StaticVector<NamedAudioFile, AUDIO_FILE_MAX_FILES> &get_named_audio_files() { return named_audio_files; }
} // namespace esphome::audio_file
#endif // AUDIO_FILE_MAX_FILES
@@ -0,0 +1,38 @@
import esphome.codegen as cg
from esphome.components import media_source, psram
import esphome.config_validation as cv
from esphome.const import CONF_ID, CONF_TASK_STACK_IN_PSRAM
from esphome.types import ConfigType
CODEOWNERS = ["@kahrendt"]
AUTO_LOAD = ["audio"]
DEPENDENCIES = ["audio_file"]
audio_file_ns = cg.esphome_ns.namespace("audio_file")
AudioFileMediaSource = audio_file_ns.class_(
"AudioFileMediaSource", cg.Component, media_source.MediaSource
)
CONFIG_SCHEMA = cv.All(
media_source.media_source_schema(
AudioFileMediaSource,
)
.extend(
{
cv.Optional(CONF_TASK_STACK_IN_PSRAM): cv.All(
cv.boolean, cv.requires_component(psram.DOMAIN)
),
}
)
.extend(cv.COMPONENT_SCHEMA),
cv.only_on_esp32,
)
async def to_code(config: ConfigType) -> None:
var = cg.new_Pvariable(config[CONF_ID])
await cg.register_component(var, config)
await media_source.register_media_source(var, config)
if CONF_TASK_STACK_IN_PSRAM in config:
cg.add(var.set_task_stack_in_psram(config[CONF_TASK_STACK_IN_PSRAM]))
@@ -0,0 +1,283 @@
#include "audio_file_media_source.h"
#ifdef USE_ESP32
#include "esphome/components/audio/audio_decoder.h"
#include <cstring>
namespace esphome::audio_file {
namespace { // anonymous namespace for internal linkage
struct AudioSinkAdapter : public audio::AudioSinkCallback {
media_source::MediaSource *source;
audio::AudioStreamInfo stream_info;
size_t audio_sink_write(uint8_t *data, size_t length, TickType_t ticks_to_wait) override {
return this->source->write_output(data, length, pdTICKS_TO_MS(ticks_to_wait), this->stream_info);
}
};
} // namespace
#if defined(USE_AUDIO_OPUS_SUPPORT)
static constexpr uint32_t DECODE_TASK_STACK_SIZE = 5 * 1024;
#else
static constexpr uint32_t DECODE_TASK_STACK_SIZE = 3 * 1024;
#endif
static const char *const TAG = "audio_file_media_source";
enum EventGroupBits : uint32_t {
// Requests to start playback (set by play_uri, handled by loop)
REQUEST_START = (1 << 0),
// Commands from main loop to decode task
COMMAND_STOP = (1 << 1),
COMMAND_PAUSE = (1 << 2),
// Decode task lifecycle signals (one-shot, cleared by loop)
TASK_STARTING = (1 << 7),
TASK_RUNNING = (1 << 8),
TASK_STOPPING = (1 << 9),
TASK_STOPPED = (1 << 10),
TASK_ERROR = (1 << 11),
// Decode task state (level-triggered, set/cleared by decode task)
TASK_PAUSED = (1 << 12),
ALL_BITS = 0x00FFFFFF, // All valid FreeRTOS event group bits
};
void AudioFileMediaSource::dump_config() {
ESP_LOGCONFIG(TAG, "Audio File Media Source:");
ESP_LOGCONFIG(TAG, " Task Stack in PSRAM: %s", this->task_stack_in_psram_ ? "Yes" : "No");
}
void AudioFileMediaSource::setup() {
this->disable_loop();
this->event_group_ = xEventGroupCreate();
if (this->event_group_ == nullptr) {
ESP_LOGE(TAG, "Failed to create event group");
this->mark_failed();
return;
}
}
void AudioFileMediaSource::loop() {
EventBits_t event_bits = xEventGroupGetBits(this->event_group_);
if (event_bits & REQUEST_START) {
xEventGroupClearBits(this->event_group_, REQUEST_START);
this->decoding_state_ = AudioFileDecodingState::START_TASK;
}
switch (this->decoding_state_) {
case AudioFileDecodingState::START_TASK: {
if (!this->decode_task_.is_created()) {
xEventGroupClearBits(this->event_group_, ALL_BITS);
if (!this->decode_task_.create(decode_task, "AudioFileDec", DECODE_TASK_STACK_SIZE, this, 1,
this->task_stack_in_psram_)) {
ESP_LOGE(TAG, "Failed to create task");
this->status_momentary_error("task_create", 1000);
this->set_state_(media_source::MediaSourceState::ERROR);
this->decoding_state_ = AudioFileDecodingState::IDLE;
return;
}
}
this->decoding_state_ = AudioFileDecodingState::DECODING;
break;
}
case AudioFileDecodingState::DECODING: {
if (event_bits & TASK_STARTING) {
ESP_LOGD(TAG, "Starting");
xEventGroupClearBits(this->event_group_, TASK_STARTING);
}
if (event_bits & TASK_RUNNING) {
ESP_LOGV(TAG, "Started");
xEventGroupClearBits(this->event_group_, TASK_RUNNING);
this->set_state_(media_source::MediaSourceState::PLAYING);
}
if ((event_bits & TASK_PAUSED) && this->get_state() != media_source::MediaSourceState::PAUSED) {
this->set_state_(media_source::MediaSourceState::PAUSED);
} else if (!(event_bits & TASK_PAUSED) && this->get_state() == media_source::MediaSourceState::PAUSED) {
this->set_state_(media_source::MediaSourceState::PLAYING);
}
if (event_bits & TASK_STOPPING) {
ESP_LOGV(TAG, "Stopping");
xEventGroupClearBits(this->event_group_, TASK_STOPPING);
}
if (event_bits & TASK_ERROR) {
// Report error so the orchestrator knows playback failed; task will have already logged the specific error
this->set_state_(media_source::MediaSourceState::ERROR);
}
if (event_bits & TASK_STOPPED) {
ESP_LOGD(TAG, "Stopped");
xEventGroupClearBits(this->event_group_, ALL_BITS);
this->decode_task_.deallocate();
this->set_state_(media_source::MediaSourceState::IDLE);
this->decoding_state_ = AudioFileDecodingState::IDLE;
}
break;
}
case AudioFileDecodingState::IDLE: {
if (this->get_state() == media_source::MediaSourceState::ERROR && !this->status_has_error()) {
this->set_state_(media_source::MediaSourceState::IDLE);
}
break;
}
}
if ((this->decoding_state_ == AudioFileDecodingState::IDLE) &&
(this->get_state() == media_source::MediaSourceState::IDLE)) {
this->disable_loop();
}
}
// Called from the orchestrator's main loop, so no synchronization needed with loop()
bool AudioFileMediaSource::play_uri(const std::string &uri) {
if (!this->is_ready() || this->is_failed() || this->status_has_error() || !this->has_listener() ||
xEventGroupGetBits(this->event_group_) & REQUEST_START) {
return false;
}
// Check if source is already playing
if (this->get_state() != media_source::MediaSourceState::IDLE) {
ESP_LOGE(TAG, "Cannot play '%s': source is busy", uri.c_str());
return false;
}
// Validate URI starts with "audio-file://"
if (!uri.starts_with("audio-file://")) {
ESP_LOGE(TAG, "Invalid URI: '%s'", uri.c_str());
return false;
}
// Strip "audio-file://" prefix and find the file
const char *file_id = uri.c_str() + 13; // "audio-file://" is 13 characters
for (const auto &named_file : get_named_audio_files()) {
if (strcmp(named_file.file_id, file_id) == 0) {
this->current_file_ = named_file.file;
xEventGroupSetBits(this->event_group_, EventGroupBits::REQUEST_START);
this->enable_loop();
return true;
}
}
ESP_LOGE(TAG, "Unknown file: '%s'", file_id);
return false;
}
// Called from the orchestrator's main loop, so no synchronization needed with loop()
void AudioFileMediaSource::handle_command(media_source::MediaSourceCommand command) {
if (this->decoding_state_ != AudioFileDecodingState::DECODING) {
return;
}
switch (command) {
case media_source::MediaSourceCommand::STOP:
xEventGroupSetBits(this->event_group_, EventGroupBits::COMMAND_STOP);
break;
case media_source::MediaSourceCommand::PAUSE:
xEventGroupSetBits(this->event_group_, EventGroupBits::COMMAND_PAUSE);
break;
case media_source::MediaSourceCommand::PLAY:
xEventGroupClearBits(this->event_group_, EventGroupBits::COMMAND_PAUSE);
break;
default:
break;
}
}
void AudioFileMediaSource::decode_task(void *params) {
AudioFileMediaSource *this_source = static_cast<AudioFileMediaSource *>(params);
do { // do-while(false) ensures RAII objects are destroyed on all exit paths via break
xEventGroupSetBits(this_source->event_group_, EventGroupBits::TASK_STARTING);
// 0 bytes for input transfer buffer makes it an inplace buffer
std::unique_ptr<audio::AudioDecoder> decoder = make_unique<audio::AudioDecoder>(0, 4096);
esp_err_t err = decoder->start(this_source->current_file_->file_type);
if (err != ESP_OK) {
ESP_LOGE(TAG, "Failed to start decoder: %s", esp_err_to_name(err));
xEventGroupSetBits(this_source->event_group_, EventGroupBits::TASK_ERROR | EventGroupBits::TASK_STOPPING);
break;
}
// Add the file as a const data source
decoder->add_source(this_source->current_file_->data, this_source->current_file_->length);
xEventGroupSetBits(this_source->event_group_, EventGroupBits::TASK_RUNNING);
AudioSinkAdapter audio_sink;
bool has_stream_info = false;
while (true) {
EventBits_t event_bits = xEventGroupGetBits(this_source->event_group_);
if (event_bits & EventGroupBits::COMMAND_STOP) {
break;
}
bool paused = event_bits & EventGroupBits::COMMAND_PAUSE;
decoder->set_pause_output_state(paused);
if (paused) {
xEventGroupSetBits(this_source->event_group_, EventGroupBits::TASK_PAUSED);
vTaskDelay(pdMS_TO_TICKS(20));
} else {
xEventGroupClearBits(this_source->event_group_, EventGroupBits::TASK_PAUSED);
}
// Will stop gracefully once finished with the current file
audio::AudioDecoderState decoder_state = decoder->decode(true);
if (decoder_state == audio::AudioDecoderState::FINISHED) {
break;
} else if (decoder_state == audio::AudioDecoderState::FAILED) {
ESP_LOGE(TAG, "Decoder failed");
xEventGroupSetBits(this_source->event_group_, EventGroupBits::TASK_ERROR);
break;
}
if (!has_stream_info && decoder->get_audio_stream_info().has_value()) {
has_stream_info = true;
audio::AudioStreamInfo stream_info = decoder->get_audio_stream_info().value();
ESP_LOGD(TAG, "Bits per sample: %d, Channels: %d, Sample rate: %d", stream_info.get_bits_per_sample(),
stream_info.get_channels(), stream_info.get_sample_rate());
if (stream_info.get_bits_per_sample() != 16 || stream_info.get_channels() > 2) {
ESP_LOGE(TAG, "Incompatible audio stream. Only 16 bits per sample and 1 or 2 channels are supported");
xEventGroupSetBits(this_source->event_group_, EventGroupBits::TASK_ERROR);
break;
}
audio_sink.source = this_source;
audio_sink.stream_info = stream_info;
esp_err_t err = decoder->add_sink(&audio_sink);
if (err != ESP_OK) {
ESP_LOGE(TAG, "Failed to add sink: %s", esp_err_to_name(err));
xEventGroupSetBits(this_source->event_group_, EventGroupBits::TASK_ERROR);
break;
}
}
}
xEventGroupSetBits(this_source->event_group_, EventGroupBits::TASK_STOPPING);
} while (false);
// All RAII objects from the do-while block (decoder, audio_sink, etc.) are now destroyed.
xEventGroupSetBits(this_source->event_group_, EventGroupBits::TASK_STOPPED);
vTaskSuspend(nullptr); // Suspend this task indefinitely until the loop method deletes it
}
} // namespace esphome::audio_file
#endif // USE_ESP32
@@ -0,0 +1,50 @@
#pragma once
#include "esphome/core/defines.h"
#ifdef USE_ESP32
#include "esphome/components/audio/audio.h"
#include "esphome/components/audio_file/audio_file.h"
#include "esphome/components/media_source/media_source.h"
#include "esphome/core/component.h"
#include "esphome/core/static_task.h"
#include <freertos/FreeRTOS.h>
#include <freertos/event_groups.h>
namespace esphome::audio_file {
enum class AudioFileDecodingState : uint8_t {
START_TASK,
DECODING,
IDLE,
};
class AudioFileMediaSource : public Component, public media_source::MediaSource {
public:
void setup() override;
void loop() override;
void dump_config() override;
// MediaSource interface implementation
bool play_uri(const std::string &uri) override;
void handle_command(media_source::MediaSourceCommand command) override;
bool can_handle(const std::string &uri) const override { return uri.starts_with("audio-file://"); }
void set_task_stack_in_psram(bool task_stack_in_psram) { this->task_stack_in_psram_ = task_stack_in_psram; }
protected:
static void decode_task(void *params);
audio::AudioFile *current_file_{nullptr};
AudioFileDecodingState decoding_state_{AudioFileDecodingState::IDLE};
EventGroupHandle_t event_group_{nullptr};
StaticTask decode_task_;
bool task_stack_in_psram_{false};
};
} // namespace esphome::audio_file
#endif // USE_ESP32
@@ -38,6 +38,11 @@ bool BParasite::parse_device(const esp32_ble_tracker::ESPBTDevice &device) {
const auto &data = service_data.data; const auto &data = service_data.data;
if (data.size() < 10) {
ESP_LOGW(TAG, "Service data too short: %zu", data.size());
return false;
}
const uint8_t protocol_version = data[0] >> 4; const uint8_t protocol_version = data[0] >> 4;
if (protocol_version != 1 && protocol_version != 2) { if (protocol_version != 1 && protocol_version != 2) {
ESP_LOGE(TAG, "Unsupported protocol version: %u", protocol_version); ESP_LOGE(TAG, "Unsupported protocol version: %u", protocol_version);
@@ -47,6 +52,11 @@ bool BParasite::parse_device(const esp32_ble_tracker::ESPBTDevice &device) {
// Some b-parasite versions have an (optional) illuminance sensor. // Some b-parasite versions have an (optional) illuminance sensor.
bool has_illuminance = data[0] & 0x1; bool has_illuminance = data[0] & 0x1;
if (has_illuminance && data.size() < 18) {
ESP_LOGW(TAG, "Service data too short for illuminance: %zu", data.size());
return false;
}
// Counter for deduplicating messages. // Counter for deduplicating messages.
uint8_t counter = data[1] & 0x0f; uint8_t counter = data[1] & 0x0f;
if (last_processed_counter_ == counter) { if (last_processed_counter_ == counter) {
+1 -1
View File
@@ -47,7 +47,7 @@ void BalluClimate::transmit_state() {
remote_state[11] = 0x1e; remote_state[11] = 0x1e;
// Fan speed // Fan speed
switch (this->fan_mode.value()) { switch (this->fan_mode.value_or(climate::CLIMATE_FAN_ON)) {
case climate::CLIMATE_FAN_HIGH: case climate::CLIMATE_FAN_HIGH:
remote_state[4] |= BALLU_FAN_HIGH; remote_state[4] |= BALLU_FAN_HIGH;
break; break;
@@ -45,17 +45,21 @@ void BangBangClimate::setup() {
} }
void BangBangClimate::control(const climate::ClimateCall &call) { void BangBangClimate::control(const climate::ClimateCall &call) {
if (call.get_mode().has_value()) { auto mode = call.get_mode();
this->mode = *call.get_mode(); if (mode.has_value()) {
this->mode = *mode;
} }
if (call.get_target_temperature_low().has_value()) { auto target_temperature_low = call.get_target_temperature_low();
this->target_temperature_low = *call.get_target_temperature_low(); if (target_temperature_low.has_value()) {
this->target_temperature_low = *target_temperature_low;
} }
if (call.get_target_temperature_high().has_value()) { auto target_temperature_high = call.get_target_temperature_high();
this->target_temperature_high = *call.get_target_temperature_high(); if (target_temperature_high.has_value()) {
this->target_temperature_high = *target_temperature_high;
} }
if (call.get_preset().has_value()) { auto preset = call.get_preset();
this->change_away_(*call.get_preset() == climate::CLIMATE_PRESET_AWAY); if (preset.has_value()) {
this->change_away_(*preset == climate::CLIMATE_PRESET_AWAY);
} }
this->compute_state_(); this->compute_state_();
+20 -8
View File
@@ -1,4 +1,5 @@
#include "bedjet_codec.h" #include "bedjet_codec.h"
#include <algorithm>
#include <cstdio> #include <cstdio>
#include <cstring> #include <cstring>
@@ -68,6 +69,10 @@ BedjetPacket *BedjetCodec::get_set_runtime_remaining_request(const uint8_t hour,
/** Decodes the extra bytes that were received after being notified with a partial packet. */ /** Decodes the extra bytes that were received after being notified with a partial packet. */
void BedjetCodec::decode_extra(const uint8_t *data, uint16_t length) { void BedjetCodec::decode_extra(const uint8_t *data, uint16_t length) {
if (length < 5) {
ESP_LOGVV(TAG, "Received extra: %d bytes (too short)", length);
return;
}
ESP_LOGVV(TAG, "Received extra: %d bytes: %d %d %d %d", length, data[1], data[2], data[3], data[4]); ESP_LOGVV(TAG, "Received extra: %d bytes: %d %d %d %d", length, data[1], data[2], data[3], data[4]);
uint8_t offset = this->last_buffer_size_; uint8_t offset = this->last_buffer_size_;
if (offset > 0 && length + offset <= sizeof(BedjetStatusPacket)) { if (offset > 0 && length + offset <= sizeof(BedjetStatusPacket)) {
@@ -90,14 +95,19 @@ void BedjetCodec::decode_extra(const uint8_t *data, uint16_t length) {
* @return `true` if the packet was decoded and represents a "partial" packet; `false` otherwise. * @return `true` if the packet was decoded and represents a "partial" packet; `false` otherwise.
*/ */
bool BedjetCodec::decode_notify(const uint8_t *data, uint16_t length) { bool BedjetCodec::decode_notify(const uint8_t *data, uint16_t length) {
if (length < 5) {
ESP_LOGW(TAG, "Received short packet: %d bytes", length);
return false;
}
ESP_LOGV(TAG, "Received: %d bytes: %d %d %d %d", length, data[1], data[2], data[3], data[4]); ESP_LOGV(TAG, "Received: %d bytes: %d %d %d %d", length, data[1], data[2], data[3], data[4]);
if (data[1] == PACKET_FORMAT_V3_HOME && data[3] == PACKET_TYPE_STATUS) { if (data[1] == PACKET_FORMAT_V3_HOME && data[3] == PACKET_TYPE_STATUS) {
// Clear old buffer // Clear old buffer
memset(&this->buf_, 0, sizeof(BedjetStatusPacket)); memset(&this->buf_, 0, sizeof(BedjetStatusPacket));
// Copy new data into buffer // Copy new data into buffer
memcpy(&this->buf_, data, length); size_t copy_len = std::min(static_cast<size_t>(length), sizeof(BedjetStatusPacket));
this->last_buffer_size_ = length; memcpy(&this->buf_, data, copy_len);
this->last_buffer_size_ = copy_len;
// TODO: validate the packet checksum? // TODO: validate the packet checksum?
if (this->buf_.mode < 7 && this->buf_.target_temp_step >= 38 && this->buf_.target_temp_step <= 86 && if (this->buf_.mode < 7 && this->buf_.target_temp_step >= 38 && this->buf_.target_temp_step <= 86 &&
@@ -113,13 +123,15 @@ bool BedjetCodec::decode_notify(const uint8_t *data, uint16_t length) {
} }
} else if (data[1] == PACKET_FORMAT_DEBUG || data[3] == PACKET_TYPE_DEBUG) { } else if (data[1] == PACKET_FORMAT_DEBUG || data[3] == PACKET_TYPE_DEBUG) {
// We don't actually know the packet format for this. Dump packets to log, in case a pattern presents itself. // We don't actually know the packet format for this. Dump packets to log, in case a pattern presents itself.
ESP_LOGVV(TAG, if (length >= 13) {
"received DEBUG packet: set1=%01fF, set2=%01fF, air=%01fF; [7]=%d, [8]=%d, [9]=%d, [10]=%d, [11]=%d, " ESP_LOGVV(TAG,
"[12]=%d, [-1]=%d", "received DEBUG packet: set1=%01fF, set2=%01fF, air=%01fF; [7]=%d, [8]=%d, [9]=%d, [10]=%d, [11]=%d, "
bedjet_temp_to_f(data[4]), bedjet_temp_to_f(data[5]), bedjet_temp_to_f(data[6]), data[7], data[8], "[12]=%d, [-1]=%d",
data[9], data[10], data[11], data[12], data[length - 1]); bedjet_temp_to_f(data[4]), bedjet_temp_to_f(data[5]), bedjet_temp_to_f(data[6]), data[7], data[8],
data[9], data[10], data[11], data[12], data[length - 1]);
}
if (this->has_status()) { if (this->has_status() && length >= 7) {
this->status_packet_->ambient_temp_step = data[6]; this->status_packet_->ambient_temp_step = data[6];
} }
} else { } else {
+4 -4
View File
@@ -164,10 +164,10 @@ class BedJetHub : public esphome::ble_client::BLEClientNode, public PollingCompo
std::unique_ptr<BedjetCodec> codec_; std::unique_ptr<BedjetCodec> codec_;
bool discover_characteristics_(); bool discover_characteristics_();
uint16_t char_handle_cmd_; uint16_t char_handle_cmd_{0};
uint16_t char_handle_name_; uint16_t char_handle_name_{0};
uint16_t char_handle_status_; uint16_t char_handle_status_{0};
uint16_t config_descr_status_; uint16_t config_descr_status_{0};
uint8_t write_notify_config_descriptor_(bool enable); uint8_t write_notify_config_descriptor_(bool enable);
}; };
@@ -96,8 +96,9 @@ void BedJetClimate::control(const ClimateCall &call) {
return; return;
} }
if (call.get_mode().has_value()) { auto mode_opt = call.get_mode();
ClimateMode mode = *call.get_mode(); if (mode_opt.has_value()) {
ClimateMode mode = *mode_opt;
bool button_result; bool button_result;
switch (mode) { switch (mode) {
case CLIMATE_MODE_OFF: case CLIMATE_MODE_OFF:
@@ -125,8 +126,9 @@ void BedJetClimate::control(const ClimateCall &call) {
} }
} }
if (call.get_target_temperature().has_value()) { auto target_temp_opt = call.get_target_temperature();
auto target_temp = *call.get_target_temperature(); if (target_temp_opt.has_value()) {
auto target_temp = *target_temp_opt;
auto result = this->parent_->set_target_temp(target_temp); auto result = this->parent_->set_target_temp(target_temp);
if (result) { if (result) {
@@ -134,8 +136,9 @@ void BedJetClimate::control(const ClimateCall &call) {
} }
} }
if (call.get_preset().has_value()) { auto preset_opt = call.get_preset();
ClimatePreset preset = *call.get_preset(); if (preset_opt.has_value()) {
ClimatePreset preset = *preset_opt;
bool result; bool result;
if (preset == CLIMATE_PRESET_BOOST) { if (preset == CLIMATE_PRESET_BOOST) {
@@ -187,10 +190,11 @@ void BedJetClimate::control(const ClimateCall &call) {
} }
} }
if (call.get_fan_mode().has_value()) { auto fan_mode_opt = call.get_fan_mode();
if (fan_mode_opt.has_value()) {
// Climate fan mode only supports low/med/high, but the BedJet supports 5-100% increments. // Climate fan mode only supports low/med/high, but the BedJet supports 5-100% increments.
// We can still support a ClimateCall that requests low/med/high, and just translate it to a step increment here. // We can still support a ClimateCall that requests low/med/high, and just translate it to a step increment here.
auto fan_mode = *call.get_fan_mode(); auto fan_mode = *fan_mode_opt;
bool result; bool result;
if (fan_mode == CLIMATE_FAN_LOW) { if (fan_mode == CLIMATE_FAN_LOW) {
result = this->parent_->set_fan_speed(20); result = this->parent_->set_fan_speed(20);
+5 -3
View File
@@ -19,7 +19,8 @@ void BedJetFan::control(const fan::FanCall &call) {
} }
bool did_change = false; bool did_change = false;
if (call.get_state().has_value() && this->state != *call.get_state()) { auto state_opt = call.get_state();
if (state_opt.has_value() && this->state != *state_opt) {
// Turning off is easy: // Turning off is easy:
if (this->state && this->parent_->button_off()) { if (this->state && this->parent_->button_off()) {
this->state = false; this->state = false;
@@ -36,8 +37,9 @@ void BedJetFan::control(const fan::FanCall &call) {
} }
// ignore speed changes if not on or turning on // ignore speed changes if not on or turning on
if (this->state && call.get_speed().has_value()) { auto speed_opt = call.get_speed();
auto speed = *call.get_speed(); if (this->state && speed_opt.has_value()) {
auto speed = *speed_opt;
if (speed >= 1) { if (speed >= 1) {
this->speed = speed; this->speed = speed;
// Fan.speed is 1-20, but Bedjet expects 0-19, so subtract 1 // Fan.speed is 1-20, but Bedjet expects 0-19, so subtract 1
@@ -78,7 +78,7 @@ static void spi_set_clock(uint32_t max_hz) {
int source_clk = 0; int source_clk = 0;
int spi_clk = 0; int spi_clk = 0;
int div = 0; int div = 0;
uint32_t param; uint32_t param = PWD_SPI_CLK_BIT;
if (max_hz > 4333000) { if (max_hz > 4333000) {
if (max_hz > 30000000) { if (max_hz > 30000000) {
spi_clk = 30000000; spi_clk = 30000000;
+9 -6
View File
@@ -18,12 +18,15 @@ fan::FanTraits BinaryFan::get_traits() {
return fan::FanTraits(this->oscillating_ != nullptr, false, this->direction_ != nullptr, 0); return fan::FanTraits(this->oscillating_ != nullptr, false, this->direction_ != nullptr, 0);
} }
void BinaryFan::control(const fan::FanCall &call) { void BinaryFan::control(const fan::FanCall &call) {
if (call.get_state().has_value()) auto state = call.get_state();
this->state = *call.get_state(); if (state.has_value())
if (call.get_oscillating().has_value()) this->state = *state;
this->oscillating = *call.get_oscillating(); auto oscillating = call.get_oscillating();
if (call.get_direction().has_value()) if (oscillating.has_value())
this->direction = *call.get_direction(); this->oscillating = *oscillating;
auto direction = call.get_direction();
if (direction.has_value())
this->direction = *direction;
this->write_state_(); this->write_state_();
this->publish_state(); this->publish_state();
+8 -5
View File
@@ -60,7 +60,11 @@ from esphome.const import (
DEVICE_CLASS_WINDOW, DEVICE_CLASS_WINDOW,
) )
from esphome.core import CORE, CoroPriority, coroutine_with_priority from esphome.core import CORE, CoroPriority, coroutine_with_priority
from esphome.core.entity_helpers import entity_duplicate_validator, setup_entity from esphome.core.entity_helpers import (
entity_duplicate_validator,
setup_device_class,
setup_entity,
)
from esphome.cpp_generator import MockObjClass from esphome.cpp_generator import MockObjClass
from esphome.util import Registry from esphome.util import Registry
@@ -604,11 +608,9 @@ async def _build_binary_sensor_automations(var, config):
) )
@setup_entity("binary_sensor")
async def setup_binary_sensor_core_(var, config): async def setup_binary_sensor_core_(var, config):
await setup_entity(var, config, "binary_sensor") setup_device_class(config)
if (device_class := config.get(CONF_DEVICE_CLASS)) is not None:
cg.add(var.set_device_class(device_class))
trigger = config.get(CONF_TRIGGER_ON_INITIAL_STATE, False) or config.get( trigger = config.get(CONF_TRIGGER_ON_INITIAL_STATE, False) or config.get(
CONF_PUBLISH_INITIAL_STATE, False CONF_PUBLISH_INITIAL_STATE, False
) )
@@ -683,6 +685,7 @@ async def to_code(config):
}, },
key=CONF_ID, key=CONF_ID,
), ),
synchronous=True,
) )
async def binary_sensor_invalidate_state_to_code(config, action_id, template_arg, args): async def binary_sensor_invalidate_state_to_code(config, action_id, template_arg, args):
paren = await cg.get_variable(config[CONF_ID]) paren = await cg.get_variable(config[CONF_ID])
@@ -30,7 +30,7 @@ void log_binary_sensor(const char *tag, const char *prefix, const char *type, Bi
* The sub classes should notify the front-end of new states via the publish_state() method which * The sub classes should notify the front-end of new states via the publish_state() method which
* handles inverted inputs for you. * handles inverted inputs for you.
*/ */
class BinarySensor : public StatefulEntityBase<bool>, public EntityBase_DeviceClass { class BinarySensor : public StatefulEntityBase<bool> {
public: public:
explicit BinarySensor(){}; explicit BinarySensor(){};
@@ -136,7 +136,6 @@ optional<bool> SettleFilter::new_value(bool value) {
return {}; return {};
} else { } else {
this->steady_ = false; this->steady_ = false;
this->output(value);
this->set_timeout(FILTER_TIMEOUT_ID, this->delay_.value(), [this]() { this->steady_ = true; }); this->set_timeout(FILTER_TIMEOUT_ID, this->delay_.value(), [this]() { this->steady_ = true; });
return value; return value;
} }
+24 -23
View File
@@ -10,7 +10,9 @@ static const char *const TAG = "bl0906";
constexpr uint32_t to_uint32_t(ube24_t input) { return input.h << 16 | input.m << 8 | input.l; } constexpr uint32_t to_uint32_t(ube24_t input) { return input.h << 16 | input.m << 8 | input.l; }
constexpr int32_t to_int32_t(sbe24_t input) { return input.h << 16 | input.m << 8 | input.l; } constexpr int32_t to_int32_t(sbe24_t input) {
return static_cast<int32_t>(encode_uint32((uint8_t) input.h, input.m, input.l, 0)) >> 8;
}
// The SUM byte is (Addr+Data_L+Data_M+Data_H)&0xFF negated; // The SUM byte is (Addr+Data_L+Data_M+Data_H)&0xFF negated;
constexpr uint8_t bl0906_checksum(const uint8_t address, const DataPacket *data) { constexpr uint8_t bl0906_checksum(const uint8_t address, const DataPacket *data) {
@@ -136,23 +138,24 @@ void BL0906::read_data_(const uint8_t address, const float reference, sensor::Se
this->write_byte(BL0906_READ_COMMAND); this->write_byte(BL0906_READ_COMMAND);
this->write_byte(address); this->write_byte(address);
if (this->read_array((uint8_t *) &buffer, sizeof(buffer) - 1)) { if (!this->read_array((uint8_t *) &buffer, sizeof(buffer) - 1)) {
if (bl0906_checksum(address, &buffer) == buffer.checksum) { ESP_LOGW(TAG, "Read failed");
if (signed_result) { return;
data_s24.l = buffer.l; }
data_s24.m = buffer.m; if (bl0906_checksum(address, &buffer) != buffer.checksum) {
data_s24.h = buffer.h; ESP_LOGW(TAG, "Junk on wire. Throwing away partial message");
} else { while (read() >= 0)
data_u24.l = buffer.l; ;
data_u24.m = buffer.m; return;
data_u24.h = buffer.h; }
} if (signed_result) {
} else { data_s24.l = buffer.l;
ESP_LOGW(TAG, "Junk on wire. Throwing away partial message"); data_s24.m = buffer.m;
while (read() >= 0) data_s24.h = buffer.h;
; } else {
return; data_u24.l = buffer.l;
} data_u24.m = buffer.m;
data_u24.h = buffer.h;
} }
// Power // Power
if (reference == BL0906_PREF) { if (reference == BL0906_PREF) {
@@ -188,11 +191,9 @@ void BL0906::bias_correction_(uint8_t address, float measurements, float correct
float i_rms0 = measurements * ki; float i_rms0 = measurements * ki;
float i_rms = correction * ki; float i_rms = correction * ki;
int32_t value = (i_rms * i_rms - i_rms0 * i_rms0) / 256; int32_t value = (i_rms * i_rms - i_rms0 * i_rms0) / 256;
data.l = value << 24 >> 24; data.l = value & 0xFF;
data.m = value << 16 >> 24; data.m = (value >> 8) & 0xFF;
if (value < 0) { data.h = (value >> 16) & 0xFF;
data.h = (value << 8 >> 24) | 0b10000000;
}
data.address = bl0906_checksum(address, &data); data.address = bl0906_checksum(address, &data);
ESP_LOGV(TAG, "RMSOS:%02X%02X%02X%02X%02X%02X", BL0906_WRITE_COMMAND, address, data.l, data.m, data.h, data.address); ESP_LOGV(TAG, "RMSOS:%02X%02X%02X%02X%02X%02X", BL0906_WRITE_COMMAND, address, data.l, data.m, data.h, data.address);
this->write_byte(BL0906_WRITE_COMMAND); this->write_byte(BL0906_WRITE_COMMAND);
+1
View File
@@ -143,6 +143,7 @@ FINAL_VALIDATE_SCHEMA = uart.final_validate_device_schema(
cv.Required(CONF_ID): cv.use_id(BL0906), cv.Required(CONF_ID): cv.use_id(BL0906),
} }
), ),
synchronous=True,
) )
async def reset_energy_to_code(config, action_id, template_arg, args): async def reset_energy_to_code(config, action_id, template_arg, args):
var = cg.new_Pvariable(action_id, template_arg) var = cg.new_Pvariable(action_id, template_arg)
+1 -3
View File
@@ -69,10 +69,8 @@ class BL0940 : public PollingComponent, public uart::UARTDevice {
void set_energy_calibration_number(number::Number *num) { this->energy_calibration_number_ = num; } void set_energy_calibration_number(number::Number *num) { this->energy_calibration_number_ = num; }
#endif #endif
#ifdef USE_BUTTON // Resets all calibration values to defaults
// Resets all calibration values to defaults (can be triggered by a button)
void reset_calibration(); void reset_calibration();
#endif
// Core component methods // Core component methods
void loop() override; void loop() override;
+1 -1
View File
@@ -173,7 +173,7 @@ void BL0942::received_package_(DataPacket *data) {
float i_rms = (uint24_t) data->i_rms / current_reference_; float i_rms = (uint24_t) data->i_rms / current_reference_;
float watt = (int24_t) data->watt / power_reference_; float watt = (int24_t) data->watt / power_reference_;
float total_energy_consumption = cf_cnt / energy_reference_; float total_energy_consumption = cf_cnt / energy_reference_;
float frequency = 1000000.0f / data->frequency; float frequency = data->frequency != 0 ? 1000000.0f / data->frequency : NAN;
if (voltage_sensor_ != nullptr) { if (voltage_sensor_ != nullptr) {
voltage_sensor_->publish_state(v_rms); voltage_sensor_->publish_state(v_rms);
+18 -4
View File
@@ -172,7 +172,10 @@ BLE_REMOVE_BOND_ACTION_SCHEMA = cv.Schema(
@automation.register_action( @automation.register_action(
"ble_client.disconnect", BLEDisconnectAction, BLE_CONNECT_ACTION_SCHEMA "ble_client.disconnect",
BLEDisconnectAction,
BLE_CONNECT_ACTION_SCHEMA,
synchronous=False,
) )
async def ble_disconnect_to_code(config, action_id, template_arg, args): async def ble_disconnect_to_code(config, action_id, template_arg, args):
parent = await cg.get_variable(config[CONF_ID]) parent = await cg.get_variable(config[CONF_ID])
@@ -180,7 +183,10 @@ async def ble_disconnect_to_code(config, action_id, template_arg, args):
@automation.register_action( @automation.register_action(
"ble_client.connect", BLEConnectAction, BLE_CONNECT_ACTION_SCHEMA "ble_client.connect",
BLEConnectAction,
BLE_CONNECT_ACTION_SCHEMA,
synchronous=False,
) )
async def ble_connect_to_code(config, action_id, template_arg, args): async def ble_connect_to_code(config, action_id, template_arg, args):
parent = await cg.get_variable(config[CONF_ID]) parent = await cg.get_variable(config[CONF_ID])
@@ -188,7 +194,10 @@ async def ble_connect_to_code(config, action_id, template_arg, args):
@automation.register_action( @automation.register_action(
"ble_client.ble_write", BLEWriteAction, BLE_WRITE_ACTION_SCHEMA "ble_client.ble_write",
BLEWriteAction,
BLE_WRITE_ACTION_SCHEMA,
synchronous=False,
) )
async def ble_write_to_code(config, action_id, template_arg, args): async def ble_write_to_code(config, action_id, template_arg, args):
parent = await cg.get_variable(config[CONF_ID]) parent = await cg.get_variable(config[CONF_ID])
@@ -247,6 +256,7 @@ async def ble_write_to_code(config, action_id, template_arg, args):
"ble_client.numeric_comparison_reply", "ble_client.numeric_comparison_reply",
BLENumericComparisonReplyAction, BLENumericComparisonReplyAction,
BLE_NUMERIC_COMPARISON_REPLY_ACTION_SCHEMA, BLE_NUMERIC_COMPARISON_REPLY_ACTION_SCHEMA,
synchronous=True,
) )
async def numeric_comparison_reply_to_code(config, action_id, template_arg, args): async def numeric_comparison_reply_to_code(config, action_id, template_arg, args):
parent = await cg.get_variable(config[CONF_ID]) parent = await cg.get_variable(config[CONF_ID])
@@ -263,7 +273,10 @@ async def numeric_comparison_reply_to_code(config, action_id, template_arg, args
@automation.register_action( @automation.register_action(
"ble_client.passkey_reply", BLEPasskeyReplyAction, BLE_PASSKEY_REPLY_ACTION_SCHEMA "ble_client.passkey_reply",
BLEPasskeyReplyAction,
BLE_PASSKEY_REPLY_ACTION_SCHEMA,
synchronous=True,
) )
async def passkey_reply_to_code(config, action_id, template_arg, args): async def passkey_reply_to_code(config, action_id, template_arg, args):
parent = await cg.get_variable(config[CONF_ID]) parent = await cg.get_variable(config[CONF_ID])
@@ -283,6 +296,7 @@ async def passkey_reply_to_code(config, action_id, template_arg, args):
"ble_client.remove_bond", "ble_client.remove_bond",
BLERemoveBondAction, BLERemoveBondAction,
BLE_REMOVE_BOND_ACTION_SCHEMA, BLE_REMOVE_BOND_ACTION_SCHEMA,
synchronous=True,
) )
async def remove_bond_to_code(config, action_id, template_arg, args): async def remove_bond_to_code(config, action_id, template_arg, args):
parent = await cg.get_variable(config[CONF_ID]) parent = await cg.get_variable(config[CONF_ID])
+48 -5
View File
@@ -1,29 +1,64 @@
import esphome.codegen as cg import esphome.codegen as cg
from esphome.components.logger import request_log_listener from esphome.components.logger import request_log_listener
from esphome.components.uart import (
UARTComponent,
debug_to_code,
maybe_empty_debug,
uart_ns,
)
from esphome.components.zephyr import zephyr_add_prj_conf from esphome.components.zephyr import zephyr_add_prj_conf
import esphome.config_validation as cv import esphome.config_validation as cv
from esphome.const import CONF_ID, CONF_LOGS, CONF_TYPE from esphome.const import (
CONF_DEBUG,
CONF_ID,
CONF_LOGS,
CONF_RX_BUFFER_SIZE,
CONF_TX_BUFFER_SIZE,
CONF_TYPE,
)
from esphome.types import ConfigType
AUTO_LOAD = ["zephyr_ble_server"] AUTO_LOAD = ["zephyr_ble_server", "uart"]
CODEOWNERS = ["@tomaszduda23"] CODEOWNERS = ["@tomaszduda23"]
ble_nus_ns = cg.esphome_ns.namespace("ble_nus") ble_nus_ns = cg.esphome_ns.namespace("ble_nus")
BLENUS = ble_nus_ns.class_("BLENUS", cg.Component) BLENUS = ble_nus_ns.class_("BLENUS", cg.Component, UARTComponent)
CONF_UART = "uart"
def validate_rx_buffer(config: ConfigType) -> ConfigType:
config = config.copy()
if config[CONF_TYPE] == CONF_LOGS:
if CONF_RX_BUFFER_SIZE in config:
raise cv.Invalid("logs does not support rx_buffer_size")
elif CONF_RX_BUFFER_SIZE not in config:
config[CONF_RX_BUFFER_SIZE] = 512
return config
CONFIG_SCHEMA = cv.All( CONFIG_SCHEMA = cv.All(
cv.Schema( cv.Schema(
{ {
cv.GenerateID(): cv.declare_id(BLENUS), cv.GenerateID(): cv.declare_id(BLENUS),
cv.Optional(CONF_TYPE, default=CONF_LOGS): cv.one_of( cv.Optional(CONF_TYPE, default=CONF_LOGS): cv.one_of(
*[CONF_LOGS], lower=True *[CONF_LOGS, CONF_UART], lower=True
), ),
cv.Optional(CONF_TX_BUFFER_SIZE, default=512): cv.All(
cv.validate_bytes, cv.int_range(min=160, max=8192)
),
cv.Optional(CONF_RX_BUFFER_SIZE): cv.All(
cv.validate_bytes, cv.int_range(min=160, max=8192)
),
cv.Optional(CONF_DEBUG): maybe_empty_debug,
} }
).extend(cv.COMPONENT_SCHEMA), ).extend(cv.COMPONENT_SCHEMA),
cv.only_with_framework("zephyr"), cv.only_with_framework("zephyr"),
validate_rx_buffer,
) )
async def to_code(config): async def to_code(config: ConfigType) -> None:
var = cg.new_Pvariable(config[CONF_ID]) var = cg.new_Pvariable(config[CONF_ID])
zephyr_add_prj_conf("BT_NUS", True) zephyr_add_prj_conf("BT_NUS", True)
expose_log = config[CONF_TYPE] == CONF_LOGS expose_log = config[CONF_TYPE] == CONF_LOGS
@@ -31,3 +66,11 @@ async def to_code(config):
if expose_log: if expose_log:
request_log_listener() # Request a log listener slot for BLE NUS log streaming request_log_listener() # Request a log listener slot for BLE NUS log streaming
await cg.register_component(var, config) await cg.register_component(var, config)
cg.add_define("ESPHOME_BLE_NUS_TX_RING_BUFFER_SIZE", config[CONF_TX_BUFFER_SIZE])
if CONF_RX_BUFFER_SIZE in config:
cg.add_define(
"ESPHOME_BLE_NUS_RX_RING_BUFFER_SIZE", config[CONF_RX_BUFFER_SIZE]
)
if CONF_DEBUG in config:
cg.add_global(uart_ns.using)
await debug_to_code(config[CONF_DEBUG], var)
+114 -15
View File
@@ -11,25 +11,115 @@
namespace esphome::ble_nus { namespace esphome::ble_nus {
constexpr size_t BLE_TX_BUF_SIZE = 2048;
// NOLINTBEGIN(cppcoreguidelines-avoid-non-const-global-variables) // NOLINTBEGIN(cppcoreguidelines-avoid-non-const-global-variables)
BLENUS *global_ble_nus; BLENUS *global_ble_nus;
RING_BUF_DECLARE(global_ble_tx_ring_buf, BLE_TX_BUF_SIZE); RING_BUF_DECLARE(global_ble_tx_ring_buf, ESPHOME_BLE_NUS_TX_RING_BUFFER_SIZE);
#ifdef ESPHOME_BLE_NUS_RX_RING_BUFFER_SIZE
RING_BUF_DECLARE(global_ble_rx_ring_buf, ESPHOME_BLE_NUS_RX_RING_BUFFER_SIZE);
#endif
// NOLINTEND(cppcoreguidelines-avoid-non-const-global-variables) // NOLINTEND(cppcoreguidelines-avoid-non-const-global-variables)
static const char *const TAG = "ble_nus"; static const char *const TAG = "ble_nus";
size_t BLENUS::write_array(const uint8_t *data, size_t len) { void BLENUS::write_array(const uint8_t *data, size_t len) {
if (atomic_get(&this->tx_status_) == TX_DISABLED) { if (atomic_get(&this->tx_status_) == TX_DISABLED) {
return 0; return;
} }
return ring_buf_put(&global_ble_tx_ring_buf, data, len); auto sent = ring_buf_put(&global_ble_tx_ring_buf, data, len);
if (sent < len) {
ESP_LOGE(TAG, "TX dropping %u bytes", len - sent);
return;
}
#ifdef USE_UART_DEBUGGER
for (size_t i = 0; i < len; i++) {
this->debug_callback_.call(uart::UART_DIRECTION_TX, data[i]);
}
#endif
}
bool BLENUS::peek_byte(uint8_t *data) {
#ifdef ESPHOME_BLE_NUS_RX_RING_BUFFER_SIZE
if (this->has_peek_) {
*data = this->peek_buffer_;
return true;
}
if (this->read_byte(&this->peek_buffer_)) {
*data = this->peek_buffer_;
this->has_peek_ = true;
return true;
}
return false;
#else
return false;
#endif
}
bool BLENUS::read_array(uint8_t *data, size_t len) {
#ifdef ESPHOME_BLE_NUS_RX_RING_BUFFER_SIZE
if (len == 0) {
return true;
}
if (this->available() < len) {
return false;
}
// First, use the peek buffer if available
if (this->has_peek_) {
data[0] = this->peek_buffer_;
this->has_peek_ = false;
data++;
if (--len == 0) { // Decrement len first, then check it...
#ifdef USE_UART_DEBUGGER
this->debug_callback_.call(uart::UART_DIRECTION_RX, this->peek_buffer_);
#endif
return true; // No more to read
}
}
if (ring_buf_get(&global_ble_rx_ring_buf, data, len) != len) {
ESP_LOGE(TAG, "UART BLE unexpected size");
return false;
}
#ifdef USE_UART_DEBUGGER
for (size_t i = 0; i < len; i++) {
this->debug_callback_.call(uart::UART_DIRECTION_RX, data[i]);
}
#endif
return true;
#else
return false;
#endif
}
size_t BLENUS::available() {
#ifdef ESPHOME_BLE_NUS_RX_RING_BUFFER_SIZE
uint32_t size = ring_buf_size_get(&global_ble_rx_ring_buf);
ESP_LOGVV(TAG, "UART BLE available %u", size);
return size + (this->has_peek_ ? 1 : 0);
#else
return 0;
#endif
}
uart::FlushResult BLENUS::flush() {
constexpr uint32_t timeout_500ms = 500;
uint32_t start = millis();
while (atomic_get(&this->tx_status_) != TX_DISABLED && !ring_buf_is_empty(&global_ble_tx_ring_buf)) {
if (millis() - start > timeout_500ms) {
ESP_LOGW(TAG, "Flush timeout");
return uart::FlushResult::TIMEOUT;
}
delay(1);
}
return uart::FlushResult::SUCCESS;
} }
void BLENUS::connected(bt_conn *conn, uint8_t err) { void BLENUS::connected(bt_conn *conn, uint8_t err) {
if (err == 0) { if (err == 0) {
global_ble_nus->conn_.store(bt_conn_ref(conn)); global_ble_nus->conn_.store(bt_conn_ref(conn));
global_ble_nus->connected_ = true;
} }
} }
@@ -38,6 +128,7 @@ void BLENUS::disconnected(bt_conn *conn, uint8_t reason) {
bt_conn_unref(global_ble_nus->conn_.load()); bt_conn_unref(global_ble_nus->conn_.load());
// Connection array is global static. // Connection array is global static.
// Reference can be kept even if disconnected. // Reference can be kept even if disconnected.
global_ble_nus->connected_ = false;
} }
} }
@@ -63,12 +154,19 @@ void BLENUS::send_enabled_callback(bt_nus_send_status status) {
break; break;
} }
} }
void BLENUS::rx_callback(bt_conn *conn, const uint8_t *const data, uint16_t len) { void BLENUS::rx_callback(bt_conn *conn, const uint8_t *const data, uint16_t len) {
ESP_LOGD(TAG, "Received %d bytes.", len); ESP_LOGV(TAG, "Received %d bytes.", len);
#ifdef ESPHOME_BLE_NUS_RX_RING_BUFFER_SIZE
auto recv_len = ring_buf_put(&global_ble_rx_ring_buf, data, len);
if (recv_len < len) {
ESP_LOGE(TAG, "RX dropping %u bytes", len - recv_len);
}
#endif
} }
void BLENUS::setup() { void BLENUS::setup() {
#ifdef ESPHOME_BLE_NUS_RX_RING_BUFFER_SIZE
this->rx_buffer_size_ = ESPHOME_BLE_NUS_RX_RING_BUFFER_SIZE;
#endif
bt_nus_cb callbacks = { bt_nus_cb callbacks = {
.received = rx_callback, .received = rx_callback,
.sent = tx_callback, .sent = tx_callback,
@@ -106,16 +204,17 @@ void BLENUS::on_log(uint8_t level, const char *tag, const char *message, size_t
#endif #endif
void BLENUS::dump_config() { void BLENUS::dump_config() {
ESP_LOGCONFIG(TAG,
"ble nus:\n"
" log: %s",
YESNO(this->expose_log_));
uint32_t mtu = 0; uint32_t mtu = 0;
bt_conn *conn = this->conn_.load(); bt_conn *conn = this->conn_.load();
if (conn) { if (conn && this->connected_) {
mtu = bt_nus_get_mtu(conn); mtu = bt_nus_get_mtu(conn);
} }
ESP_LOGCONFIG(TAG, " MTU: %u", mtu); ESP_LOGCONFIG(TAG,
"ble nus:\n"
" log: %s\n"
" connected: %s\n"
" MTU: %u",
YESNO(this->expose_log_), YESNO(this->connected_.load()), mtu);
} }
void BLENUS::loop() { void BLENUS::loop() {
+14 -2
View File
@@ -2,6 +2,7 @@
#ifdef USE_ZEPHYR #ifdef USE_ZEPHYR
#include "esphome/core/defines.h" #include "esphome/core/defines.h"
#include "esphome/core/component.h" #include "esphome/core/component.h"
#include "esphome/components/uart/uart_component.h"
#ifdef USE_LOGGER #ifdef USE_LOGGER
#include "esphome/components/logger/logger.h" #include "esphome/components/logger/logger.h"
#endif #endif
@@ -10,7 +11,7 @@
namespace esphome::ble_nus { namespace esphome::ble_nus {
class BLENUS : public Component { class BLENUS : public uart::UARTComponent, public Component {
enum TxStatus { enum TxStatus {
TX_DISABLED, TX_DISABLED,
TX_ENABLED, TX_ENABLED,
@@ -21,7 +22,12 @@ class BLENUS : public Component {
void setup() override; void setup() override;
void dump_config() override; void dump_config() override;
void loop() override; void loop() override;
size_t write_array(const uint8_t *data, size_t len); void write_array(const uint8_t *data, size_t len) override;
bool peek_byte(uint8_t *data) override;
bool read_array(uint8_t *data, size_t len) override;
size_t available() override;
uart::FlushResult flush() override;
void check_logger_conflict() override {}
void set_expose_log(bool expose_log) { this->expose_log_ = expose_log; } void set_expose_log(bool expose_log) { this->expose_log_ = expose_log; }
#ifdef USE_LOGGER #ifdef USE_LOGGER
void on_log(uint8_t level, const char *tag, const char *message, size_t message_len); void on_log(uint8_t level, const char *tag, const char *message, size_t message_len);
@@ -37,6 +43,12 @@ class BLENUS : public Component {
std::atomic<bt_conn *> conn_ = nullptr; std::atomic<bt_conn *> conn_ = nullptr;
bool expose_log_ = false; bool expose_log_ = false;
atomic_t tx_status_ = ATOMIC_INIT(TX_DISABLED); atomic_t tx_status_ = ATOMIC_INIT(TX_DISABLED);
std::atomic<bool> connected_{};
#ifdef ESPHOME_BLE_NUS_RX_RING_BUFFER_SIZE
// RX buffer for peek functionality
uint8_t peek_buffer_{0};
bool has_peek_{false};
#endif
}; };
} // namespace esphome::ble_nus } // namespace esphome::ble_nus
@@ -76,11 +76,12 @@ class BLEPresenceDevice : public binary_sensor::BinarySensorInitiallyOff,
} }
break; break;
case MATCH_BY_IBEACON_UUID: case MATCH_BY_IBEACON_UUID:
if (!device.get_ibeacon().has_value()) { auto maybe_ibeacon = device.get_ibeacon();
if (!maybe_ibeacon.has_value()) {
return false; return false;
} }
auto ibeacon = device.get_ibeacon().value(); auto ibeacon = *maybe_ibeacon;
if (this->ibeacon_uuid_ != ibeacon.get_uuid()) { if (this->ibeacon_uuid_ != ibeacon.get_uuid()) {
return false; return false;
@@ -74,11 +74,12 @@ class BLERSSISensor : public sensor::Sensor, public esp32_ble_tracker::ESPBTDevi
} }
break; break;
case MATCH_BY_IBEACON_UUID: case MATCH_BY_IBEACON_UUID:
if (!device.get_ibeacon().has_value()) { auto maybe_ibeacon = device.get_ibeacon();
if (!maybe_ibeacon.has_value()) {
return false; return false;
} }
auto ibeacon = device.get_ibeacon().value(); auto ibeacon = *maybe_ibeacon;
if (this->ibeacon_uuid_ != ibeacon.get_uuid()) { if (this->ibeacon_uuid_ != ibeacon.get_uuid()) {
return false; return false;
+19 -4
View File
@@ -16,12 +16,27 @@ namespace ble_scanner {
class BLEScanner : public text_sensor::TextSensor, public esp32_ble_tracker::ESPBTDeviceListener, public Component { class BLEScanner : public text_sensor::TextSensor, public esp32_ble_tracker::ESPBTDeviceListener, public Component {
public: public:
bool parse_device(const esp32_ble_tracker::ESPBTDevice &device) override { bool parse_device(const esp32_ble_tracker::ESPBTDevice &device) override {
// Format JSON using stack buffer to avoid heap allocations from string concatenation
char buf[128];
char addr_buf[MAC_ADDRESS_PRETTY_BUFFER_SIZE]; char addr_buf[MAC_ADDRESS_PRETTY_BUFFER_SIZE];
// Escape special characters in the device name for valid JSON
const char *name = device.get_name().c_str();
char escaped_name[128];
size_t pos = 0;
for (; *name != '\0' && pos < sizeof(escaped_name) - 7; name++) {
uint8_t c = static_cast<uint8_t>(*name);
if (c == '"' || c == '\\') {
escaped_name[pos++] = '\\';
escaped_name[pos++] = c;
} else if (c < 0x20) {
pos += snprintf(escaped_name + pos, sizeof(escaped_name) - pos, "\\u%04x", c);
} else {
escaped_name[pos++] = c;
}
}
escaped_name[pos] = '\0';
char buf[256];
snprintf(buf, sizeof(buf), "{\"timestamp\":%" PRId64 ",\"address\":\"%s\",\"rssi\":%d,\"name\":\"%s\"}", snprintf(buf, sizeof(buf), "{\"timestamp\":%" PRId64 ",\"address\":\"%s\",\"rssi\":%d,\"name\":\"%s\"}",
static_cast<int64_t>(::time(nullptr)), device.address_str_to(addr_buf), device.get_rssi(), static_cast<int64_t>(::time(nullptr)), device.address_str_to(addr_buf), device.get_rssi(), escaped_name);
device.get_name().c_str());
this->publish_state(buf); this->publish_state(buf);
return true; return true;
} }
@@ -183,10 +183,7 @@ void BluetoothConnection::send_service_for_discovery_() {
static constexpr size_t MAX_PACKET_SIZE = 1360; static constexpr size_t MAX_PACKET_SIZE = 1360;
// Keep running total of actual message size // Keep running total of actual message size
size_t current_size = 0; size_t current_size = resp.calculate_size();
api::ProtoSize size;
resp.calculate_size(size);
current_size = size.get_size();
while (this->send_service_ < this->service_count_) { while (this->send_service_ < this->service_count_) {
esp_gattc_service_elem_t service_result; esp_gattc_service_elem_t service_result;
@@ -302,9 +299,7 @@ void BluetoothConnection::send_service_for_discovery_() {
} // end if (total_char_count > 0) } // end if (total_char_count > 0)
// Calculate the actual size of just this service // Calculate the actual size of just this service
api::ProtoSize service_sizer; size_t service_size = service_resp.calculate_size() + 1; // +1 for field tag
service_resp.calculate_size(service_sizer);
size_t service_size = service_sizer.get_size() + 1; // +1 for field tag
// Check if adding this service would exceed the limit // Check if adding this service would exceed the limit
if (current_size + service_size > MAX_PACKET_SIZE) { if (current_size + service_size > MAX_PACKET_SIZE) {
@@ -333,7 +328,7 @@ void BluetoothConnection::send_service_for_discovery_() {
} }
// Send the message with dynamically batched services // Send the message with dynamically batched services
api_conn->send_message(resp, api::BluetoothGATTGetServicesResponse::MESSAGE_TYPE); api_conn->send_message(resp);
} }
void BluetoothConnection::log_connection_error_(const char *operation, esp_gatt_status_t status) { void BluetoothConnection::log_connection_error_(const char *operation, esp_gatt_status_t status) {
@@ -415,11 +410,14 @@ bool BluetoothConnection::gattc_event_handler(esp_gattc_cb_event_t event, esp_ga
this->proxy_->send_gatt_error(this->address_, param->read.handle, param->read.status); this->proxy_->send_gatt_error(this->address_, param->read.handle, param->read.status);
break; break;
} }
auto *api_connection = this->proxy_->get_api_connection();
if (api_connection == nullptr)
break;
api::BluetoothGATTReadResponse resp; api::BluetoothGATTReadResponse resp;
resp.address = this->address_; resp.address = this->address_;
resp.handle = param->read.handle; resp.handle = param->read.handle;
resp.set_data(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); api_connection->send_message(resp);
break; break;
} }
case ESP_GATTC_WRITE_CHAR_EVT: case ESP_GATTC_WRITE_CHAR_EVT:
@@ -429,10 +427,13 @@ bool BluetoothConnection::gattc_event_handler(esp_gattc_cb_event_t event, esp_ga
this->proxy_->send_gatt_error(this->address_, param->write.handle, param->write.status); this->proxy_->send_gatt_error(this->address_, param->write.handle, param->write.status);
break; break;
} }
auto *api_connection = this->proxy_->get_api_connection();
if (api_connection == nullptr)
break;
api::BluetoothGATTWriteResponse resp; api::BluetoothGATTWriteResponse resp;
resp.address = this->address_; resp.address = this->address_;
resp.handle = param->write.handle; resp.handle = param->write.handle;
this->proxy_->get_api_connection()->send_message(resp, api::BluetoothGATTWriteResponse::MESSAGE_TYPE); api_connection->send_message(resp);
break; break;
} }
case ESP_GATTC_UNREG_FOR_NOTIFY_EVT: { case ESP_GATTC_UNREG_FOR_NOTIFY_EVT: {
@@ -442,10 +443,13 @@ bool BluetoothConnection::gattc_event_handler(esp_gattc_cb_event_t event, esp_ga
this->proxy_->send_gatt_error(this->address_, param->unreg_for_notify.handle, param->unreg_for_notify.status); this->proxy_->send_gatt_error(this->address_, param->unreg_for_notify.handle, param->unreg_for_notify.status);
break; break;
} }
auto *api_connection = this->proxy_->get_api_connection();
if (api_connection == nullptr)
break;
api::BluetoothGATTNotifyResponse resp; api::BluetoothGATTNotifyResponse resp;
resp.address = this->address_; resp.address = this->address_;
resp.handle = param->unreg_for_notify.handle; resp.handle = param->unreg_for_notify.handle;
this->proxy_->get_api_connection()->send_message(resp, api::BluetoothGATTNotifyResponse::MESSAGE_TYPE); api_connection->send_message(resp);
break; break;
} }
case ESP_GATTC_REG_FOR_NOTIFY_EVT: { case ESP_GATTC_REG_FOR_NOTIFY_EVT: {
@@ -455,20 +459,26 @@ bool BluetoothConnection::gattc_event_handler(esp_gattc_cb_event_t event, esp_ga
this->proxy_->send_gatt_error(this->address_, param->reg_for_notify.handle, param->reg_for_notify.status); this->proxy_->send_gatt_error(this->address_, param->reg_for_notify.handle, param->reg_for_notify.status);
break; break;
} }
auto *api_connection = this->proxy_->get_api_connection();
if (api_connection == nullptr)
break;
api::BluetoothGATTNotifyResponse resp; api::BluetoothGATTNotifyResponse resp;
resp.address = this->address_; resp.address = this->address_;
resp.handle = param->reg_for_notify.handle; resp.handle = param->reg_for_notify.handle;
this->proxy_->get_api_connection()->send_message(resp, api::BluetoothGATTNotifyResponse::MESSAGE_TYPE); api_connection->send_message(resp);
break; break;
} }
case ESP_GATTC_NOTIFY_EVT: { case ESP_GATTC_NOTIFY_EVT: {
ESP_LOGV(TAG, "[%d] [%s] ESP_GATTC_NOTIFY_EVT: handle=0x%2X", this->connection_index_, this->address_str_, ESP_LOGV(TAG, "[%d] [%s] ESP_GATTC_NOTIFY_EVT: handle=0x%2X", this->connection_index_, this->address_str_,
param->notify.handle); param->notify.handle);
auto *api_connection = this->proxy_->get_api_connection();
if (api_connection == nullptr)
break;
api::BluetoothGATTNotifyDataResponse resp; api::BluetoothGATTNotifyDataResponse resp;
resp.address = this->address_; resp.address = this->address_;
resp.handle = param->notify.handle; resp.handle = param->notify.handle;
resp.set_data(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); api_connection->send_message(resp);
break; break;
} }
default: default:
@@ -24,6 +24,10 @@ class BluetoothConnection final : public esp32_ble_client::BLEClientBase {
esp_err_t notify_characteristic(uint16_t handle, bool enable); esp_err_t notify_characteristic(uint16_t handle, bool enable);
esp_err_t update_connection_params(uint16_t min_interval, uint16_t max_interval, uint16_t latency, uint16_t timeout) {
return this->update_conn_params_(min_interval, max_interval, latency, timeout, "custom");
}
void set_address(uint64_t address) override; void set_address(uint64_t address) override;
protected: protected:
@@ -3,7 +3,9 @@
#include "esphome/core/log.h" #include "esphome/core/log.h"
#include "esphome/core/macros.h" #include "esphome/core/macros.h"
#include "esphome/core/application.h" #include "esphome/core/application.h"
#include <algorithm>
#include <cstring> #include <cstring>
#include <limits>
#ifdef USE_ESP32 #ifdef USE_ESP32
@@ -44,7 +46,7 @@ void BluetoothProxy::send_bluetooth_scanner_state_(esp32_ble_tracker::ScannerSta
resp.configured_mode = this->configured_scan_active_ resp.configured_mode = this->configured_scan_active_
? api::enums::BluetoothScannerMode::BLUETOOTH_SCANNER_MODE_ACTIVE ? api::enums::BluetoothScannerMode::BLUETOOTH_SCANNER_MODE_ACTIVE
: api::enums::BluetoothScannerMode::BLUETOOTH_SCANNER_MODE_PASSIVE; : api::enums::BluetoothScannerMode::BLUETOOTH_SCANNER_MODE_PASSIVE;
this->api_connection_->send_message(resp, api::BluetoothScannerStateResponse::MESSAGE_TYPE); this->api_connection_->send_message(resp);
} }
void BluetoothProxy::log_connection_request_ignored_(BluetoothConnection *connection, espbt::ClientState state) { void BluetoothProxy::log_connection_request_ignored_(BluetoothConnection *connection, espbt::ClientState state) {
@@ -112,7 +114,7 @@ void BluetoothProxy::flush_pending_advertisements() {
return; return;
// Send the message // Send the message
this->api_connection_->send_message(this->response_, api::BluetoothLERawAdvertisementsResponse::MESSAGE_TYPE); this->api_connection_->send_message(this->response_);
ESP_LOGV(TAG, "Sent batch of %u BLE advertisements", this->response_.advertisements_len); ESP_LOGV(TAG, "Sent batch of %u BLE advertisements", this->response_.advertisements_len);
@@ -269,7 +271,7 @@ void BluetoothProxy::bluetooth_device_request(const api::BluetoothDeviceRequest
call.success = ret == ESP_OK; call.success = ret == ESP_OK;
call.error = ret; call.error = ret;
this->api_connection_->send_message(call, api::BluetoothDeviceClearCacheResponse::MESSAGE_TYPE); this->api_connection_->send_message(call);
break; break;
} }
@@ -361,6 +363,33 @@ void BluetoothProxy::bluetooth_gatt_notify(const api::BluetoothGATTNotifyRequest
} }
} }
void BluetoothProxy::bluetooth_set_connection_params(const api::BluetoothSetConnectionParamsRequest &msg) {
if (this->api_connection_ == nullptr)
return;
auto *connection = this->get_connection_(msg.address, false);
api::BluetoothSetConnectionParamsResponse resp;
resp.address = msg.address;
if (connection == nullptr || !connection->connected()) {
ESP_LOGW(TAG, "[%d] [%s] Cannot set connection params, not connected",
connection ? static_cast<int>(connection->connection_index_) : -1,
connection ? connection->address_str() : "unknown");
resp.error = ESP_GATT_NOT_CONNECTED;
this->api_connection_->send_message(resp);
return;
}
// Protobuf fields are uint32_t to future-proof the API if BLE ever supports wider values;
// clamp to uint16_t since the current BLE spec defines these as 16-bit.
constexpr uint32_t max_val = std::numeric_limits<uint16_t>::max();
resp.error = connection->update_connection_params(static_cast<uint16_t>(std::min(msg.min_interval, max_val)),
static_cast<uint16_t>(std::min(msg.max_interval, max_val)),
static_cast<uint16_t>(std::min(msg.latency, max_val)),
static_cast<uint16_t>(std::min(msg.timeout, max_val)));
this->api_connection_->send_message(resp);
}
void BluetoothProxy::subscribe_api_connection(api::APIConnection *api_connection, uint32_t flags) { void BluetoothProxy::subscribe_api_connection(api::APIConnection *api_connection, uint32_t flags) {
if (this->api_connection_ != nullptr) { if (this->api_connection_ != nullptr) {
ESP_LOGE(TAG, "Only one API subscription is allowed at a time"); ESP_LOGE(TAG, "Only one API subscription is allowed at a time");
@@ -389,7 +418,7 @@ void BluetoothProxy::send_device_connection(uint64_t address, bool connected, ui
call.connected = connected; call.connected = connected;
call.mtu = mtu; call.mtu = mtu;
call.error = error; call.error = error;
this->api_connection_->send_message(call, api::BluetoothDeviceConnectionResponse::MESSAGE_TYPE); this->api_connection_->send_message(call);
} }
void BluetoothProxy::send_connections_free() { void BluetoothProxy::send_connections_free() {
if (this->api_connection_ != nullptr) { if (this->api_connection_ != nullptr) {
@@ -398,7 +427,7 @@ void BluetoothProxy::send_connections_free() {
} }
void BluetoothProxy::send_connections_free(api::APIConnection *api_connection) { void BluetoothProxy::send_connections_free(api::APIConnection *api_connection) {
api_connection->send_message(this->connections_free_response_, api::BluetoothConnectionsFreeResponse::MESSAGE_TYPE); api_connection->send_message(this->connections_free_response_);
} }
void BluetoothProxy::send_gatt_services_done(uint64_t address) { void BluetoothProxy::send_gatt_services_done(uint64_t address) {
@@ -406,7 +435,7 @@ void BluetoothProxy::send_gatt_services_done(uint64_t address) {
return; return;
api::BluetoothGATTGetServicesDoneResponse call; api::BluetoothGATTGetServicesDoneResponse call;
call.address = address; call.address = address;
this->api_connection_->send_message(call, api::BluetoothGATTGetServicesDoneResponse::MESSAGE_TYPE); this->api_connection_->send_message(call);
} }
void BluetoothProxy::send_gatt_error(uint64_t address, uint16_t handle, esp_err_t error) { void BluetoothProxy::send_gatt_error(uint64_t address, uint16_t handle, esp_err_t error) {
@@ -416,25 +445,29 @@ void BluetoothProxy::send_gatt_error(uint64_t address, uint16_t handle, esp_err_
call.address = address; call.address = address;
call.handle = handle; call.handle = handle;
call.error = error; call.error = error;
this->api_connection_->send_message(call, api::BluetoothGATTWriteResponse::MESSAGE_TYPE); this->api_connection_->send_message(call);
} }
void BluetoothProxy::send_device_pairing(uint64_t address, bool paired, esp_err_t error) { void BluetoothProxy::send_device_pairing(uint64_t address, bool paired, esp_err_t error) {
if (this->api_connection_ == nullptr)
return;
api::BluetoothDevicePairingResponse call; api::BluetoothDevicePairingResponse call;
call.address = address; call.address = address;
call.paired = paired; call.paired = paired;
call.error = error; call.error = error;
this->api_connection_->send_message(call, api::BluetoothDevicePairingResponse::MESSAGE_TYPE); this->api_connection_->send_message(call);
} }
void BluetoothProxy::send_device_unpairing(uint64_t address, bool success, esp_err_t error) { void BluetoothProxy::send_device_unpairing(uint64_t address, bool success, esp_err_t error) {
if (this->api_connection_ == nullptr)
return;
api::BluetoothDeviceUnpairingResponse call; api::BluetoothDeviceUnpairingResponse call;
call.address = address; call.address = address;
call.success = success; call.success = success;
call.error = error; call.error = error;
this->api_connection_->send_message(call, api::BluetoothDeviceUnpairingResponse::MESSAGE_TYPE); this->api_connection_->send_message(call);
} }
void BluetoothProxy::bluetooth_scanner_set_mode(bool active) { void BluetoothProxy::bluetooth_scanner_set_mode(bool active) {

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