mirror of
https://github.com/esphome/esphome.git
synced 2026-10-06 19:06:37 +00:00
Merge branch 'dev' into remove_posix_tz_parser
This commit is contained in:
@@ -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
@@ -1 +1 @@
|
|||||||
b97e16a84153b2a4cfc51137cd6121db3c32374504b2bea55144413b3e573052
|
e4b9c4b54e705d3c9400e1cdda8ba0b32634780cfa5f32271832e911bdcafe7e
|
||||||
|
|||||||
@@ -25,7 +25,7 @@
|
|||||||
- [ ] ESP32
|
- [ ] ESP32
|
||||||
- [ ] ESP32 IDF
|
- [ ] ESP32 IDF
|
||||||
- [ ] ESP8266
|
- [ ] ESP8266
|
||||||
- [ ] RP2040
|
- [ ] RP2040/RP2350
|
||||||
- [ ] BK72xx
|
- [ ] BK72xx
|
||||||
- [ ] RTL87xx
|
- [ ] RTL87xx
|
||||||
- [ ] LN882x
|
- [ ] LN882x
|
||||||
|
|||||||
@@ -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',
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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),
|
||||||
|
|||||||
@@ -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
|
||||||
|
};
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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({
|
||||||
|
|||||||
@@ -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}}"
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 }}
|
||||||
|
|||||||
@@ -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
@@ -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
@@ -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
@@ -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)
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -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
|
||||||
@@ -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_++;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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];
|
||||||
}
|
}
|
||||||
|
|||||||
+1212
-817
File diff suppressed because it is too large
Load Diff
+449
-225
File diff suppressed because it is too large
Load Diff
@@ -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;
|
||||||
|
|||||||
@@ -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;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -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_;
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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
@@ -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_() {
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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]) {
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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]);
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
@@ -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_();
|
||||||
|
|
||||||
|
|||||||
@@ -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])
|
||||||
|
|||||||
@@ -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])
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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_();
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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])
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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() {
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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
Reference in New Issue
Block a user