mirror of
https://github.com/esphome/esphome.git
synced 2026-03-04 11:48:21 -07:00
Merge branch 'dev' into 20260210-serial-proxy
This commit is contained in:
47
.github/scripts/auto-label-pr/detectors.js
vendored
47
.github/scripts/auto-label-pr/detectors.js
vendored
@@ -7,6 +7,7 @@ const {
|
||||
hasDashboardChanges,
|
||||
hasGitHubActionsChanges,
|
||||
} = require('../detect-tags');
|
||||
const { loadCodeowners, getEffectiveOwners } = require('../codeowners');
|
||||
|
||||
// Strategy: Merge branch detection
|
||||
async function detectMergeBranch(context) {
|
||||
@@ -148,51 +149,15 @@ async function detectGitHubActionsChanges(changedFiles) {
|
||||
// Strategy: Code owner detection
|
||||
async function detectCodeOwner(github, context, changedFiles) {
|
||||
const labels = new Set();
|
||||
const { owner, repo } = context.repo;
|
||||
|
||||
try {
|
||||
const { data: codeownersFile } = await github.rest.repos.getContent({
|
||||
owner,
|
||||
repo,
|
||||
path: 'CODEOWNERS',
|
||||
});
|
||||
|
||||
const codeownersContent = Buffer.from(codeownersFile.content, 'base64').toString('utf8');
|
||||
const codeownersPatterns = loadCodeowners();
|
||||
const prAuthor = context.payload.pull_request.user.login;
|
||||
|
||||
const codeownersLines = codeownersContent.split('\n')
|
||||
.map(line => line.trim())
|
||||
.filter(line => line && !line.startsWith('#'));
|
||||
|
||||
const codeownersRegexes = codeownersLines.map(line => {
|
||||
const parts = line.split(/\s+/);
|
||||
const pattern = parts[0];
|
||||
const owners = parts.slice(1);
|
||||
|
||||
let regex;
|
||||
if (pattern.endsWith('*')) {
|
||||
const dir = pattern.slice(0, -1);
|
||||
regex = new RegExp(`^${dir.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}`);
|
||||
} else if (pattern.includes('*')) {
|
||||
// First escape all regex special chars except *, then replace * with .*
|
||||
const regexPattern = pattern
|
||||
.replace(/[.+?^${}()|[\]\\]/g, '\\$&')
|
||||
.replace(/\*/g, '.*');
|
||||
regex = new RegExp(`^${regexPattern}$`);
|
||||
} else {
|
||||
regex = new RegExp(`^${pattern.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}$`);
|
||||
}
|
||||
|
||||
return { regex, owners };
|
||||
});
|
||||
|
||||
for (const file of changedFiles) {
|
||||
for (const { regex, owners } of codeownersRegexes) {
|
||||
if (regex.test(file) && owners.some(owner => owner === `@${prAuthor}`)) {
|
||||
labels.add('by-code-owner');
|
||||
return labels;
|
||||
}
|
||||
}
|
||||
// Check if PR author is a codeowner of any changed file
|
||||
const effective = getEffectiveOwners(changedFiles, codeownersPatterns);
|
||||
if (effective.users.has(prAuthor)) {
|
||||
labels.add('by-code-owner');
|
||||
}
|
||||
} catch (error) {
|
||||
console.log('Failed to read or parse CODEOWNERS file:', error.message);
|
||||
|
||||
143
.github/scripts/codeowners.js
vendored
Normal file
143
.github/scripts/codeowners.js
vendored
Normal file
@@ -0,0 +1,143 @@
|
||||
// Shared CODEOWNERS parsing and matching utilities.
|
||||
//
|
||||
// Used by:
|
||||
// - codeowner-review-request.yml
|
||||
// - codeowner-approved-label.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);
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
globToRegex,
|
||||
parseCodeowners,
|
||||
fetchCodeowners,
|
||||
loadCodeowners,
|
||||
classifyOwners,
|
||||
getEffectiveOwners
|
||||
};
|
||||
158
.github/workflows/codeowner-approved-label.yml
vendored
Normal file
158
.github/workflows/codeowner-approved-label.yml
vendored
Normal file
@@ -0,0 +1,158 @@
|
||||
# This workflow adds/removes a 'code-owner-approved' label when a
|
||||
# component-specific codeowner approves (or dismisses) a PR.
|
||||
# This helps maintainers prioritize PRs that have codeowner sign-off.
|
||||
#
|
||||
# Only component-specific codeowners count — the catch-all @esphome/core
|
||||
# team is excluded so the label reflects domain-expert approval.
|
||||
|
||||
name: Codeowner Approved Label
|
||||
|
||||
on:
|
||||
pull_request_review:
|
||||
types: [submitted, dismissed]
|
||||
|
||||
permissions:
|
||||
pull-requests: write
|
||||
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 }}
|
||||
|
||||
- name: Check codeowner approval and update label
|
||||
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
|
||||
with:
|
||||
script: |
|
||||
const { loadCodeowners, getEffectiveOwners } = require('./.github/scripts/codeowners.js');
|
||||
|
||||
const owner = context.repo.owner;
|
||||
const repo = context.repo.repo;
|
||||
const pr_number = context.payload.pull_request.number;
|
||||
const LABEL_NAME = 'code-owner-approved';
|
||||
|
||||
console.log(`Processing PR #${pr_number} for codeowner approval label`);
|
||||
|
||||
try {
|
||||
// Get the list of changed files in this PR (with pagination)
|
||||
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, skipping');
|
||||
return;
|
||||
}
|
||||
|
||||
// Parse CODEOWNERS from the checked-out base branch
|
||||
const codeownersPatterns = loadCodeowners();
|
||||
|
||||
// Get effective owners using last-match-wins semantics
|
||||
const effective = getEffectiveOwners(changedFiles, codeownersPatterns);
|
||||
|
||||
// Only keep individual component-specific codeowners (exclude teams)
|
||||
const componentCodeowners = effective.users;
|
||||
|
||||
console.log(`Component-specific codeowners for changed files: ${Array.from(componentCodeowners).join(', ') || '(none)'}`);
|
||||
|
||||
if (componentCodeowners.size === 0) {
|
||||
console.log('No component-specific codeowners found for changed files');
|
||||
// Remove label if present since there are no component codeowners
|
||||
try {
|
||||
await github.rest.issues.removeLabel({
|
||||
owner,
|
||||
repo,
|
||||
issue_number: pr_number,
|
||||
name: LABEL_NAME
|
||||
});
|
||||
console.log(`Removed '${LABEL_NAME}' label (no component codeowners)`);
|
||||
} catch (error) {
|
||||
if (error.status !== 404) {
|
||||
console.log(`Failed to remove label: ${error.message}`);
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Get all reviews on the PR
|
||||
const reviews = await github.paginate(
|
||||
github.rest.pulls.listReviews,
|
||||
{
|
||||
owner,
|
||||
repo,
|
||||
pull_number: pr_number
|
||||
}
|
||||
);
|
||||
|
||||
// Get the latest review per user (reviews are returned chronologically)
|
||||
const latestReviewByUser = new Map();
|
||||
for (const review of reviews) {
|
||||
// Skip bot reviews and comment-only 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;
|
||||
}
|
||||
}
|
||||
|
||||
// Get current labels to check if label is already present
|
||||
const { data: currentLabels } = await github.rest.issues.listLabelsOnIssue({
|
||||
owner,
|
||||
repo,
|
||||
issue_number: pr_number
|
||||
});
|
||||
const hasLabel = currentLabels.some(label => label.name === LABEL_NAME);
|
||||
|
||||
if (hasCodeownerApproval && !hasLabel) {
|
||||
// Add the label
|
||||
await github.rest.issues.addLabels({
|
||||
owner,
|
||||
repo,
|
||||
issue_number: pr_number,
|
||||
labels: [LABEL_NAME]
|
||||
});
|
||||
console.log(`Added '${LABEL_NAME}' label`);
|
||||
} else if (!hasCodeownerApproval && hasLabel) {
|
||||
// Remove the label
|
||||
try {
|
||||
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 !== 404) {
|
||||
console.log(`Failed to remove label: ${error.message}`);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
console.log(`Label already ${hasLabel ? 'present' : 'absent'}, no change needed`);
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
core.setFailed(`Failed to process codeowner approval label: ${error.message}`);
|
||||
}
|
||||
118
.github/workflows/codeowner-review-request.yml
vendored
118
.github/workflows/codeowner-review-request.yml
vendored
@@ -24,10 +24,17 @@ jobs:
|
||||
if: ${{ github.repository == 'esphome/esphome' && !github.event.pull_request.draft }}
|
||||
runs-on: ubuntu-latest
|
||||
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
|
||||
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
|
||||
with:
|
||||
script: |
|
||||
const { loadCodeowners, getEffectiveOwners } = require('./.github/scripts/codeowners.js');
|
||||
|
||||
const owner = context.repo.owner;
|
||||
const repo = context.repo.repo;
|
||||
const pr_number = context.payload.pull_request.number;
|
||||
@@ -38,12 +45,15 @@ jobs:
|
||||
const BOT_COMMENT_MARKER = '<!-- codeowner-review-request-bot -->';
|
||||
|
||||
try {
|
||||
// Get the list of changed files in this PR
|
||||
const { data: files } = await github.rest.pulls.listFiles({
|
||||
owner,
|
||||
repo,
|
||||
pull_number: pr_number
|
||||
});
|
||||
// Get the list of changed files in this PR (with pagination)
|
||||
const files = await github.paginate(
|
||||
github.rest.pulls.listFiles,
|
||||
{
|
||||
owner,
|
||||
repo,
|
||||
pull_number: pr_number
|
||||
}
|
||||
);
|
||||
|
||||
const changedFiles = files.map(file => file.filename);
|
||||
console.log(`Found ${changedFiles.length} changed files`);
|
||||
@@ -53,32 +63,10 @@ jobs:
|
||||
return;
|
||||
}
|
||||
|
||||
// Fetch CODEOWNERS file from root
|
||||
const { data: codeownersFile } = await github.rest.repos.getContent({
|
||||
owner,
|
||||
repo,
|
||||
path: 'CODEOWNERS',
|
||||
ref: context.payload.pull_request.base.sha
|
||||
});
|
||||
const codeownersContent = Buffer.from(codeownersFile.content, 'base64').toString('utf8');
|
||||
// Parse CODEOWNERS from the checked-out base branch
|
||||
const codeownersPatterns = loadCodeowners();
|
||||
|
||||
// Parse CODEOWNERS file to extract all patterns and their owners
|
||||
const codeownersLines = codeownersContent.split('\n')
|
||||
.map(line => line.trim())
|
||||
.filter(line => line && !line.startsWith('#'));
|
||||
|
||||
const codeownersPatterns = [];
|
||||
|
||||
// Convert CODEOWNERS pattern to regex (robust glob handling)
|
||||
function globToRegex(pattern) {
|
||||
// Escape regex special characters except for glob wildcards
|
||||
let regexStr = pattern
|
||||
.replace(/([.+^=!:${}()|[\]\\])/g, '\\$1') // escape regex chars
|
||||
.replace(/\*\*/g, '.*') // globstar
|
||||
.replace(/\*/g, '[^/]*') // single star
|
||||
.replace(/\?/g, '.'); // question mark
|
||||
return new RegExp('^' + regexStr + '$');
|
||||
}
|
||||
console.log(`Parsed ${codeownersPatterns.length} codeowner patterns`);
|
||||
|
||||
// Helper function to create comment body
|
||||
function createCommentBody(reviewersList, teamsList, matchedFileCount, isSuccessful = true) {
|
||||
@@ -93,50 +81,11 @@ jobs:
|
||||
}
|
||||
}
|
||||
|
||||
for (const line of codeownersLines) {
|
||||
const parts = line.split(/\s+/);
|
||||
if (parts.length < 2) continue;
|
||||
|
||||
const pattern = parts[0];
|
||||
const owners = parts.slice(1);
|
||||
|
||||
// Use robust glob-to-regex conversion
|
||||
const regex = globToRegex(pattern);
|
||||
codeownersPatterns.push({ pattern, regex, owners });
|
||||
}
|
||||
|
||||
console.log(`Parsed ${codeownersPatterns.length} codeowner patterns`);
|
||||
|
||||
// Match changed files against CODEOWNERS patterns
|
||||
const matchedOwners = new Set();
|
||||
const matchedTeams = new Set();
|
||||
const fileMatches = new Map(); // Track which files matched which patterns
|
||||
|
||||
for (const file of changedFiles) {
|
||||
for (const { pattern, regex, owners } of codeownersPatterns) {
|
||||
if (regex.test(file)) {
|
||||
console.log(`File '${file}' matches pattern '${pattern}' with owners: ${owners.join(', ')}`);
|
||||
|
||||
if (!fileMatches.has(file)) {
|
||||
fileMatches.set(file, []);
|
||||
}
|
||||
fileMatches.get(file).push({ pattern, owners });
|
||||
|
||||
// Add owners to the appropriate set (remove @ prefix)
|
||||
for (const owner of owners) {
|
||||
const cleanOwner = owner.startsWith('@') ? owner.slice(1) : owner;
|
||||
if (cleanOwner.includes('/')) {
|
||||
// Team mention (org/team-name)
|
||||
const teamName = cleanOwner.split('/')[1];
|
||||
matchedTeams.add(teamName);
|
||||
} else {
|
||||
// Individual user
|
||||
matchedOwners.add(cleanOwner);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// Match changed files against CODEOWNERS patterns using last-match-wins semantics
|
||||
const effective = getEffectiveOwners(changedFiles, codeownersPatterns);
|
||||
const matchedOwners = effective.users;
|
||||
const matchedTeams = effective.teams;
|
||||
const matchedFileCount = effective.matchedFileCount;
|
||||
|
||||
if (matchedOwners.size === 0 && matchedTeams.size === 0) {
|
||||
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
|
||||
const { data: reviews } = await github.rest.pulls.listReviews({
|
||||
owner,
|
||||
repo,
|
||||
pull_number: pr_number
|
||||
});
|
||||
const reviews = await github.paginate(
|
||||
github.rest.pulls.listReviews,
|
||||
{
|
||||
owner,
|
||||
repo,
|
||||
pull_number: pr_number
|
||||
}
|
||||
);
|
||||
|
||||
const reviewedUsers = new Set();
|
||||
reviews.forEach(review => {
|
||||
@@ -247,7 +199,7 @@ jobs:
|
||||
}
|
||||
|
||||
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
|
||||
try {
|
||||
@@ -279,7 +231,7 @@ jobs:
|
||||
|
||||
// Only add a comment if there are new codeowners to mention (not previously pinged)
|
||||
if (reviewersList.length > 0 || teamsList.length > 0) {
|
||||
const commentBody = createCommentBody(reviewersList, teamsList, fileMatches.size, true);
|
||||
const commentBody = createCommentBody(reviewersList, teamsList, matchedFileCount, true);
|
||||
|
||||
await github.rest.issues.createComment({
|
||||
owner,
|
||||
@@ -297,7 +249,7 @@ jobs:
|
||||
|
||||
// Only try to add a comment if there are new codeowners to mention
|
||||
if (reviewersList.length > 0 || teamsList.length > 0) {
|
||||
const commentBody = createCommentBody(reviewersList, teamsList, fileMatches.size, false);
|
||||
const commentBody = createCommentBody(reviewersList, teamsList, matchedFileCount, false);
|
||||
|
||||
try {
|
||||
await github.rest.issues.createComment({
|
||||
|
||||
4
.github/workflows/codeql.yml
vendored
4
.github/workflows/codeql.yml
vendored
@@ -58,7 +58,7 @@ jobs:
|
||||
|
||||
# Initializes the CodeQL tools for scanning.
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@89a39a4e59826350b863aa6b6252a07ad50cf83e # v4.32.4
|
||||
uses: github/codeql-action/init@c793b717bc78562f491db7b0e93a3a178b099162 # v4.32.5
|
||||
with:
|
||||
languages: ${{ matrix.language }}
|
||||
build-mode: ${{ matrix.build-mode }}
|
||||
@@ -86,6 +86,6 @@ jobs:
|
||||
exit 1
|
||||
|
||||
- name: Perform CodeQL Analysis
|
||||
uses: github/codeql-action/analyze@89a39a4e59826350b863aa6b6252a07ad50cf83e # v4.32.4
|
||||
uses: github/codeql-action/analyze@c793b717bc78562f491db7b0e93a3a178b099162 # v4.32.5
|
||||
with:
|
||||
category: "/language:${{matrix.language}}"
|
||||
|
||||
9
.github/workflows/pr-title-check.yml
vendored
9
.github/workflows/pr-title-check.yml
vendored
@@ -26,14 +26,19 @@ jobs:
|
||||
} = require('./.github/scripts/detect-tags.js');
|
||||
|
||||
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
|
||||
const commitStylePattern = /^\w+(\(.*?\))?[!]?\s*:/;
|
||||
if (commitStylePattern.test(title)) {
|
||||
core.setFailed(
|
||||
`PR title should not start with a "prefix:" style format.\n` +
|
||||
`Please use the format: [component] Brief description\n` +
|
||||
`Example: [pn532] Add health checking and auto-reset`
|
||||
`Please use the format: [component] Brief description\n`
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -316,6 +316,7 @@ esphome/components/mcp9808/* @k7hpn
|
||||
esphome/components/md5/* @esphome/core
|
||||
esphome/components/mdns/* @esphome/core
|
||||
esphome/components/media_player/* @jesserockz
|
||||
esphome/components/media_source/* @kahrendt
|
||||
esphome/components/micro_wake_word/* @jesserockz @kahrendt
|
||||
esphome/components/micronova/* @edenhaus @jorre05
|
||||
esphome/components/microphone/* @jesserockz @kahrendt
|
||||
@@ -411,7 +412,7 @@ esphome/components/rp2040_pio_led_strip/* @Papa-DMan
|
||||
esphome/components/rp2040_pwm/* @jesserockz
|
||||
esphome/components/rpi_dpi_rgb/* @clydebarrow
|
||||
esphome/components/rtl87xx/* @kuba2k2
|
||||
esphome/components/rtttl/* @glmnet
|
||||
esphome/components/rtttl/* @glmnet @ximex
|
||||
esphome/components/runtime_image/* @clydebarrow @guillempages @kahrendt
|
||||
esphome/components/runtime_stats/* @bdraco
|
||||
esphome/components/rx8130/* @beormund
|
||||
|
||||
@@ -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):
|
||||
await setup_entity(var, config, "alarm_control_panel")
|
||||
for conf in config.get(CONF_ON_STATE, []):
|
||||
trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var)
|
||||
await automation.build_automation(trigger, [], conf)
|
||||
|
||||
@@ -773,9 +773,9 @@ uint16_t APIConnection::try_send_number_state(EntityBase *entity, APIConnection
|
||||
uint16_t APIConnection::try_send_number_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size) {
|
||||
auto *number = static_cast<number::Number *>(entity);
|
||||
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.device_class = number->traits.get_device_class_ref();
|
||||
msg.device_class = number->get_device_class_ref();
|
||||
msg.min_value = number->traits.get_min_value();
|
||||
msg.max_value = number->traits.get_max_value();
|
||||
msg.step = number->traits.get_step();
|
||||
|
||||
@@ -9,6 +9,7 @@ from esphome.const import (
|
||||
CONF_ID,
|
||||
CONF_POWER_MODE,
|
||||
CONF_RANGE,
|
||||
CONF_WATCHDOG,
|
||||
)
|
||||
|
||||
CODEOWNERS = ["@ammmze"]
|
||||
@@ -57,7 +58,6 @@ FAST_FILTER = {
|
||||
|
||||
CONF_RAW_ANGLE = "raw_angle"
|
||||
CONF_RAW_POSITION = "raw_position"
|
||||
CONF_WATCHDOG = "watchdog"
|
||||
CONF_SLOW_FILTER = "slow_filter"
|
||||
CONF_FAST_FILTER = "fast_filter"
|
||||
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_POSITION = "raw_position"
|
||||
CONF_WATCHDOG = "watchdog"
|
||||
CONF_SLOW_FILTER = "slow_filter"
|
||||
CONF_FAST_FILTER = "fast_filter"
|
||||
CONF_PWM_FREQUENCY = "pwm_frequency"
|
||||
|
||||
@@ -60,7 +60,11 @@ from esphome.const import (
|
||||
DEVICE_CLASS_WINDOW,
|
||||
)
|
||||
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.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):
|
||||
await setup_entity(var, config, "binary_sensor")
|
||||
|
||||
if (device_class := config.get(CONF_DEVICE_CLASS)) is not None:
|
||||
cg.add(var.set_device_class(device_class))
|
||||
setup_device_class(config)
|
||||
trigger = config.get(CONF_TRIGGER_ON_INITIAL_STATE, False) or config.get(
|
||||
CONF_PUBLISH_INITIAL_STATE, False
|
||||
)
|
||||
|
||||
@@ -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
|
||||
* handles inverted inputs for you.
|
||||
*/
|
||||
class BinarySensor : public StatefulEntityBase<bool>, public EntityBase_DeviceClass {
|
||||
class BinarySensor : public StatefulEntityBase<bool> {
|
||||
public:
|
||||
explicit BinarySensor(){};
|
||||
|
||||
|
||||
@@ -18,7 +18,11 @@ from esphome.const import (
|
||||
DEVICE_CLASS_UPDATE,
|
||||
)
|
||||
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
|
||||
|
||||
CODEOWNERS = ["@esphome/core"]
|
||||
@@ -84,15 +88,13 @@ def button_schema(
|
||||
return _BUTTON_SCHEMA.extend(schema)
|
||||
|
||||
|
||||
@setup_entity("button")
|
||||
async def setup_button_core_(var, config):
|
||||
await setup_entity(var, config, "button")
|
||||
|
||||
for conf in config.get(CONF_ON_PRESS, []):
|
||||
trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var)
|
||||
await automation.build_automation(trigger, [], conf)
|
||||
|
||||
if device_class := config.get(CONF_DEVICE_CLASS):
|
||||
cg.add(var.set_device_class(device_class))
|
||||
setup_device_class(config)
|
||||
|
||||
if mqtt_id := config.get(CONF_MQTT_ID):
|
||||
mqtt_ = cg.new_Pvariable(mqtt_id, var)
|
||||
|
||||
@@ -22,7 +22,7 @@ void log_button(const char *tag, const char *prefix, const char *type, Button *o
|
||||
*
|
||||
* A button is just a momentary switch that does not have a state, only a trigger.
|
||||
*/
|
||||
class Button : public EntityBase, public EntityBase_DeviceClass {
|
||||
class Button : public EntityBase {
|
||||
public:
|
||||
/** Press this button. This is called by the front-end.
|
||||
*
|
||||
|
||||
@@ -268,9 +268,8 @@ def climate_schema(
|
||||
return _CLIMATE_SCHEMA.extend(schema)
|
||||
|
||||
|
||||
@setup_entity("climate")
|
||||
async def setup_climate_core_(var, config):
|
||||
await setup_entity(var, config, "climate")
|
||||
|
||||
visual = config[CONF_VISUAL]
|
||||
if (min_temp := visual.get(CONF_MIN_TEMPERATURE)) is not None:
|
||||
cg.add_define("USE_CLIMATE_VISUAL_OVERRIDES")
|
||||
|
||||
@@ -8,14 +8,17 @@ BYTE_ORDER_BIG = "big_endian"
|
||||
|
||||
CONF_COLOR_DEPTH = "color_depth"
|
||||
CONF_CRC_ENABLE = "crc_enable"
|
||||
CONF_DATA_BITS = "data_bits"
|
||||
CONF_DRAW_ROUNDING = "draw_rounding"
|
||||
CONF_ENABLED = "enabled"
|
||||
CONF_IGNORE_NOT_FOUND = "ignore_not_found"
|
||||
CONF_ON_PACKET = "on_packet"
|
||||
CONF_ON_RECEIVE = "on_receive"
|
||||
CONF_ON_STATE_CHANGE = "on_state_change"
|
||||
CONF_PARITY = "parity"
|
||||
CONF_REQUEST_HEADERS = "request_headers"
|
||||
CONF_ROWS = "rows"
|
||||
CONF_STOP_BITS = "stop_bits"
|
||||
CONF_USE_PSRAM = "use_psram"
|
||||
|
||||
ICON_CURRENT_DC = "mdi:current-dc"
|
||||
|
||||
@@ -37,7 +37,11 @@ from esphome.const import (
|
||||
DEVICE_CLASS_WINDOW,
|
||||
)
|
||||
from esphome.core import CORE, ID, 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 MockObj, MockObjClass
|
||||
from esphome.types import ConfigType, TemplateArgsType
|
||||
|
||||
@@ -190,11 +194,9 @@ def cover_schema(
|
||||
return _COVER_SCHEMA.extend(schema)
|
||||
|
||||
|
||||
@setup_entity("cover")
|
||||
async def setup_cover_core_(var, config):
|
||||
await setup_entity(var, config, "cover")
|
||||
|
||||
if (device_class := config.get(CONF_DEVICE_CLASS)) is not None:
|
||||
cg.add(var.set_device_class(device_class))
|
||||
setup_device_class(config)
|
||||
|
||||
if CONF_ON_OPEN in config:
|
||||
_LOGGER.warning(
|
||||
|
||||
@@ -107,7 +107,7 @@ const LogString *cover_operation_to_str(CoverOperation op);
|
||||
* to control all values of the cover. Also implement get_traits() to return what operations
|
||||
* the cover supports.
|
||||
*/
|
||||
class Cover : public EntityBase, public EntityBase_DeviceClass {
|
||||
class Cover : public EntityBase {
|
||||
public:
|
||||
explicit Cover();
|
||||
|
||||
|
||||
@@ -134,9 +134,8 @@ def datetime_schema(class_: MockObjClass) -> cv.Schema:
|
||||
return _DATETIME_SCHEMA.extend(schema)
|
||||
|
||||
|
||||
@setup_entity("datetime")
|
||||
async def setup_datetime_core_(var, config):
|
||||
await setup_entity(var, config, "datetime")
|
||||
|
||||
if (mqtt_id := config.get(CONF_MQTT_ID)) is not None:
|
||||
mqtt_ = cg.new_Pvariable(mqtt_id, var)
|
||||
await mqtt.register_mqtt_component(mqtt_, config)
|
||||
|
||||
@@ -22,8 +22,8 @@ extern "C" __attribute__((weak)) void initArduino() {}
|
||||
namespace esphome {
|
||||
|
||||
void HOT yield() { vPortYield(); }
|
||||
uint32_t IRAM_ATTR HOT millis() { return (uint32_t) (esp_timer_get_time() / 1000ULL); }
|
||||
uint64_t HOT millis_64() { return static_cast<uint64_t>(esp_timer_get_time()) / 1000ULL; }
|
||||
uint32_t IRAM_ATTR HOT millis() { return micros_to_millis(static_cast<uint64_t>(esp_timer_get_time())); }
|
||||
uint64_t HOT millis_64() { return micros_to_millis<uint64_t>(static_cast<uint64_t>(esp_timer_get_time())); }
|
||||
void HOT delay(uint32_t ms) { vTaskDelay(ms / portTICK_PERIOD_MS); }
|
||||
uint32_t IRAM_ATTR HOT micros() { return (uint32_t) esp_timer_get_time(); }
|
||||
void IRAM_ATTR HOT delayMicroseconds(uint32_t us) { delay_microseconds_safe(us); }
|
||||
@@ -48,6 +48,7 @@ void arch_init() {
|
||||
void HOT arch_feed_wdt() { esp_task_wdt_reset(); }
|
||||
|
||||
uint8_t progmem_read_byte(const uint8_t *addr) { return *addr; }
|
||||
const char *progmem_read_ptr(const char *const *addr) { return *addr; }
|
||||
uint16_t progmem_read_uint16(const uint16_t *addr) { return *addr; }
|
||||
uint32_t arch_get_cpu_cycle_count() { return esp_cpu_get_cycle_count(); }
|
||||
uint32_t arch_get_cpu_freq_hz() {
|
||||
|
||||
@@ -34,6 +34,9 @@ void HOT arch_feed_wdt() { system_soft_wdt_feed(); }
|
||||
uint8_t progmem_read_byte(const uint8_t *addr) {
|
||||
return pgm_read_byte(addr); // NOLINT
|
||||
}
|
||||
const char *progmem_read_ptr(const char *const *addr) {
|
||||
return reinterpret_cast<const char *>(pgm_read_ptr(addr)); // NOLINT
|
||||
}
|
||||
uint16_t progmem_read_uint16(const uint16_t *addr) {
|
||||
return pgm_read_word(addr); // NOLINT
|
||||
}
|
||||
|
||||
@@ -18,7 +18,11 @@ from esphome.const import (
|
||||
DEVICE_CLASS_MOTION,
|
||||
)
|
||||
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
|
||||
|
||||
CODEOWNERS = ["@nohat"]
|
||||
@@ -85,17 +89,15 @@ def event_schema(
|
||||
return _EVENT_SCHEMA.extend(schema)
|
||||
|
||||
|
||||
@setup_entity("event")
|
||||
async def setup_event_core_(var, config, *, event_types: list[str]):
|
||||
await setup_entity(var, config, "event")
|
||||
|
||||
for conf in config.get(CONF_ON_EVENT, []):
|
||||
trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var)
|
||||
await automation.build_automation(trigger, [(cg.StringRef, "event_type")], conf)
|
||||
|
||||
cg.add(var.set_event_types(event_types))
|
||||
|
||||
if (device_class := config.get(CONF_DEVICE_CLASS)) is not None:
|
||||
cg.add(var.set_device_class(device_class))
|
||||
setup_device_class(config)
|
||||
|
||||
if mqtt_id := config.get(CONF_MQTT_ID):
|
||||
mqtt_ = cg.new_Pvariable(mqtt_id, var)
|
||||
|
||||
@@ -20,7 +20,7 @@ namespace event {
|
||||
LOG_ENTITY_DEVICE_CLASS(TAG, prefix, *(obj)); \
|
||||
}
|
||||
|
||||
class Event : public EntityBase, public EntityBase_DeviceClass {
|
||||
class Event : public EntityBase {
|
||||
public:
|
||||
void trigger(const std::string &event_type);
|
||||
|
||||
|
||||
@@ -222,9 +222,8 @@ def validate_preset_modes(value):
|
||||
return value
|
||||
|
||||
|
||||
@setup_entity("fan")
|
||||
async def setup_fan_core_(var, config):
|
||||
await setup_entity(var, config, "fan")
|
||||
|
||||
cg.add(var.set_restore_mode(config[CONF_RESTORE_MODE]))
|
||||
|
||||
if (mqtt_id := config.get(CONF_MQTT_ID)) is not None:
|
||||
|
||||
@@ -51,7 +51,7 @@ _RESTORING_SCHEMA = cv.Schema(
|
||||
|
||||
def _globals_schema(config: ConfigType) -> ConfigType:
|
||||
"""Select schema based on restore_value setting."""
|
||||
if config.get(CONF_RESTORE_VALUE, False):
|
||||
if cv.boolean(config.get(CONF_RESTORE_VALUE, False)):
|
||||
return _RESTORING_SCHEMA(config)
|
||||
return _NON_RESTORING_SCHEMA(config)
|
||||
|
||||
|
||||
@@ -59,6 +59,7 @@ void HOT arch_feed_wdt() {
|
||||
}
|
||||
|
||||
uint8_t progmem_read_byte(const uint8_t *addr) { return *addr; }
|
||||
const char *progmem_read_ptr(const char *const *addr) { return *addr; }
|
||||
uint16_t progmem_read_uint16(const uint16_t *addr) { return *addr; }
|
||||
uint32_t arch_get_cpu_cycle_count() {
|
||||
struct timespec spec;
|
||||
|
||||
@@ -45,9 +45,9 @@ def infrared_schema(class_: type[cg.MockObjClass]) -> cv.Schema:
|
||||
)
|
||||
|
||||
|
||||
@setup_entity("infrared")
|
||||
async def setup_infrared_core_(var: cg.Pvariable, config: ConfigType) -> None:
|
||||
"""Set up core infrared configuration."""
|
||||
await setup_entity(var, config, "infrared")
|
||||
|
||||
|
||||
async def register_infrared(var: cg.Pvariable, config: ConfigType) -> None:
|
||||
|
||||
@@ -474,15 +474,12 @@ void LD2450Component::handle_periodic_data_() {
|
||||
is_moving = false;
|
||||
// tx is used for further calculations, so always needs to be populated
|
||||
tx = ld2450::decode_coordinate(this->buffer_data_[start], this->buffer_data_[start + 1]);
|
||||
SAFE_PUBLISH_SENSOR(this->move_x_sensors_[index], tx);
|
||||
// Y
|
||||
start = TARGET_Y + index * 8;
|
||||
ty = ld2450::decode_coordinate(this->buffer_data_[start], this->buffer_data_[start + 1]);
|
||||
SAFE_PUBLISH_SENSOR(this->move_y_sensors_[index], ty);
|
||||
// RESOLUTION
|
||||
start = TARGET_RESOLUTION + index * 8;
|
||||
res = (this->buffer_data_[start + 1] << 8) | this->buffer_data_[start];
|
||||
SAFE_PUBLISH_SENSOR(this->move_resolution_sensors_[index], res);
|
||||
#endif
|
||||
// SPEED
|
||||
start = TARGET_SPEED + index * 8;
|
||||
@@ -491,9 +488,6 @@ void LD2450Component::handle_periodic_data_() {
|
||||
is_moving = true;
|
||||
moving_target_count++;
|
||||
}
|
||||
#ifdef USE_SENSOR
|
||||
SAFE_PUBLISH_SENSOR(this->move_speed_sensors_[index], ts);
|
||||
#endif
|
||||
// DISTANCE
|
||||
// Optimized: use already decoded tx and ty values, replace pow() with multiplication
|
||||
int32_t x_squared = (int32_t) tx * tx;
|
||||
@@ -503,10 +497,23 @@ void LD2450Component::handle_periodic_data_() {
|
||||
target_count++;
|
||||
}
|
||||
#ifdef USE_SENSOR
|
||||
SAFE_PUBLISH_SENSOR(this->move_distance_sensors_[index], td);
|
||||
// ANGLE - atan2f computes angle from Y axis directly, no sqrt/division needed
|
||||
angle = atan2f(static_cast<float>(-tx), static_cast<float>(ty)) * (180.0f / std::numbers::pi_v<float>);
|
||||
SAFE_PUBLISH_SENSOR(this->move_angle_sensors_[index], angle);
|
||||
if (td == 0) {
|
||||
SAFE_PUBLISH_SENSOR_UNKNOWN(this->move_x_sensors_[index]);
|
||||
SAFE_PUBLISH_SENSOR_UNKNOWN(this->move_y_sensors_[index]);
|
||||
SAFE_PUBLISH_SENSOR_UNKNOWN(this->move_resolution_sensors_[index]);
|
||||
SAFE_PUBLISH_SENSOR_UNKNOWN(this->move_speed_sensors_[index]);
|
||||
SAFE_PUBLISH_SENSOR_UNKNOWN(this->move_distance_sensors_[index]);
|
||||
SAFE_PUBLISH_SENSOR_UNKNOWN(this->move_angle_sensors_[index]);
|
||||
} else {
|
||||
SAFE_PUBLISH_SENSOR(this->move_x_sensors_[index], tx);
|
||||
SAFE_PUBLISH_SENSOR(this->move_y_sensors_[index], ty);
|
||||
SAFE_PUBLISH_SENSOR(this->move_resolution_sensors_[index], res);
|
||||
SAFE_PUBLISH_SENSOR(this->move_speed_sensors_[index], ts);
|
||||
SAFE_PUBLISH_SENSOR(this->move_distance_sensors_[index], td);
|
||||
// ANGLE - atan2f computes angle from Y axis directly, no sqrt/division needed
|
||||
angle = atan2f(static_cast<float>(-tx), static_cast<float>(ty)) * (180.0f / std::numbers::pi_v<float>);
|
||||
SAFE_PUBLISH_SENSOR(this->move_angle_sensors_[index], angle);
|
||||
}
|
||||
#endif
|
||||
#ifdef USE_TEXT_SENSOR
|
||||
// DIRECTION
|
||||
|
||||
@@ -7,6 +7,9 @@
|
||||
#include "esphome/core/helpers.h"
|
||||
#include "preferences.h"
|
||||
|
||||
#include <FreeRTOS.h>
|
||||
#include <task.h>
|
||||
|
||||
void setup();
|
||||
void loop();
|
||||
|
||||
@@ -22,6 +25,22 @@ void IRAM_ATTR HOT delayMicroseconds(uint32_t us) { ::delayMicroseconds(us); }
|
||||
void arch_init() {
|
||||
libretiny::setup_preferences();
|
||||
lt_wdt_enable(10000L);
|
||||
#ifdef USE_BK72XX
|
||||
// BK72xx SDK creates the main Arduino task at priority 3, which is lower than
|
||||
// all WiFi (4-5), LwIP (4), and TCP/IP (7) tasks. This causes ~100ms loop
|
||||
// stalls whenever WiFi background processing runs, because the main task
|
||||
// cannot resume until every higher-priority task finishes.
|
||||
//
|
||||
// By contrast, RTL87xx creates the main task at osPriorityRealtime (highest).
|
||||
//
|
||||
// Raise to priority 6: above WiFi/LwIP tasks (4-5) so they don't preempt the
|
||||
// main loop, but below the TCP/IP thread (7) so packet processing keeps priority.
|
||||
// This is safe because ESPHome yields voluntarily via yield_with_select_() and
|
||||
// the Arduino mainTask yield() after each loop() iteration.
|
||||
static constexpr UBaseType_t MAIN_TASK_PRIORITY = 6;
|
||||
static_assert(MAIN_TASK_PRIORITY < configMAX_PRIORITIES, "MAIN_TASK_PRIORITY must be less than configMAX_PRIORITIES");
|
||||
vTaskPrioritySet(nullptr, MAIN_TASK_PRIORITY);
|
||||
#endif
|
||||
#if LT_GPIO_RECOVER
|
||||
lt_gpio_recover();
|
||||
#endif
|
||||
@@ -36,6 +55,7 @@ void HOT arch_feed_wdt() { lt_wdt_feed(); }
|
||||
uint32_t arch_get_cpu_cycle_count() { return lt_cpu_get_cycle_count(); }
|
||||
uint32_t arch_get_cpu_freq_hz() { return lt_cpu_get_freq(); }
|
||||
uint8_t progmem_read_byte(const uint8_t *addr) { return *addr; }
|
||||
const char *progmem_read_ptr(const char *const *addr) { return *addr; }
|
||||
uint16_t progmem_read_uint16(const uint16_t *addr) { return *addr; }
|
||||
|
||||
} // namespace esphome
|
||||
|
||||
@@ -243,9 +243,8 @@ def validate_color_temperature_channels(value):
|
||||
return value
|
||||
|
||||
|
||||
async def setup_light_core_(light_var, output_var, config):
|
||||
await setup_entity(light_var, config, "light")
|
||||
|
||||
@setup_entity("light")
|
||||
async def setup_light_core_(light_var, config, output_var):
|
||||
cg.add(light_var.set_restore_mode(config[CONF_RESTORE_MODE]))
|
||||
|
||||
if (initial_state_config := config.get(CONF_INITIAL_STATE)) is not None:
|
||||
@@ -312,7 +311,7 @@ async def register_light(output_var, config):
|
||||
cg.add(cg.App.register_light(light_var))
|
||||
CORE.register_platform_component("light", light_var)
|
||||
await cg.register_component(light_var, config)
|
||||
await setup_light_core_(light_var, output_var, config)
|
||||
await setup_light_core_(light_var, config, output_var)
|
||||
|
||||
|
||||
async def new_light(config, *args):
|
||||
|
||||
@@ -91,9 +91,8 @@ def lock_schema(
|
||||
return _LOCK_SCHEMA.extend(schema)
|
||||
|
||||
|
||||
@setup_entity("lock")
|
||||
async def _setup_lock_core(var, config):
|
||||
await setup_entity(var, config, "lock")
|
||||
|
||||
for conf in config.get(CONF_ON_LOCK, []):
|
||||
trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var)
|
||||
await automation.build_automation(trigger, [], conf)
|
||||
|
||||
@@ -8,6 +8,7 @@ static constexpr const char *const TAG = "lps22";
|
||||
static constexpr uint8_t WHO_AM_I = 0x0F;
|
||||
static constexpr uint8_t LPS22HB_ID = 0xB1;
|
||||
static constexpr uint8_t LPS22HH_ID = 0xB3;
|
||||
static constexpr uint8_t LPS22DF_ID = 0xB4;
|
||||
static constexpr uint8_t CTRL_REG2 = 0x11;
|
||||
static constexpr uint8_t CTRL_REG2_ONE_SHOT_MASK = 0b1;
|
||||
static constexpr uint8_t STATUS = 0x27;
|
||||
@@ -24,8 +25,8 @@ static constexpr float TEMPERATURE_SCALE = 0.01f;
|
||||
void LPS22Component::setup() {
|
||||
uint8_t value = 0x00;
|
||||
this->read_register(WHO_AM_I, &value, 1);
|
||||
if (value != LPS22HB_ID && value != LPS22HH_ID) {
|
||||
ESP_LOGW(TAG, "device IDs as %02x, which isn't a known LPS22HB or LPS22HH ID", value);
|
||||
if (value != LPS22HB_ID && value != LPS22HH_ID && value != LPS22DF_ID) {
|
||||
ESP_LOGW(TAG, "device IDs as %02x, which isn't a known LPS22HB/HH/DF ID", value);
|
||||
this->mark_failed();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -96,8 +96,8 @@ VolumeSetAction = media_player_ns.class_(
|
||||
)
|
||||
|
||||
|
||||
@setup_entity("media_player")
|
||||
async def setup_media_player_core_(var, config):
|
||||
await setup_entity(var, config, "media_player")
|
||||
for conf_key, _ in _STATE_TRIGGERS:
|
||||
for conf in config.get(conf_key, []):
|
||||
trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var)
|
||||
|
||||
40
esphome/components/media_source/__init__.py
Normal file
40
esphome/components/media_source/__init__.py
Normal file
@@ -0,0 +1,40 @@
|
||||
import esphome.codegen as cg
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import CONF_ID
|
||||
from esphome.core import CORE
|
||||
from esphome.coroutine import CoroPriority, coroutine_with_priority
|
||||
from esphome.cpp_generator import MockObjClass
|
||||
|
||||
CODEOWNERS = ["@kahrendt"]
|
||||
|
||||
AUTO_LOAD = ["audio"]
|
||||
|
||||
IS_PLATFORM_COMPONENT = True
|
||||
|
||||
media_source_ns = cg.esphome_ns.namespace("media_source")
|
||||
|
||||
MediaSource = media_source_ns.class_("MediaSource")
|
||||
|
||||
|
||||
async def register_media_source(var, config):
|
||||
if not CORE.has_id(config[CONF_ID]):
|
||||
var = cg.Pvariable(config[CONF_ID], var)
|
||||
CORE.register_platform_component("media_source", var)
|
||||
return var
|
||||
|
||||
|
||||
_MEDIA_SOURCE_SCHEMA = cv.Schema({})
|
||||
|
||||
|
||||
def media_source_schema(
|
||||
class_: MockObjClass,
|
||||
) -> cv.Schema:
|
||||
schema = {cv.GenerateID(CONF_ID): cv.declare_id(class_)}
|
||||
|
||||
return _MEDIA_SOURCE_SCHEMA.extend(schema)
|
||||
|
||||
|
||||
@coroutine_with_priority(CoroPriority.CORE)
|
||||
async def to_code(config):
|
||||
cg.add_global(media_source_ns.using)
|
||||
cg.add_define("USE_MEDIA_SOURCE")
|
||||
159
esphome/components/media_source/media_source.h
Normal file
159
esphome/components/media_source/media_source.h
Normal file
@@ -0,0 +1,159 @@
|
||||
#pragma once
|
||||
|
||||
#include "esphome/components/audio/audio.h"
|
||||
#include "esphome/core/helpers.h"
|
||||
|
||||
#include <cstdint>
|
||||
#include <string>
|
||||
|
||||
namespace esphome::media_source {
|
||||
|
||||
enum class MediaSourceState : uint8_t {
|
||||
IDLE, // Not playing, ready to accept play_uri
|
||||
PLAYING, // Currently playing media
|
||||
PAUSED, // Playback paused, can be resumed
|
||||
ERROR, // Error occurred during playback; sources are responsible for logging their own error details
|
||||
};
|
||||
|
||||
/// @brief Commands that are sent from the orchestrator to a media source
|
||||
enum class MediaSourceCommand : uint8_t {
|
||||
// All sources should support these basic commands.
|
||||
PLAY,
|
||||
PAUSE,
|
||||
STOP,
|
||||
|
||||
// Only sources with internal playlists will handle these; simple sources should ignore them.
|
||||
NEXT,
|
||||
PREVIOUS,
|
||||
CLEAR_PLAYLIST,
|
||||
REPEAT_ALL,
|
||||
REPEAT_ONE,
|
||||
REPEAT_OFF,
|
||||
SHUFFLE,
|
||||
UNSHUFFLE,
|
||||
};
|
||||
|
||||
/// @brief Callbacks from a MediaSource to its orchestrator
|
||||
class MediaSourceListener {
|
||||
public:
|
||||
virtual ~MediaSourceListener() = default;
|
||||
|
||||
// Callbacks that all sources use to send data and state changes to the orchestrator.
|
||||
/// @brief Send audio data to the listener
|
||||
virtual size_t write_audio(const uint8_t *data, size_t length, uint32_t timeout_ms,
|
||||
const audio::AudioStreamInfo &stream_info) = 0;
|
||||
/// @brief Notify listener of state changes
|
||||
virtual void report_state(MediaSourceState state) = 0;
|
||||
|
||||
// Callbacks from smart sources requesting the orchestrator to change volume, mute, or start a new URI.
|
||||
// Simple sources never invoke these.
|
||||
/// @brief Request the orchestrator to change volume
|
||||
virtual void request_volume(float volume) {}
|
||||
/// @brief Request the orchestrator to change mute state
|
||||
virtual void request_mute(bool is_muted) {}
|
||||
/// @brief Request the orchestrator to play a new URI
|
||||
virtual void request_play_uri(const std::string &uri) {}
|
||||
};
|
||||
|
||||
/// @brief Abstract base class for media sources
|
||||
/// MediaSource provides audio data to an orchestrator via the MediaSourceListener interface. It also receives commands
|
||||
/// from the orchestrator to control playback.
|
||||
class MediaSource {
|
||||
public:
|
||||
virtual ~MediaSource() = default;
|
||||
|
||||
// === Playback Control ===
|
||||
|
||||
/// @brief Start playing the given URI
|
||||
/// Sources should validate the URI and state, returning false if the source is busy.
|
||||
/// The orchestrator is responsible for stopping active sources before starting a new one.
|
||||
/// @param uri URI to play; e.g., "http://stream_url"
|
||||
/// @return true if playback started successfully, false otherwise
|
||||
virtual bool play_uri(const std::string &uri) = 0;
|
||||
|
||||
/// @brief Handle playback commands (pause, stop, next, etc.)
|
||||
/// @param command Command to execute
|
||||
virtual void handle_command(MediaSourceCommand command) = 0;
|
||||
|
||||
/// @brief Whether this source manages its own playlist internally
|
||||
/// Smart sources that handle next/previous/repeat/shuffle should override this to return true.
|
||||
virtual bool has_internal_playlist() const { return false; }
|
||||
|
||||
// === State Access ===
|
||||
|
||||
/// @brief Get current playback state (must only be called from the main loop)
|
||||
/// @return Current state of this source
|
||||
MediaSourceState get_state() const { return this->state_; }
|
||||
|
||||
// === URI Matching ===
|
||||
|
||||
/// @brief Check if this source can handle the given URI
|
||||
/// Each source must override this to match its supported URI scheme(s).
|
||||
/// @param uri URI to check
|
||||
/// @return true if this source can handle the URI
|
||||
virtual bool can_handle(const std::string &uri) const = 0;
|
||||
|
||||
// === Listener: Source -> Orchestrator ===
|
||||
|
||||
/// @brief Set the listener that receives callbacks from this source
|
||||
/// @param listener Pointer to the MediaSourceListener implementation
|
||||
void set_listener(MediaSourceListener *listener) { this->listener_ = listener; }
|
||||
|
||||
/// @brief Check if a listener has been registered
|
||||
bool has_listener() const { return this->listener_ != nullptr; }
|
||||
|
||||
/// @brief Write audio data to the listener
|
||||
/// @param data Pointer to audio data buffer (not modified by this method)
|
||||
/// @param length Number of bytes to write
|
||||
/// @param timeout_ms Milliseconds to wait if the listener can't accept data immediately
|
||||
/// @param stream_info Audio stream format information
|
||||
/// @return Number of bytes written, or 0 if no listener is set
|
||||
size_t write_output(const uint8_t *data, size_t length, uint32_t timeout_ms,
|
||||
const audio::AudioStreamInfo &stream_info) {
|
||||
if (this->listener_ != nullptr) {
|
||||
return this->listener_->write_audio(data, length, timeout_ms, stream_info);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
// === Callbacks: Orchestrator -> Source ===
|
||||
|
||||
/// @brief Notify the source that volume changed
|
||||
/// Simple sources ignore this. Override for smart sources that track volume state.
|
||||
/// @param volume New volume level (0.0 to 1.0)
|
||||
virtual void notify_volume_changed(float volume) {}
|
||||
|
||||
/// @brief Notify the source that mute state changed
|
||||
/// Simple sources ignore this. Override for smart sources that track mute state.
|
||||
/// @param is_muted New mute state
|
||||
virtual void notify_mute_changed(bool is_muted) {}
|
||||
|
||||
/// @brief Notify the source about audio that has been played
|
||||
/// Called when the speaker reports that audio frames have been written to the DAC.
|
||||
/// Sources can override this to track playback progress for synchronization.
|
||||
/// @param frames Number of audio frames that were played
|
||||
/// @param timestamp System time in microseconds when the frames finished writing to the DAC
|
||||
virtual void notify_audio_played(uint32_t frames, int64_t timestamp) {}
|
||||
|
||||
protected:
|
||||
/// @brief Update state and notify listener (must only be called from the main loop)
|
||||
/// This is the only way to change state_, ensuring listener notifications always fire.
|
||||
/// Sources running FreeRTOS tasks should signal via event groups and call this from loop().
|
||||
/// @param state New state to set
|
||||
void set_state_(MediaSourceState state) {
|
||||
if (this->state_ != state) {
|
||||
this->state_ = state;
|
||||
if (this->listener_ != nullptr) {
|
||||
this->listener_->report_state(state);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private:
|
||||
// Private to enforce the invariant that listener notifications always fire on state changes.
|
||||
// All state transitions must go through set_state_() which couples the update with notification.
|
||||
MediaSourceState state_{MediaSourceState::IDLE};
|
||||
MediaSourceListener *listener_{nullptr};
|
||||
};
|
||||
|
||||
} // namespace esphome::media_source
|
||||
@@ -48,7 +48,7 @@ void MQTTNumberComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryCon
|
||||
root[MQTT_MAX] = traits.get_max_value();
|
||||
root[MQTT_STEP] = traits.get_step();
|
||||
// NOLINTBEGIN(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson
|
||||
const auto unit_of_measurement = this->number_->traits.get_unit_of_measurement_ref();
|
||||
const auto unit_of_measurement = this->number_->get_unit_of_measurement_ref();
|
||||
if (!unit_of_measurement.empty()) {
|
||||
root[MQTT_UNIT_OF_MEASUREMENT] = unit_of_measurement;
|
||||
}
|
||||
@@ -57,7 +57,7 @@ void MQTTNumberComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryCon
|
||||
root[MQTT_MODE] =
|
||||
NumberMqttModeStrings::get_progmem_str(static_cast<uint8_t>(mode), static_cast<uint8_t>(NUMBER_MODE_BOX));
|
||||
}
|
||||
const auto device_class = this->number_->traits.get_device_class_ref();
|
||||
const auto device_class = this->number_->get_device_class_ref();
|
||||
if (!device_class.empty()) {
|
||||
root[MQTT_DEVICE_CLASS] = device_class;
|
||||
}
|
||||
|
||||
@@ -79,7 +79,12 @@ from esphome.const import (
|
||||
DEVICE_CLASS_WIND_SPEED,
|
||||
)
|
||||
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,
|
||||
setup_unit_of_measurement,
|
||||
)
|
||||
from esphome.cpp_generator import MockObjClass
|
||||
|
||||
CODEOWNERS = ["@esphome/core"]
|
||||
@@ -257,11 +262,10 @@ async def _build_number_automations(var, config):
|
||||
await automation.build_automation(trigger, [(float, "x")], conf)
|
||||
|
||||
|
||||
@setup_entity("number")
|
||||
async def setup_number_core_(
|
||||
var, config, *, min_value: float, max_value: float, step: float
|
||||
):
|
||||
await setup_entity(var, config, "number")
|
||||
|
||||
cg.add(var.traits.set_min_value(min_value))
|
||||
cg.add(var.traits.set_max_value(max_value))
|
||||
cg.add(var.traits.set_step(step))
|
||||
@@ -273,10 +277,8 @@ async def setup_number_core_(
|
||||
|
||||
CORE.add_job(_build_number_automations, var, config)
|
||||
|
||||
if (unit_of_measurement := config.get(CONF_UNIT_OF_MEASUREMENT)) is not None:
|
||||
cg.add(var.traits.set_unit_of_measurement(unit_of_measurement))
|
||||
if (device_class := config.get(CONF_DEVICE_CLASS)) is not None:
|
||||
cg.add(var.traits.set_device_class(device_class))
|
||||
setup_device_class(config)
|
||||
setup_unit_of_measurement(config)
|
||||
|
||||
if (mqtt_id := config.get(CONF_MQTT_ID)) is not None:
|
||||
mqtt_ = cg.new_Pvariable(mqtt_id, var)
|
||||
|
||||
@@ -15,8 +15,8 @@ void log_number(const char *tag, const char *prefix, const char *type, Number *o
|
||||
|
||||
ESP_LOGCONFIG(tag, "%s%s '%s'", prefix, type, obj->get_name().c_str());
|
||||
LOG_ENTITY_ICON(tag, prefix, *obj);
|
||||
LOG_ENTITY_UNIT_OF_MEASUREMENT(tag, prefix, obj->traits);
|
||||
LOG_ENTITY_DEVICE_CLASS(tag, prefix, obj->traits);
|
||||
LOG_ENTITY_UNIT_OF_MEASUREMENT(tag, prefix, *obj);
|
||||
LOG_ENTITY_DEVICE_CLASS(tag, prefix, *obj);
|
||||
}
|
||||
|
||||
void Number::publish_state(float state) {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
#pragma once
|
||||
|
||||
#include "esphome/core/entity_base.h"
|
||||
#include "esphome/core/helpers.h"
|
||||
#include <cmath>
|
||||
#include <cstdint>
|
||||
|
||||
namespace esphome::number {
|
||||
|
||||
@@ -11,7 +11,7 @@ enum NumberMode : uint8_t {
|
||||
NUMBER_MODE_SLIDER = 2,
|
||||
};
|
||||
|
||||
class NumberTraits : public EntityBase_DeviceClass, public EntityBase_UnitOfMeasurement {
|
||||
class NumberTraits {
|
||||
public:
|
||||
// Set/get the number value boundaries.
|
||||
void set_min_value(float min_value) { min_value_ = min_value; }
|
||||
|
||||
@@ -4,13 +4,20 @@ from esphome.components.esp32 import (
|
||||
VARIANT_ESP32C6,
|
||||
VARIANT_ESP32H2,
|
||||
add_idf_sdkconfig_option,
|
||||
get_esp32_variant,
|
||||
include_builtin_idf_component,
|
||||
only_on_variant,
|
||||
require_vfs_select,
|
||||
)
|
||||
from esphome.components.mdns import MDNSComponent, enable_mdns_storage
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import CONF_CHANNEL, CONF_ENABLE_IPV6, CONF_ID, CONF_USE_ADDRESS
|
||||
from esphome.const import (
|
||||
CONF_CHANNEL,
|
||||
CONF_ENABLE_IPV6,
|
||||
CONF_ID,
|
||||
CONF_OUTPUT_POWER,
|
||||
CONF_USE_ADDRESS,
|
||||
)
|
||||
from esphome.core import CORE, TimePeriodMilliseconds
|
||||
import esphome.final_validate as fv
|
||||
from esphome.types import ConfigType
|
||||
@@ -45,6 +52,20 @@ CONF_DEVICE_TYPES = [
|
||||
]
|
||||
|
||||
|
||||
def _validate_txpower(value):
|
||||
if CORE.is_esp32:
|
||||
variant = get_esp32_variant()
|
||||
|
||||
# HW limits: Datasheet section "802.15.4 RF Transmitter (TX) Characteristics"
|
||||
# Further regulatory/soft limit may apply, e.g. by region
|
||||
if variant in (VARIANT_ESP32C6, VARIANT_ESP32C5):
|
||||
return cv.int_range(min=-15, max=20)(value)
|
||||
if variant == VARIANT_ESP32H2:
|
||||
return cv.int_range(min=-24, max=20)(value)
|
||||
|
||||
return value # Unsupported, fail later with clear error
|
||||
|
||||
|
||||
def set_sdkconfig_options(config):
|
||||
# and expose options for using SPI/UART RCPs
|
||||
add_idf_sdkconfig_option("CONFIG_IEEE802154_ENABLED", True)
|
||||
@@ -155,6 +176,10 @@ CONFIG_SCHEMA = cv.All(
|
||||
cv.Optional(CONF_TLV): cv.string_strict,
|
||||
cv.Optional(CONF_USE_ADDRESS): cv.string_strict,
|
||||
cv.Optional(CONF_POLL_PERIOD): cv.positive_time_period_milliseconds,
|
||||
cv.Optional(CONF_OUTPUT_POWER): cv.All(
|
||||
cv.decibel,
|
||||
_validate_txpower,
|
||||
),
|
||||
}
|
||||
).extend(_CONNECTION_SCHEMA),
|
||||
cv.has_exactly_one_key(CONF_NETWORK_KEY, CONF_TLV),
|
||||
@@ -197,4 +222,7 @@ async def to_code(config):
|
||||
cg.add(srp.set_mdns(mdns_component))
|
||||
await cg.register_component(srp, config)
|
||||
|
||||
if (output_power := config.get(CONF_OUTPUT_POWER)) is not None:
|
||||
cg.add(ot.set_output_power(output_power))
|
||||
|
||||
set_sdkconfig_options(config)
|
||||
|
||||
@@ -43,6 +43,9 @@ void OpenThreadComponent::dump_config() {
|
||||
ESP_LOGCONFIG(TAG, " Device is configured as Minimal End Device (MED)");
|
||||
}
|
||||
#endif
|
||||
if (this->output_power_.has_value()) {
|
||||
ESP_LOGCONFIG(TAG, " Output power: %" PRId8 "dBm", *this->output_power_);
|
||||
}
|
||||
}
|
||||
|
||||
bool OpenThreadComponent::is_connected() {
|
||||
|
||||
@@ -38,6 +38,7 @@ class OpenThreadComponent : public Component {
|
||||
#if CONFIG_OPENTHREAD_MTD
|
||||
void set_poll_period(uint32_t poll_period) { this->poll_period_ = poll_period; }
|
||||
#endif
|
||||
void set_output_power(int8_t output_power) { this->output_power_ = output_power; }
|
||||
|
||||
protected:
|
||||
std::optional<otIp6Address> get_omr_address_(InstanceLock &lock);
|
||||
@@ -45,6 +46,7 @@ class OpenThreadComponent : public Component {
|
||||
#if CONFIG_OPENTHREAD_MTD
|
||||
uint32_t poll_period_{0};
|
||||
#endif
|
||||
std::optional<int8_t> output_power_{};
|
||||
bool teardown_started_{false};
|
||||
bool teardown_complete_{false};
|
||||
|
||||
|
||||
@@ -135,6 +135,12 @@ void OpenThreadComponent::ot_main() {
|
||||
TRUEFALSE(link_mode_config.mRxOnWhenIdle));
|
||||
#endif
|
||||
|
||||
if (this->output_power_.has_value()) {
|
||||
if (const auto err = otPlatRadioSetTransmitPower(instance, *this->output_power_); err != OT_ERROR_NONE) {
|
||||
ESP_LOGE(TAG, "Failed to set power: %s", otThreadErrorToString(err));
|
||||
}
|
||||
}
|
||||
|
||||
// Run the main loop
|
||||
#if CONFIG_OPENTHREAD_CLI
|
||||
esp_openthread_cli_create_task();
|
||||
|
||||
@@ -11,8 +11,8 @@
|
||||
namespace esphome {
|
||||
|
||||
void HOT yield() { ::yield(); }
|
||||
uint64_t millis_64() { return time_us_64() / 1000ULL; }
|
||||
uint32_t HOT millis() { return static_cast<uint32_t>(millis_64()); }
|
||||
uint64_t millis_64() { return micros_to_millis<uint64_t>(time_us_64()); }
|
||||
uint32_t HOT millis() { return micros_to_millis(time_us_64()); }
|
||||
void HOT delay(uint32_t ms) { ::delay(ms); }
|
||||
uint32_t HOT micros() { return ::micros(); }
|
||||
void HOT delayMicroseconds(uint32_t us) { delay_microseconds_safe(us); }
|
||||
@@ -34,6 +34,9 @@ void HOT arch_feed_wdt() { watchdog_update(); }
|
||||
uint8_t progmem_read_byte(const uint8_t *addr) {
|
||||
return pgm_read_byte(addr); // NOLINT
|
||||
}
|
||||
const char *progmem_read_ptr(const char *const *addr) {
|
||||
return reinterpret_cast<const char *>(pgm_read_ptr(addr)); // NOLINT
|
||||
}
|
||||
uint16_t progmem_read_uint16(const uint16_t *addr) { return *addr; }
|
||||
uint32_t HOT arch_get_cpu_cycle_count() { return ulMainGetRunTimeCounterValue(); }
|
||||
uint32_t arch_get_cpu_freq_hz() { return RP2040::f_cpu(); }
|
||||
|
||||
@@ -17,7 +17,7 @@ import esphome.final_validate as fv
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
CODEOWNERS = ["@glmnet"]
|
||||
CODEOWNERS = ["@glmnet", "@ximex"]
|
||||
CONF_RTTTL = "rtttl"
|
||||
CONF_ON_FINISHED_PLAYBACK = "on_finished_playback"
|
||||
|
||||
|
||||
@@ -92,9 +92,8 @@ def select_schema(
|
||||
return _SELECT_SCHEMA.extend(schema)
|
||||
|
||||
|
||||
@setup_entity("select")
|
||||
async def setup_select_core_(var, config, *, options: list[str]):
|
||||
await setup_entity(var, config, "select")
|
||||
|
||||
cg.add(var.traits.set_options(options))
|
||||
|
||||
for conf in config.get(CONF_ON_VALUE, []):
|
||||
|
||||
@@ -106,7 +106,12 @@ from esphome.const import (
|
||||
ENTITY_CATEGORY_CONFIG,
|
||||
)
|
||||
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,
|
||||
setup_unit_of_measurement,
|
||||
)
|
||||
from esphome.cpp_generator import MockObj, MockObjClass
|
||||
from esphome.util import Registry
|
||||
|
||||
@@ -908,15 +913,12 @@ async def _build_sensor_automations(var, config):
|
||||
await automation.build_automation(trigger, [(float, "x")], conf)
|
||||
|
||||
|
||||
@setup_entity("sensor")
|
||||
async def setup_sensor_core_(var, config):
|
||||
await setup_entity(var, config, "sensor")
|
||||
|
||||
if (device_class := config.get(CONF_DEVICE_CLASS)) is not None:
|
||||
cg.add(var.set_device_class(device_class))
|
||||
setup_device_class(config)
|
||||
setup_unit_of_measurement(config)
|
||||
if (state_class := config.get(CONF_STATE_CLASS)) is not None:
|
||||
cg.add(var.set_state_class(state_class))
|
||||
if (unit_of_measurement := config.get(CONF_UNIT_OF_MEASUREMENT)) is not None:
|
||||
cg.add(var.set_unit_of_measurement(unit_of_measurement))
|
||||
if (accuracy_decimals := config.get(CONF_ACCURACY_DECIMALS)) is not None:
|
||||
cg.add(var.set_accuracy_decimals(accuracy_decimals))
|
||||
# Only set force_update if True (default is False)
|
||||
|
||||
@@ -44,7 +44,7 @@ const LogString *state_class_to_string(StateClass state_class);
|
||||
*
|
||||
* A sensor has unit of measurement and can use publish_state to send out a new value with the specified accuracy.
|
||||
*/
|
||||
class Sensor : public EntityBase, public EntityBase_DeviceClass, public EntityBase_UnitOfMeasurement {
|
||||
class Sensor : public EntityBase {
|
||||
public:
|
||||
explicit Sensor();
|
||||
|
||||
|
||||
@@ -11,11 +11,15 @@ namespace esphome::socket {
|
||||
|
||||
BSDSocketImpl::BSDSocketImpl(int fd, bool monitor_loop) {
|
||||
this->fd_ = fd;
|
||||
// Register new socket with the application for select() if monitoring requested
|
||||
if (monitor_loop && this->fd_ >= 0) {
|
||||
// Only set loop_monitored_ to true if registration succeeds
|
||||
this->loop_monitored_ = App.register_socket_fd(this->fd_);
|
||||
}
|
||||
if (!monitor_loop || this->fd_ < 0)
|
||||
return;
|
||||
#ifdef USE_LWIP_FAST_SELECT
|
||||
// Cache lwip_sock pointer and register for monitoring (hooks callback internally)
|
||||
this->cached_sock_ = esphome_lwip_get_sock(this->fd_);
|
||||
this->loop_monitored_ = App.register_socket(this->cached_sock_);
|
||||
#else
|
||||
this->loop_monitored_ = App.register_socket_fd(this->fd_);
|
||||
#endif
|
||||
}
|
||||
|
||||
BSDSocketImpl::~BSDSocketImpl() {
|
||||
@@ -26,10 +30,17 @@ BSDSocketImpl::~BSDSocketImpl() {
|
||||
|
||||
int BSDSocketImpl::close() {
|
||||
if (!this->closed_) {
|
||||
// Unregister from select() before closing if monitored
|
||||
// Unregister before closing to avoid dangling pointer in monitored set
|
||||
#ifdef USE_LWIP_FAST_SELECT
|
||||
if (this->loop_monitored_) {
|
||||
App.unregister_socket(this->cached_sock_);
|
||||
this->cached_sock_ = nullptr;
|
||||
}
|
||||
#else
|
||||
if (this->loop_monitored_) {
|
||||
App.unregister_socket_fd(this->fd_);
|
||||
}
|
||||
#endif
|
||||
int ret = ::close(this->fd_);
|
||||
this->closed_ = true;
|
||||
return ret;
|
||||
@@ -48,8 +59,6 @@ int BSDSocketImpl::setblocking(bool blocking) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
bool BSDSocketImpl::ready() const { return socket_ready_fd(this->fd_, this->loop_monitored_); }
|
||||
|
||||
size_t BSDSocketImpl::getpeername_to(std::span<char, SOCKADDR_STR_LEN> buf) {
|
||||
struct sockaddr_storage storage;
|
||||
socklen_t len = sizeof(storage);
|
||||
@@ -86,14 +95,6 @@ std::unique_ptr<Socket> socket_loop_monitored(int domain, int type, int protocol
|
||||
return create_socket(domain, type, protocol, true);
|
||||
}
|
||||
|
||||
std::unique_ptr<ListenSocket> socket_listen(int domain, int type, int protocol) {
|
||||
return create_socket(domain, type, protocol, false);
|
||||
}
|
||||
|
||||
std::unique_ptr<ListenSocket> socket_listen_loop_monitored(int domain, int type, int protocol) {
|
||||
return create_socket(domain, type, protocol, true);
|
||||
}
|
||||
|
||||
} // namespace esphome::socket
|
||||
|
||||
#endif // USE_SOCKET_IMPL_BSD_SOCKETS
|
||||
|
||||
@@ -13,6 +13,10 @@
|
||||
#include <lwip/sockets.h>
|
||||
#endif
|
||||
|
||||
#ifdef USE_LWIP_FAST_SELECT
|
||||
struct lwip_sock;
|
||||
#endif
|
||||
|
||||
namespace esphome::socket {
|
||||
|
||||
class BSDSocketImpl {
|
||||
@@ -105,6 +109,9 @@ class BSDSocketImpl {
|
||||
|
||||
protected:
|
||||
int fd_{-1};
|
||||
#ifdef USE_LWIP_FAST_SELECT
|
||||
struct lwip_sock *cached_sock_{nullptr}; // Cached for direct rcvevent read in ready()
|
||||
#endif
|
||||
bool closed_{false};
|
||||
bool loop_monitored_{false};
|
||||
};
|
||||
|
||||
@@ -36,7 +36,7 @@ void socket_delay(uint32_t ms) {
|
||||
esp_delay(ms, []() { return !s_socket_woke; });
|
||||
}
|
||||
|
||||
void socket_wake() {
|
||||
void IRAM_ATTR socket_wake() {
|
||||
s_socket_woke = true;
|
||||
esp_schedule();
|
||||
}
|
||||
|
||||
@@ -11,11 +11,15 @@ namespace esphome::socket {
|
||||
|
||||
LwIPSocketImpl::LwIPSocketImpl(int fd, bool monitor_loop) {
|
||||
this->fd_ = fd;
|
||||
// Register new socket with the application for select() if monitoring requested
|
||||
if (monitor_loop && this->fd_ >= 0) {
|
||||
// Only set loop_monitored_ to true if registration succeeds
|
||||
this->loop_monitored_ = App.register_socket_fd(this->fd_);
|
||||
}
|
||||
if (!monitor_loop || this->fd_ < 0)
|
||||
return;
|
||||
#ifdef USE_LWIP_FAST_SELECT
|
||||
// Cache lwip_sock pointer and register for monitoring (hooks callback internally)
|
||||
this->cached_sock_ = esphome_lwip_get_sock(this->fd_);
|
||||
this->loop_monitored_ = App.register_socket(this->cached_sock_);
|
||||
#else
|
||||
this->loop_monitored_ = App.register_socket_fd(this->fd_);
|
||||
#endif
|
||||
}
|
||||
|
||||
LwIPSocketImpl::~LwIPSocketImpl() {
|
||||
@@ -26,10 +30,17 @@ LwIPSocketImpl::~LwIPSocketImpl() {
|
||||
|
||||
int LwIPSocketImpl::close() {
|
||||
if (!this->closed_) {
|
||||
// Unregister from select() before closing if monitored
|
||||
// Unregister before closing to avoid dangling pointer in monitored set
|
||||
#ifdef USE_LWIP_FAST_SELECT
|
||||
if (this->loop_monitored_) {
|
||||
App.unregister_socket(this->cached_sock_);
|
||||
this->cached_sock_ = nullptr;
|
||||
}
|
||||
#else
|
||||
if (this->loop_monitored_) {
|
||||
App.unregister_socket_fd(this->fd_);
|
||||
}
|
||||
#endif
|
||||
int ret = lwip_close(this->fd_);
|
||||
this->closed_ = true;
|
||||
return ret;
|
||||
@@ -48,8 +59,6 @@ int LwIPSocketImpl::setblocking(bool blocking) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
bool LwIPSocketImpl::ready() const { return socket_ready_fd(this->fd_, this->loop_monitored_); }
|
||||
|
||||
size_t LwIPSocketImpl::getpeername_to(std::span<char, SOCKADDR_STR_LEN> buf) {
|
||||
struct sockaddr_storage storage;
|
||||
socklen_t len = sizeof(storage);
|
||||
@@ -86,14 +95,6 @@ std::unique_ptr<Socket> socket_loop_monitored(int domain, int type, int protocol
|
||||
return create_socket(domain, type, protocol, true);
|
||||
}
|
||||
|
||||
std::unique_ptr<ListenSocket> socket_listen(int domain, int type, int protocol) {
|
||||
return create_socket(domain, type, protocol, false);
|
||||
}
|
||||
|
||||
std::unique_ptr<ListenSocket> socket_listen_loop_monitored(int domain, int type, int protocol) {
|
||||
return create_socket(domain, type, protocol, true);
|
||||
}
|
||||
|
||||
} // namespace esphome::socket
|
||||
|
||||
#endif // USE_SOCKET_IMPL_LWIP_SOCKETS
|
||||
|
||||
@@ -9,6 +9,10 @@
|
||||
#include "esphome/core/helpers.h"
|
||||
#include "headers.h"
|
||||
|
||||
#ifdef USE_LWIP_FAST_SELECT
|
||||
struct lwip_sock;
|
||||
#endif
|
||||
|
||||
namespace esphome::socket {
|
||||
|
||||
class LwIPSocketImpl {
|
||||
@@ -71,6 +75,9 @@ class LwIPSocketImpl {
|
||||
|
||||
protected:
|
||||
int fd_{-1};
|
||||
#ifdef USE_LWIP_FAST_SELECT
|
||||
struct lwip_sock *cached_sock_{nullptr}; // Cached for direct rcvevent read in ready()
|
||||
#endif
|
||||
bool closed_{false};
|
||||
bool loop_monitored_{false};
|
||||
};
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
|
||||
namespace esphome::socket {
|
||||
|
||||
#ifdef USE_SOCKET_SELECT_SUPPORT
|
||||
#if defined(USE_SOCKET_SELECT_SUPPORT) && !defined(USE_LWIP_FAST_SELECT)
|
||||
// Shared ready() implementation for fd-based socket implementations (BSD and LWIP sockets).
|
||||
// Checks if the Application's select() loop has marked this fd as ready.
|
||||
bool socket_ready_fd(int fd, bool loop_monitored) { return !loop_monitored || App.is_socket_ready_(fd); }
|
||||
@@ -89,6 +89,9 @@ std::unique_ptr<Socket> socket_ip(int type, int protocol) {
|
||||
#endif /* USE_NETWORK_IPV6 */
|
||||
}
|
||||
|
||||
#ifdef USE_SOCKET_IMPL_LWIP_TCP
|
||||
// LWIP_TCP has separate Socket/ListenSocket types — needs out-of-line factory.
|
||||
// BSD and LWIP_SOCKETS define this inline in socket.h.
|
||||
std::unique_ptr<ListenSocket> socket_ip_loop_monitored(int type, int protocol) {
|
||||
#if USE_NETWORK_IPV6
|
||||
return socket_listen_loop_monitored(AF_INET6, type, protocol);
|
||||
@@ -96,6 +99,7 @@ std::unique_ptr<ListenSocket> socket_ip_loop_monitored(int type, int protocol) {
|
||||
return socket_listen_loop_monitored(AF_INET, type, protocol);
|
||||
#endif /* USE_NETWORK_IPV6 */
|
||||
}
|
||||
#endif
|
||||
|
||||
socklen_t set_sockaddr(struct sockaddr *addr, socklen_t addrlen, const char *ip_address, uint16_t port) {
|
||||
#if USE_NETWORK_IPV6
|
||||
|
||||
@@ -6,6 +6,10 @@
|
||||
#include "esphome/core/optional.h"
|
||||
#include "headers.h"
|
||||
|
||||
#ifdef USE_LWIP_FAST_SELECT
|
||||
#include "esphome/core/lwip_fast_select.h"
|
||||
#endif
|
||||
|
||||
#if defined(USE_SOCKET_IMPL_LWIP_TCP) || defined(USE_SOCKET_IMPL_LWIP_SOCKETS) || defined(USE_SOCKET_IMPL_BSD_SOCKETS)
|
||||
|
||||
// Include only the active implementation's header.
|
||||
@@ -36,12 +40,29 @@ using Socket = LWIPRawImpl;
|
||||
using ListenSocket = LWIPRawListenImpl;
|
||||
#endif
|
||||
|
||||
#ifdef USE_SOCKET_SELECT_SUPPORT
|
||||
#ifdef USE_LWIP_FAST_SELECT
|
||||
/// Shared ready() helper using cached lwip_sock pointer for direct rcvevent read.
|
||||
inline bool socket_ready(struct lwip_sock *cached_sock, bool loop_monitored) {
|
||||
return !loop_monitored || (cached_sock != nullptr && esphome_lwip_socket_has_data(cached_sock));
|
||||
}
|
||||
#elif defined(USE_SOCKET_SELECT_SUPPORT)
|
||||
/// Shared ready() helper for fd-based socket implementations.
|
||||
/// Checks if the Application's select() loop has marked this fd as ready.
|
||||
bool socket_ready_fd(int fd, bool loop_monitored);
|
||||
#endif
|
||||
|
||||
// Inline ready() — defined here because it depends on socket_ready/socket_ready_fd
|
||||
// declared above, while the impl headers are included before those declarations.
|
||||
#if defined(USE_SOCKET_IMPL_BSD_SOCKETS) || defined(USE_SOCKET_IMPL_LWIP_SOCKETS)
|
||||
inline bool Socket::ready() const {
|
||||
#ifdef USE_LWIP_FAST_SELECT
|
||||
return socket_ready(this->cached_sock_, this->loop_monitored_);
|
||||
#else
|
||||
return socket_ready_fd(this->fd_, this->loop_monitored_);
|
||||
#endif
|
||||
}
|
||||
#endif
|
||||
|
||||
/// Create a socket of the given domain, type and protocol.
|
||||
std::unique_ptr<Socket> socket(int domain, int type, int protocol);
|
||||
/// Create a socket in the newest available IP domain (IPv6 or IPv4) of the given type and protocol.
|
||||
@@ -56,11 +77,29 @@ std::unique_ptr<Socket> socket_ip(int type, int protocol);
|
||||
std::unique_ptr<Socket> socket_loop_monitored(int domain, int type, int protocol);
|
||||
|
||||
/// Create a listening socket of the given domain, type and protocol.
|
||||
std::unique_ptr<ListenSocket> socket_listen(int domain, int type, int protocol);
|
||||
/// Create a listening socket and monitor it for data in the main loop.
|
||||
std::unique_ptr<ListenSocket> socket_listen_loop_monitored(int domain, int type, int protocol);
|
||||
/// Create a listening socket in the newest available IP domain and monitor it.
|
||||
#ifdef USE_SOCKET_IMPL_LWIP_TCP
|
||||
// LWIP_TCP has separate Socket/ListenSocket types — needs distinct factory functions.
|
||||
std::unique_ptr<ListenSocket> socket_listen(int domain, int type, int protocol);
|
||||
std::unique_ptr<ListenSocket> socket_listen_loop_monitored(int domain, int type, int protocol);
|
||||
std::unique_ptr<ListenSocket> socket_ip_loop_monitored(int type, int protocol);
|
||||
#else
|
||||
// BSD and LWIP_SOCKETS: Socket == ListenSocket, so listen variants just delegate.
|
||||
inline std::unique_ptr<ListenSocket> socket_listen(int domain, int type, int protocol) {
|
||||
return socket(domain, type, protocol);
|
||||
}
|
||||
inline std::unique_ptr<ListenSocket> socket_listen_loop_monitored(int domain, int type, int protocol) {
|
||||
return socket_loop_monitored(domain, type, protocol);
|
||||
}
|
||||
inline std::unique_ptr<ListenSocket> socket_ip_loop_monitored(int type, int protocol) {
|
||||
#if USE_NETWORK_IPV6
|
||||
return socket_loop_monitored(AF_INET6, type, protocol);
|
||||
#else
|
||||
return socket_loop_monitored(AF_INET, type, protocol);
|
||||
#endif
|
||||
}
|
||||
#endif
|
||||
|
||||
/// Set a sockaddr to the specified address and port for the IP version used by socket_ip().
|
||||
/// @param addr Destination sockaddr structure
|
||||
@@ -86,8 +125,9 @@ size_t format_sockaddr_to(const struct sockaddr *addr_ptr, socklen_t len, std::s
|
||||
/// On ESP8266, lwip callbacks set a flag and call esp_schedule() to wake the delay.
|
||||
void socket_delay(uint32_t ms);
|
||||
|
||||
/// Called by lwip callbacks to signal socket activity and wake delay.
|
||||
void socket_wake();
|
||||
/// Signal socket/IO activity and wake the main loop from esp_delay() early.
|
||||
/// ISR-safe: uses IRAM_ATTR internally and only sets a volatile flag + esp_schedule().
|
||||
void socket_wake(); // NOLINT(readability-redundant-declaration)
|
||||
#endif
|
||||
|
||||
} // namespace esphome::socket
|
||||
|
||||
@@ -15,6 +15,8 @@ from esphome.const import (
|
||||
CONF_FORMAT,
|
||||
CONF_ID,
|
||||
CONF_NUM_CHANNELS,
|
||||
CONF_ON_TURN_OFF,
|
||||
CONF_ON_TURN_ON,
|
||||
CONF_PATH,
|
||||
CONF_RAW_DATA_ID,
|
||||
CONF_SAMPLE_RATE,
|
||||
@@ -401,6 +403,9 @@ FINAL_VALIDATE_SCHEMA = cv.All(
|
||||
|
||||
|
||||
async def to_code(config):
|
||||
if CONF_ON_TURN_OFF in config or CONF_ON_TURN_ON in config:
|
||||
cg.add_define("USE_SPEAKER_MEDIA_PLAYER_ON_OFF", True)
|
||||
|
||||
var = await media_player.new_media_player(config)
|
||||
await cg.register_component(var, config)
|
||||
|
||||
|
||||
@@ -51,7 +51,11 @@ static const UBaseType_t ANNOUNCEMENT_PIPELINE_TASK_PRIORITY = 1;
|
||||
static const char *const TAG = "speaker_media_player";
|
||||
|
||||
void SpeakerMediaPlayer::setup() {
|
||||
#ifdef USE_SPEAKER_MEDIA_PLAYER_ON_OFF
|
||||
state = media_player::MEDIA_PLAYER_STATE_OFF;
|
||||
#else
|
||||
state = media_player::MEDIA_PLAYER_STATE_IDLE;
|
||||
#endif
|
||||
|
||||
this->media_control_command_queue_ = xQueueCreate(MEDIA_CONTROLS_QUEUE_LENGTH, sizeof(MediaCallCommand));
|
||||
|
||||
@@ -128,6 +132,12 @@ void SpeakerMediaPlayer::watch_media_commands_() {
|
||||
bool enqueue = media_command.enqueue.has_value() && media_command.enqueue.value();
|
||||
|
||||
if (media_command.url.has_value() || media_command.file.has_value()) {
|
||||
#ifdef USE_SPEAKER_MEDIA_PLAYER_ON_OFF
|
||||
if (this->state == media_player::MEDIA_PLAYER_STATE_OFF) {
|
||||
this->state = media_player::MEDIA_PLAYER_STATE_ON;
|
||||
publish_state();
|
||||
}
|
||||
#endif
|
||||
PlaylistItem playlist_item;
|
||||
if (media_command.url.has_value()) {
|
||||
playlist_item.url = *media_command.url.value();
|
||||
@@ -184,6 +194,12 @@ void SpeakerMediaPlayer::watch_media_commands_() {
|
||||
if (media_command.command.has_value()) {
|
||||
switch (media_command.command.value()) {
|
||||
case media_player::MEDIA_PLAYER_COMMAND_PLAY:
|
||||
#ifdef USE_SPEAKER_MEDIA_PLAYER_ON_OFF
|
||||
if (this->state == media_player::MEDIA_PLAYER_STATE_OFF) {
|
||||
this->state = media_player::MEDIA_PLAYER_STATE_ON;
|
||||
publish_state();
|
||||
}
|
||||
#endif
|
||||
if ((this->media_pipeline_ != nullptr) && (this->is_paused_)) {
|
||||
this->media_pipeline_->set_pause_state(false);
|
||||
}
|
||||
@@ -195,10 +211,26 @@ void SpeakerMediaPlayer::watch_media_commands_() {
|
||||
}
|
||||
this->is_paused_ = true;
|
||||
break;
|
||||
#ifdef USE_SPEAKER_MEDIA_PLAYER_ON_OFF
|
||||
case media_player::MEDIA_PLAYER_COMMAND_TURN_ON:
|
||||
if (this->state == media_player::MEDIA_PLAYER_STATE_OFF) {
|
||||
this->state = media_player::MEDIA_PLAYER_STATE_ON;
|
||||
this->publish_state();
|
||||
}
|
||||
break;
|
||||
case media_player::MEDIA_PLAYER_COMMAND_TURN_OFF:
|
||||
this->is_turn_off_ = true;
|
||||
// Intentional Fall-through
|
||||
#endif
|
||||
case media_player::MEDIA_PLAYER_COMMAND_STOP:
|
||||
// Pipelines do not stop immediately after calling the stop command, so confirm its stopped before unpausing.
|
||||
// This avoids an audible short segment playing after receiving the stop command in a paused state.
|
||||
#ifdef USE_SPEAKER_MEDIA_PLAYER_ON_OFF
|
||||
if (this->single_pipeline_() || (media_command.announce.has_value() && media_command.announce.value()) ||
|
||||
(this->is_turn_off_ && this->announcement_pipeline_state_ != AudioPipelineState::STOPPED)) {
|
||||
#else
|
||||
if (this->single_pipeline_() || (media_command.announce.has_value() && media_command.announce.value())) {
|
||||
#endif
|
||||
if (this->announcement_pipeline_ != nullptr) {
|
||||
this->cancel_timeout("next_ann");
|
||||
this->announcement_playlist_.clear();
|
||||
@@ -366,7 +398,13 @@ void SpeakerMediaPlayer::loop() {
|
||||
}
|
||||
} else {
|
||||
if (this->is_paused_) {
|
||||
#ifdef USE_SPEAKER_MEDIA_PLAYER_ON_OFF
|
||||
if (this->state != media_player::MEDIA_PLAYER_STATE_OFF) {
|
||||
this->state = media_player::MEDIA_PLAYER_STATE_PAUSED;
|
||||
}
|
||||
#else
|
||||
this->state = media_player::MEDIA_PLAYER_STATE_PAUSED;
|
||||
#endif
|
||||
} else if (this->media_pipeline_state_ == AudioPipelineState::PLAYING) {
|
||||
this->state = media_player::MEDIA_PLAYER_STATE_PLAYING;
|
||||
} else if (this->media_pipeline_state_ == AudioPipelineState::STOPPED) {
|
||||
@@ -399,7 +437,13 @@ void SpeakerMediaPlayer::loop() {
|
||||
}
|
||||
}
|
||||
} else {
|
||||
#ifdef USE_SPEAKER_MEDIA_PLAYER_ON_OFF
|
||||
if (this->state != media_player::MEDIA_PLAYER_STATE_OFF) {
|
||||
this->state = media_player::MEDIA_PLAYER_STATE_IDLE;
|
||||
}
|
||||
#else
|
||||
this->state = media_player::MEDIA_PLAYER_STATE_IDLE;
|
||||
#endif
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -409,6 +453,20 @@ void SpeakerMediaPlayer::loop() {
|
||||
this->publish_state();
|
||||
ESP_LOGD(TAG, "State changed to %s", media_player::media_player_state_to_string(this->state));
|
||||
}
|
||||
#ifdef USE_SPEAKER_MEDIA_PLAYER_ON_OFF
|
||||
if (this->is_turn_off_ && (this->state == media_player::MEDIA_PLAYER_STATE_PAUSED ||
|
||||
this->state == media_player::MEDIA_PLAYER_STATE_IDLE)) {
|
||||
this->is_turn_off_ = false;
|
||||
if (this->state == media_player::MEDIA_PLAYER_STATE_PAUSED) {
|
||||
this->state = media_player::MEDIA_PLAYER_STATE_IDLE;
|
||||
this->publish_state();
|
||||
ESP_LOGD(TAG, "State changed to %s", media_player::media_player_state_to_string(this->state));
|
||||
}
|
||||
this->state = media_player::MEDIA_PLAYER_STATE_OFF;
|
||||
this->publish_state();
|
||||
ESP_LOGD(TAG, "State changed to %s", media_player::media_player_state_to_string(this->state));
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
void SpeakerMediaPlayer::play_file(audio::AudioFile *media_file, bool announcement, bool enqueue) {
|
||||
@@ -481,6 +539,9 @@ media_player::MediaPlayerTraits SpeakerMediaPlayer::get_traits() {
|
||||
if (!this->single_pipeline_()) {
|
||||
traits.set_supports_pause(true);
|
||||
}
|
||||
#ifdef USE_SPEAKER_MEDIA_PLAYER_ON_OFF
|
||||
traits.set_supports_turn_off_on(true);
|
||||
#endif
|
||||
|
||||
if (this->announcement_format_.has_value()) {
|
||||
traits.get_supported_formats().push_back(this->announcement_format_.value());
|
||||
|
||||
@@ -144,6 +144,9 @@ class SpeakerMediaPlayer : public Component,
|
||||
|
||||
bool is_paused_{false};
|
||||
bool is_muted_{false};
|
||||
#ifdef USE_SPEAKER_MEDIA_PLAYER_ON_OFF
|
||||
bool is_turn_off_{false};
|
||||
#endif
|
||||
uint8_t unpause_media_remaining_{0};
|
||||
uint8_t unpause_announcement_remaining_{0};
|
||||
|
||||
|
||||
@@ -567,7 +567,7 @@ void Sprinkler::set_valve_run_duration(const optional<size_t> valve_number, cons
|
||||
return;
|
||||
}
|
||||
auto call = this->valve_[valve_number.value()].run_duration_number->make_call();
|
||||
if (this->valve_[valve_number.value()].run_duration_number->traits.get_unit_of_measurement_ref() == MIN_STR) {
|
||||
if (this->valve_[valve_number.value()].run_duration_number->get_unit_of_measurement_ref() == MIN_STR) {
|
||||
call.set_value(run_duration.value() / 60.0);
|
||||
} else {
|
||||
call.set_value(run_duration.value());
|
||||
@@ -649,7 +649,7 @@ uint32_t Sprinkler::valve_run_duration(const size_t valve_number) {
|
||||
return 0;
|
||||
}
|
||||
if (this->valve_[valve_number].run_duration_number != nullptr) {
|
||||
if (this->valve_[valve_number].run_duration_number->traits.get_unit_of_measurement_ref() == MIN_STR) {
|
||||
if (this->valve_[valve_number].run_duration_number->get_unit_of_measurement_ref() == MIN_STR) {
|
||||
return static_cast<uint32_t>(roundf(this->valve_[valve_number].run_duration_number->state * 60));
|
||||
} else {
|
||||
return static_cast<uint32_t>(roundf(this->valve_[valve_number].run_duration_number->state));
|
||||
|
||||
@@ -22,7 +22,11 @@ from esphome.const import (
|
||||
DEVICE_CLASS_SWITCH,
|
||||
)
|
||||
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
|
||||
|
||||
CODEOWNERS = ["@esphome/core"]
|
||||
@@ -154,9 +158,8 @@ async def _build_switch_automations(var, config):
|
||||
await automation.build_automation(trigger, [], conf)
|
||||
|
||||
|
||||
@setup_entity("switch")
|
||||
async def setup_switch_core_(var, config):
|
||||
await setup_entity(var, config, "switch")
|
||||
|
||||
if (inverted := config.get(CONF_INVERTED)) is not None:
|
||||
cg.add(var.set_inverted(inverted))
|
||||
|
||||
@@ -169,8 +172,7 @@ async def setup_switch_core_(var, config):
|
||||
if web_server_config := config.get(CONF_WEB_SERVER):
|
||||
await web_server.add_entity_config(var, web_server_config)
|
||||
|
||||
if (device_class := config.get(CONF_DEVICE_CLASS)) is not None:
|
||||
cg.add(var.set_device_class(device_class))
|
||||
setup_device_class(config)
|
||||
|
||||
cg.add(var.set_restore_mode(config[CONF_RESTORE_MODE]))
|
||||
await zigbee.setup_switch(var, config)
|
||||
|
||||
@@ -35,7 +35,7 @@ enum SwitchRestoreMode : uint8_t {
|
||||
* A switch is basically just a combination of a binary sensor (for reporting switch values)
|
||||
* and a write_state method that writes a state to the hardware.
|
||||
*/
|
||||
class Switch : public EntityBase, public EntityBase_DeviceClass {
|
||||
class Switch : public EntityBase {
|
||||
public:
|
||||
explicit Switch();
|
||||
|
||||
|
||||
@@ -84,6 +84,7 @@ def text_schema(
|
||||
return _TEXT_SCHEMA.extend(schema)
|
||||
|
||||
|
||||
@setup_entity("text")
|
||||
async def setup_text_core_(
|
||||
var,
|
||||
config,
|
||||
@@ -92,8 +93,6 @@ async def setup_text_core_(
|
||||
max_length: int | None,
|
||||
pattern: str | None,
|
||||
):
|
||||
await setup_entity(var, config, "text")
|
||||
|
||||
cg.add(var.traits.set_min_length(min_length))
|
||||
cg.add(var.traits.set_max_length(max_length))
|
||||
if pattern is not None:
|
||||
|
||||
@@ -21,7 +21,11 @@ from esphome.const import (
|
||||
DEVICE_CLASS_TIMESTAMP,
|
||||
)
|
||||
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.util import Registry
|
||||
|
||||
@@ -208,11 +212,9 @@ async def _build_text_sensor_automations(var, config):
|
||||
await automation.build_automation(trigger, [(cg.std_string, "x")], conf)
|
||||
|
||||
|
||||
@setup_entity("text_sensor")
|
||||
async def setup_text_sensor_core_(var, config):
|
||||
await setup_entity(var, config, "text_sensor")
|
||||
|
||||
if (device_class := config.get(CONF_DEVICE_CLASS)) is not None:
|
||||
cg.add(var.set_device_class(device_class))
|
||||
setup_device_class(config)
|
||||
|
||||
if config.get(CONF_FILTERS): # must exist and not be empty
|
||||
cg.add_define("USE_TEXT_SENSOR_FILTER")
|
||||
|
||||
@@ -25,7 +25,7 @@ void log_text_sensor(const char *tag, const char *prefix, const char *type, Text
|
||||
public: \
|
||||
void set_##name##_text_sensor(text_sensor::TextSensor *text_sensor) { this->name##_text_sensor_ = text_sensor; }
|
||||
|
||||
class TextSensor : public EntityBase, public EntityBase_DeviceClass {
|
||||
class TextSensor : public EntityBase {
|
||||
public:
|
||||
std::string state;
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@ import re
|
||||
|
||||
from esphome import automation, pins
|
||||
import esphome.codegen as cg
|
||||
from esphome.components.const import CONF_DATA_BITS, CONF_PARITY, CONF_STOP_BITS
|
||||
from esphome.config_helpers import filter_source_files_from_platform
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import (
|
||||
@@ -11,7 +12,6 @@ from esphome.const import (
|
||||
CONF_BAUD_RATE,
|
||||
CONF_BYTES,
|
||||
CONF_DATA,
|
||||
CONF_DATA_BITS,
|
||||
CONF_DEBUG,
|
||||
CONF_DELIMITER,
|
||||
CONF_DIRECTION,
|
||||
@@ -21,12 +21,10 @@ from esphome.const import (
|
||||
CONF_ID,
|
||||
CONF_LAMBDA,
|
||||
CONF_NUMBER,
|
||||
CONF_PARITY,
|
||||
CONF_PORT,
|
||||
CONF_RX_BUFFER_SIZE,
|
||||
CONF_RX_PIN,
|
||||
CONF_SEQUENCE,
|
||||
CONF_STOP_BITS,
|
||||
CONF_TIMEOUT,
|
||||
CONF_TRIGGER_ID,
|
||||
CONF_TX_PIN,
|
||||
|
||||
@@ -15,7 +15,11 @@ from esphome.const import (
|
||||
ENTITY_CATEGORY_CONFIG,
|
||||
)
|
||||
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
|
||||
|
||||
CODEOWNERS = ["@jesserockz"]
|
||||
@@ -87,11 +91,9 @@ def update_schema(
|
||||
return _UPDATE_SCHEMA.extend(schema)
|
||||
|
||||
|
||||
@setup_entity("update")
|
||||
async def setup_update_core_(var, config):
|
||||
await setup_entity(var, config, "update")
|
||||
|
||||
if device_class_config := config.get(CONF_DEVICE_CLASS):
|
||||
cg.add(var.set_device_class(device_class_config))
|
||||
setup_device_class(config)
|
||||
|
||||
if on_update_available := config.get(CONF_ON_UPDATE_AVAILABLE):
|
||||
await automation.build_automation(
|
||||
|
||||
@@ -29,7 +29,7 @@ enum UpdateState : uint8_t {
|
||||
|
||||
const LogString *update_state_to_string(UpdateState state);
|
||||
|
||||
class UpdateEntity : public EntityBase, public EntityBase_DeviceClass {
|
||||
class UpdateEntity : public EntityBase {
|
||||
public:
|
||||
void publish_state();
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import esphome.codegen as cg
|
||||
from esphome.components import socket
|
||||
from esphome.components.const import CONF_DATA_BITS, CONF_PARITY, CONF_STOP_BITS
|
||||
from esphome.components.uart import UARTComponent
|
||||
from esphome.components.usb_host import register_usb_client, usb_device_schema
|
||||
import esphome.config_validation as cv
|
||||
@@ -7,12 +8,9 @@ from esphome.const import (
|
||||
CONF_BAUD_RATE,
|
||||
CONF_BUFFER_SIZE,
|
||||
CONF_CHANNELS,
|
||||
CONF_DATA_BITS,
|
||||
CONF_DEBUG,
|
||||
CONF_DUMMY_RECEIVER,
|
||||
CONF_ID,
|
||||
CONF_PARITY,
|
||||
CONF_STOP_BITS,
|
||||
)
|
||||
from esphome.cpp_types import Component
|
||||
|
||||
|
||||
@@ -75,6 +75,15 @@ void USBUartTypeCH34X::enable_channels() {
|
||||
}
|
||||
this->start_channels();
|
||||
}
|
||||
|
||||
std::vector<CdcEps> USBUartTypeCH34X::parse_descriptors(usb_device_handle_t dev_hdl) {
|
||||
auto result = USBUartTypeCdcAcm::parse_descriptors(dev_hdl);
|
||||
// ch34x doesn't use the interrupt endpoint, and we don't have endpoints to spare
|
||||
for (auto &cdc_dev : result) {
|
||||
cdc_dev.interrupt_interface_number = 0xFF;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
} // namespace esphome::usb_uart
|
||||
|
||||
#endif // USE_ESP32_VARIANT_ESP32P4 || USE_ESP32_VARIANT_ESP32S2 || USE_ESP32_VARIANT_ESP32S3
|
||||
|
||||
@@ -20,6 +20,7 @@ static optional<CdcEps> get_cdc(const usb_config_desc_t *config_desc, uint8_t in
|
||||
// look for an interface with an interrupt endpoint (notify), and one with two bulk endpoints (data in/out)
|
||||
CdcEps eps{};
|
||||
eps.bulk_interface_number = 0xFF;
|
||||
eps.interrupt_interface_number = 0xFF;
|
||||
for (;;) {
|
||||
const auto *intf_desc = usb_parse_interface_descriptor(config_desc, intf_idx++, 0, &conf_offset);
|
||||
if (!intf_desc) {
|
||||
@@ -130,7 +131,7 @@ size_t RingBuffer::pop(uint8_t *data, size_t len) {
|
||||
}
|
||||
void USBUartChannel::write_array(const uint8_t *data, size_t len) {
|
||||
if (!this->initialised_.load()) {
|
||||
ESP_LOGV(TAG, "Channel not initialised - write ignored");
|
||||
ESP_LOGD(TAG, "Channel not initialised - write ignored");
|
||||
return;
|
||||
}
|
||||
#ifdef USE_UART_DEBUGGER
|
||||
@@ -415,14 +416,15 @@ void USBUartTypeCdcAcm::on_connected() {
|
||||
// Claim the communication (interrupt) interface so CDC class requests are accepted
|
||||
// by the device. Some CDC ACM implementations (e.g. EFR32 NCP) require this before
|
||||
// they enable data flow on the bulk endpoints.
|
||||
if (channel->cdc_dev_.interrupt_interface_number != channel->cdc_dev_.bulk_interface_number) {
|
||||
if (channel->cdc_dev_.interrupt_interface_number != 0xFF &&
|
||||
channel->cdc_dev_.interrupt_interface_number != channel->cdc_dev_.bulk_interface_number) {
|
||||
auto err_comm = usb_host_interface_claim(this->handle_, this->device_handle_,
|
||||
channel->cdc_dev_.interrupt_interface_number, 0);
|
||||
if (err_comm != ESP_OK) {
|
||||
ESP_LOGW(TAG, "Could not claim comm interface %d: %s", channel->cdc_dev_.interrupt_interface_number,
|
||||
esp_err_to_name(err_comm));
|
||||
channel->cdc_dev_.interrupt_interface_number = 0xFF; // Mark as unavailable, but continue anyway
|
||||
} else {
|
||||
channel->cdc_dev_.comm_interface_claimed = true;
|
||||
ESP_LOGD(TAG, "Claimed comm interface %d", channel->cdc_dev_.interrupt_interface_number);
|
||||
}
|
||||
}
|
||||
@@ -436,6 +438,7 @@ void USBUartTypeCdcAcm::on_connected() {
|
||||
return;
|
||||
}
|
||||
}
|
||||
this->status_clear_error();
|
||||
this->enable_channels();
|
||||
}
|
||||
|
||||
@@ -453,9 +456,10 @@ void USBUartTypeCdcAcm::on_disconnected() {
|
||||
usb_host_endpoint_halt(this->device_handle_, channel->cdc_dev_.notify_ep->bEndpointAddress);
|
||||
usb_host_endpoint_flush(this->device_handle_, channel->cdc_dev_.notify_ep->bEndpointAddress);
|
||||
}
|
||||
if (channel->cdc_dev_.comm_interface_claimed) {
|
||||
if (channel->cdc_dev_.interrupt_interface_number != 0xFF &&
|
||||
channel->cdc_dev_.interrupt_interface_number != channel->cdc_dev_.bulk_interface_number) {
|
||||
usb_host_interface_release(this->handle_, this->device_handle_, channel->cdc_dev_.interrupt_interface_number);
|
||||
channel->cdc_dev_.comm_interface_claimed = false;
|
||||
channel->cdc_dev_.interrupt_interface_number = 0xFF;
|
||||
}
|
||||
usb_host_interface_release(this->handle_, this->device_handle_, channel->cdc_dev_.bulk_interface_number);
|
||||
// Reset the input and output started flags to their initial state to avoid the possibility of spurious restarts
|
||||
|
||||
@@ -32,7 +32,6 @@ struct CdcEps {
|
||||
const usb_ep_desc_t *out_ep;
|
||||
uint8_t bulk_interface_number;
|
||||
uint8_t interrupt_interface_number;
|
||||
bool comm_interface_claimed{false};
|
||||
};
|
||||
|
||||
enum UARTParityOptions {
|
||||
@@ -192,6 +191,7 @@ class USBUartTypeCH34X : public USBUartTypeCdcAcm {
|
||||
|
||||
protected:
|
||||
void enable_channels() override;
|
||||
std::vector<CdcEps> parse_descriptors(usb_device_handle_t dev_hdl) override;
|
||||
};
|
||||
|
||||
} // namespace esphome::usb_uart
|
||||
|
||||
@@ -22,7 +22,11 @@ from esphome.const import (
|
||||
DEVICE_CLASS_WATER,
|
||||
)
|
||||
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
|
||||
|
||||
IS_PLATFORM_COMPONENT = True
|
||||
@@ -129,11 +133,9 @@ def valve_schema(
|
||||
return _VALVE_SCHEMA.extend(schema)
|
||||
|
||||
|
||||
@setup_entity("valve")
|
||||
async def _setup_valve_core(var, config):
|
||||
await setup_entity(var, config, "valve")
|
||||
|
||||
if device_class_config := config.get(CONF_DEVICE_CLASS):
|
||||
cg.add(var.set_device_class(device_class_config))
|
||||
setup_device_class(config)
|
||||
|
||||
for conf in config.get(CONF_ON_OPEN, []):
|
||||
trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var)
|
||||
|
||||
@@ -101,7 +101,7 @@ const LogString *valve_operation_to_str(ValveOperation op);
|
||||
* to control all values of the valve. Also implement get_traits() to return what operations
|
||||
* the valve supports.
|
||||
*/
|
||||
class Valve : public EntityBase, public EntityBase_DeviceClass {
|
||||
class Valve : public EntityBase {
|
||||
public:
|
||||
explicit Valve();
|
||||
|
||||
|
||||
@@ -69,10 +69,9 @@ def water_heater_schema(
|
||||
return _WATER_HEATER_SCHEMA.extend(schema)
|
||||
|
||||
|
||||
@setup_entity("water_heater")
|
||||
async def setup_water_heater_core_(var: cg.Pvariable, config: ConfigType) -> None:
|
||||
"""Set up the core water heater properties in C++."""
|
||||
await setup_entity(var, config, "water_heater")
|
||||
|
||||
visual = config[CONF_VISUAL]
|
||||
if (min_temp := visual.get(CONF_MIN_TEMPERATURE)) is not None:
|
||||
cg.add_define("USE_WATER_HEATER_VISUAL_OVERRIDES")
|
||||
|
||||
@@ -1139,7 +1139,7 @@ json::SerializationBuffer<> WebServer::number_json_(number::Number *obj, float v
|
||||
json::JsonBuilder builder;
|
||||
JsonObject root = builder.root();
|
||||
|
||||
const auto uom_ref = obj->traits.get_unit_of_measurement_ref();
|
||||
const auto uom_ref = obj->get_unit_of_measurement_ref();
|
||||
const int8_t accuracy = step_to_accuracy_decimals(obj->traits.get_step());
|
||||
|
||||
// Need two buffers: one for value, one for state with UOM
|
||||
|
||||
@@ -1,18 +1,16 @@
|
||||
import esphome.codegen as cg
|
||||
from esphome.components import uart
|
||||
from esphome.components.const import CONF_DATA_BITS, CONF_PARITY, CONF_STOP_BITS
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import (
|
||||
CONF_BAUD_RATE,
|
||||
CONF_CHANNEL,
|
||||
CONF_DATA_BITS,
|
||||
CONF_ID,
|
||||
CONF_INPUT,
|
||||
CONF_INVERTED,
|
||||
CONF_MODE,
|
||||
CONF_NUMBER,
|
||||
CONF_OUTPUT,
|
||||
CONF_PARITY,
|
||||
CONF_STOP_BITS,
|
||||
)
|
||||
|
||||
CODEOWNERS = ["@DrCoolZic"]
|
||||
|
||||
@@ -60,6 +60,7 @@ void arch_restart() { sys_reboot(SYS_REBOOT_COLD); }
|
||||
uint32_t arch_get_cpu_cycle_count() { return k_cycle_get_32(); }
|
||||
uint32_t arch_get_cpu_freq_hz() { return sys_clock_hw_cycles_per_sec(); }
|
||||
uint8_t progmem_read_byte(const uint8_t *addr) { return *addr; }
|
||||
const char *progmem_read_ptr(const char *const *addr) { return *addr; }
|
||||
uint16_t progmem_read_uint16(const uint16_t *addr) { return *addr; }
|
||||
|
||||
Mutex::Mutex() {
|
||||
|
||||
@@ -280,7 +280,6 @@ CONF_CUSTOM_PRESETS = "custom_presets"
|
||||
CONF_CYCLE = "cycle"
|
||||
CONF_DALLAS_ID = "dallas_id"
|
||||
CONF_DATA = "data"
|
||||
CONF_DATA_BITS = "data_bits"
|
||||
CONF_DATA_PIN = "data_pin"
|
||||
CONF_DATA_PINS = "data_pins"
|
||||
CONF_DATA_RATE = "data_rate"
|
||||
@@ -760,7 +759,6 @@ CONF_PAGE_ID = "page_id"
|
||||
CONF_PAGES = "pages"
|
||||
CONF_PANASONIC = "panasonic"
|
||||
CONF_PARAMETERS = "parameters"
|
||||
CONF_PARITY = "parity"
|
||||
CONF_PASSWORD = "password"
|
||||
CONF_PATH = "path"
|
||||
CONF_PATTERN = "pattern"
|
||||
@@ -963,7 +961,6 @@ CONF_STEP_PIN = "step_pin"
|
||||
CONF_STILL_THRESHOLD = "still_threshold"
|
||||
CONF_STOP = "stop"
|
||||
CONF_STOP_ACTION = "stop_action"
|
||||
CONF_STOP_BITS = "stop_bits"
|
||||
CONF_STORE_BASELINE = "store_baseline"
|
||||
CONF_SUBNET = "subnet"
|
||||
CONF_SUBSCRIBE_QOS = "subscribe_qos"
|
||||
@@ -1094,6 +1091,7 @@ CONF_WAND_ID = "wand_id"
|
||||
CONF_WARM_WHITE = "warm_white"
|
||||
CONF_WARM_WHITE_COLOR_TEMPERATURE = "warm_white_color_temperature"
|
||||
CONF_WARMUP_TIME = "warmup_time"
|
||||
CONF_WATCHDOG = "watchdog"
|
||||
CONF_WATCHDOG_THRESHOLD = "watchdog_threshold"
|
||||
CONF_WATCHDOG_TIMEOUT = "watchdog_timeout"
|
||||
CONF_WATER_HEATER = "water_heater"
|
||||
|
||||
@@ -552,7 +552,32 @@ void Application::after_loop_tasks_() {
|
||||
this->in_loop_ = false;
|
||||
}
|
||||
|
||||
#ifdef USE_SOCKET_SELECT_SUPPORT
|
||||
#ifdef USE_LWIP_FAST_SELECT
|
||||
bool Application::register_socket(struct lwip_sock *sock) {
|
||||
// It modifies monitored_sockets_ without locking — must only be called from the main loop.
|
||||
if (sock == nullptr)
|
||||
return false;
|
||||
esphome_lwip_hook_socket(sock);
|
||||
this->monitored_sockets_.push_back(sock);
|
||||
return true;
|
||||
}
|
||||
|
||||
void Application::unregister_socket(struct lwip_sock *sock) {
|
||||
// It modifies monitored_sockets_ without locking — must only be called from the main loop.
|
||||
for (size_t i = 0; i < this->monitored_sockets_.size(); i++) {
|
||||
if (this->monitored_sockets_[i] != sock)
|
||||
continue;
|
||||
|
||||
// Swap with last element and pop - O(1) removal since order doesn't matter.
|
||||
// No need to unhook the netconn callback — all LwIP sockets share the same
|
||||
// static event_callback, and the socket will be closed by the caller.
|
||||
if (i < this->monitored_sockets_.size() - 1)
|
||||
this->monitored_sockets_[i] = this->monitored_sockets_.back();
|
||||
this->monitored_sockets_.pop_back();
|
||||
return;
|
||||
}
|
||||
}
|
||||
#elif defined(USE_SOCKET_SELECT_SUPPORT)
|
||||
bool Application::register_socket_fd(int fd) {
|
||||
// WARNING: This function is NOT thread-safe and must only be called from the main loop
|
||||
// It modifies socket_fds_ and related variables without locking
|
||||
@@ -571,15 +596,10 @@ bool Application::register_socket_fd(int fd) {
|
||||
#endif
|
||||
|
||||
this->socket_fds_.push_back(fd);
|
||||
#ifdef USE_LWIP_FAST_SELECT
|
||||
// Hook the socket's netconn callback for instant wake on receive events
|
||||
esphome_lwip_hook_socket(fd);
|
||||
#else
|
||||
this->socket_fds_changed_ = true;
|
||||
if (fd > this->max_fd_) {
|
||||
this->max_fd_ = fd;
|
||||
}
|
||||
#endif
|
||||
|
||||
return true;
|
||||
}
|
||||
@@ -595,13 +615,9 @@ void Application::unregister_socket_fd(int fd) {
|
||||
continue;
|
||||
|
||||
// Swap with last element and pop - O(1) removal since order doesn't matter.
|
||||
// No need to unhook the netconn callback on fast select platforms — all LwIP
|
||||
// sockets share the same static event_callback, and the socket will be closed
|
||||
// by the caller.
|
||||
if (i < this->socket_fds_.size() - 1)
|
||||
this->socket_fds_[i] = this->socket_fds_.back();
|
||||
this->socket_fds_.pop_back();
|
||||
#ifndef USE_LWIP_FAST_SELECT
|
||||
this->socket_fds_changed_ = true;
|
||||
// Only recalculate max_fd if we removed the current max
|
||||
if (fd == this->max_fd_) {
|
||||
@@ -611,7 +627,6 @@ void Application::unregister_socket_fd(int fd) {
|
||||
this->max_fd_ = sock_fd;
|
||||
}
|
||||
}
|
||||
#endif
|
||||
return;
|
||||
}
|
||||
}
|
||||
@@ -621,7 +636,7 @@ void Application::unregister_socket_fd(int fd) {
|
||||
void Application::yield_with_select_(uint32_t delay_ms) {
|
||||
// Delay while monitoring sockets. When delay_ms is 0, always yield() to ensure other tasks run.
|
||||
#if defined(USE_SOCKET_SELECT_SUPPORT) && defined(USE_LWIP_FAST_SELECT)
|
||||
// Fast path (ESP32/LibreTiny): reads rcvevent directly via lwip_socket_dbg_get_socket().
|
||||
// Fast path (ESP32/LibreTiny): reads rcvevent directly from cached lwip_sock pointers.
|
||||
// Safe because this runs on the main loop which owns socket lifetime (create, read, close).
|
||||
if (delay_ms == 0) [[unlikely]] {
|
||||
yield();
|
||||
@@ -632,8 +647,8 @@ void Application::yield_with_select_(uint32_t delay_ms) {
|
||||
// If a socket still has unread data (rcvevent > 0) but the task notification was already
|
||||
// consumed, ulTaskNotifyTake would block until timeout — adding up to delay_ms latency.
|
||||
// This scan preserves select() semantics: return immediately when any fd is ready.
|
||||
for (int fd : this->socket_fds_) {
|
||||
if (esphome_lwip_socket_has_data(fd)) {
|
||||
for (struct lwip_sock *sock : this->monitored_sockets_) {
|
||||
if (esphome_lwip_socket_has_data(sock)) {
|
||||
yield();
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -34,7 +34,11 @@
|
||||
#endif
|
||||
#endif
|
||||
#endif // USE_SOCKET_SELECT_SUPPORT
|
||||
|
||||
#if defined(USE_ESP8266) && defined(USE_SOCKET_IMPL_LWIP_TCP)
|
||||
namespace esphome::socket {
|
||||
void socket_wake(); // NOLINT(readability-redundant-declaration)
|
||||
} // namespace esphome::socket
|
||||
#endif
|
||||
#ifdef USE_BINARY_SENSOR
|
||||
#include "esphome/components/binary_sensor/binary_sensor.h"
|
||||
#endif
|
||||
@@ -510,14 +514,20 @@ class Application {
|
||||
|
||||
Scheduler scheduler;
|
||||
|
||||
/// Register/unregister a socket file descriptor to be monitored for read events.
|
||||
#ifdef USE_SOCKET_SELECT_SUPPORT
|
||||
/// These functions update the fd_set used by select() in the main loop.
|
||||
/// Register/unregister a socket to be monitored for read events.
|
||||
/// WARNING: These functions are NOT thread-safe. They must only be called from the main loop.
|
||||
#ifdef USE_LWIP_FAST_SELECT
|
||||
/// Fast select path: hooks netconn callback and registers for monitoring.
|
||||
/// @return true if registration was successful, false if sock is null
|
||||
bool register_socket(struct lwip_sock *sock);
|
||||
void unregister_socket(struct lwip_sock *sock);
|
||||
#elif defined(USE_SOCKET_SELECT_SUPPORT)
|
||||
/// Fallback select() path: monitors file descriptors.
|
||||
/// NOTE: File descriptors >= FD_SETSIZE (typically 10 on ESP) will be rejected with an error.
|
||||
/// @return true if registration was successful, false if fd exceeds limits
|
||||
bool register_socket_fd(int fd);
|
||||
void unregister_socket_fd(int fd);
|
||||
#endif
|
||||
|
||||
#ifdef USE_WAKE_LOOP_THREADSAFE
|
||||
/// Wake the main event loop from another FreeRTOS task.
|
||||
@@ -542,27 +552,23 @@ class Application {
|
||||
static void IRAM_ATTR wake_loop_any_context() { esphome_lwip_wake_main_loop_any_context(); }
|
||||
#endif
|
||||
#endif
|
||||
|
||||
#if defined(USE_ESP8266) && defined(USE_SOCKET_IMPL_LWIP_TCP)
|
||||
/// Wake the main event loop from any context (ISR, thread, or main loop).
|
||||
/// On ESP8266: sets the socket wake flag and calls esp_schedule() to exit esp_delay() early.
|
||||
static void IRAM_ATTR wake_loop_any_context() { socket::socket_wake(); }
|
||||
#endif
|
||||
|
||||
protected:
|
||||
friend Component;
|
||||
#ifdef USE_SOCKET_SELECT_SUPPORT
|
||||
#if defined(USE_SOCKET_SELECT_SUPPORT) && !defined(USE_LWIP_FAST_SELECT)
|
||||
friend bool socket::socket_ready_fd(int fd, bool loop_monitored);
|
||||
#endif
|
||||
friend void ::setup();
|
||||
friend void ::original_setup();
|
||||
|
||||
#ifdef USE_SOCKET_SELECT_SUPPORT
|
||||
/// Fast path for Socket::ready() via friendship - skips negative fd check.
|
||||
/// Main loop only — with USE_LWIP_FAST_SELECT, reads rcvevent via
|
||||
/// lwip_socket_dbg_get_socket(), which has no refcount; safe only because
|
||||
/// the main loop owns socket lifetime (creates, reads, and closes sockets
|
||||
/// on the same thread).
|
||||
#ifdef USE_LWIP_FAST_SELECT
|
||||
bool is_socket_ready_(int fd) const { return esphome_lwip_socket_has_data(fd); }
|
||||
#else
|
||||
#if defined(USE_SOCKET_SELECT_SUPPORT) && !defined(USE_LWIP_FAST_SELECT)
|
||||
bool is_socket_ready_(int fd) const { return FD_ISSET(fd, &this->read_fds_); }
|
||||
#endif
|
||||
#endif
|
||||
|
||||
/// Register a component, detecting loop() override at compile time.
|
||||
@@ -624,8 +630,12 @@ class Application {
|
||||
// and active_end_ is incremented
|
||||
// - This eliminates branch mispredictions from flag checking in the hot loop
|
||||
FixedVector<Component *> looping_components_{};
|
||||
#ifdef USE_SOCKET_SELECT_SUPPORT
|
||||
#ifdef USE_LWIP_FAST_SELECT
|
||||
std::vector<struct lwip_sock *> monitored_sockets_; // Cached lwip_sock pointers for direct rcvevent read
|
||||
#elif defined(USE_SOCKET_SELECT_SUPPORT)
|
||||
std::vector<int> socket_fds_; // Vector of all monitored socket file descriptors
|
||||
#endif
|
||||
#ifdef USE_SOCKET_SELECT_SUPPORT
|
||||
#if defined(USE_WAKE_LOOP_THREADSAFE) && !defined(USE_LWIP_FAST_SELECT)
|
||||
int wake_socket_fd_{-1}; // Shared wake notification socket for waking main loop from tasks
|
||||
#endif
|
||||
|
||||
@@ -233,7 +233,6 @@ void Component::call_dump_config_() {
|
||||
}
|
||||
}
|
||||
|
||||
uint8_t Component::get_component_state() const { return this->component_state_; }
|
||||
void Component::call() {
|
||||
uint8_t state = this->component_state_ & COMPONENT_STATE_MASK;
|
||||
switch (state) {
|
||||
@@ -323,10 +322,11 @@ void IRAM_ATTR HOT Component::enable_loop_soon_any_context() {
|
||||
// 8. Race condition with main loop is handled by clearing flag before processing
|
||||
this->pending_enable_loop_ = true;
|
||||
App.has_pending_enable_loop_requests_ = true;
|
||||
#if defined(USE_LWIP_FAST_SELECT) && defined(USE_ESP32)
|
||||
// Wake the main loop if sleeping in ulTaskNotifyTake(). Without this,
|
||||
// the main loop would not wake until the select timeout expires (~16ms).
|
||||
// Uses xPortInIsrContext() to choose the correct FreeRTOS notify API.
|
||||
#if (defined(USE_LWIP_FAST_SELECT) && defined(USE_ESP32)) || (defined(USE_ESP8266) && defined(USE_SOCKET_IMPL_LWIP_TCP))
|
||||
// Wake the main loop from sleep. Without this, the main loop would not
|
||||
// wake until the select/delay timeout expires (~16ms).
|
||||
// ESP32: uses xPortInIsrContext() to choose the correct FreeRTOS notify API.
|
||||
// ESP8266: sets socket wake flag and calls esp_schedule() to exit esp_delay() early.
|
||||
Application::wake_loop_any_context();
|
||||
#endif
|
||||
}
|
||||
@@ -338,9 +338,6 @@ void Component::reset_to_construction_state() {
|
||||
this->status_clear_error();
|
||||
}
|
||||
}
|
||||
bool Component::is_in_loop_state() const {
|
||||
return (this->component_state_ & COMPONENT_STATE_MASK) == COMPONENT_STATE_LOOP;
|
||||
}
|
||||
void Component::defer(std::function<void()> &&f) { // NOLINT
|
||||
App.scheduler.set_timeout(this, static_cast<const char *>(nullptr), 0, std::move(f));
|
||||
}
|
||||
@@ -379,16 +376,12 @@ void Component::set_retry(uint32_t initial_wait_time, uint8_t max_attempts, std:
|
||||
App.scheduler.set_retry(this, "", initial_wait_time, max_attempts, std::move(f), backoff_increase_factor);
|
||||
#pragma GCC diagnostic pop
|
||||
}
|
||||
bool Component::is_failed() const { return (this->component_state_ & COMPONENT_STATE_MASK) == COMPONENT_STATE_FAILED; }
|
||||
bool Component::is_ready() const {
|
||||
return (this->component_state_ & COMPONENT_STATE_MASK) == COMPONENT_STATE_LOOP ||
|
||||
(this->component_state_ & COMPONENT_STATE_MASK) == COMPONENT_STATE_LOOP_DONE ||
|
||||
(this->component_state_ & COMPONENT_STATE_MASK) == COMPONENT_STATE_SETUP;
|
||||
}
|
||||
bool Component::is_idle() const { return (this->component_state_ & COMPONENT_STATE_MASK) == COMPONENT_STATE_LOOP_DONE; }
|
||||
bool Component::can_proceed() { return true; }
|
||||
bool Component::status_has_warning() const { return this->component_state_ & STATUS_LED_WARNING; }
|
||||
bool Component::status_has_error() const { return this->component_state_ & STATUS_LED_ERROR; }
|
||||
bool Component::set_status_flag_(uint8_t flag) {
|
||||
if ((this->component_state_ & flag) != 0)
|
||||
return false;
|
||||
|
||||
@@ -142,7 +142,7 @@ class Component {
|
||||
*/
|
||||
virtual void on_powerdown() {}
|
||||
|
||||
uint8_t get_component_state() const;
|
||||
uint8_t get_component_state() const { return this->component_state_; }
|
||||
|
||||
/** Reset this component back to the construction state to allow setup to run again.
|
||||
*
|
||||
@@ -154,7 +154,7 @@ class Component {
|
||||
*
|
||||
* @return True if in loop state, false otherwise.
|
||||
*/
|
||||
bool is_in_loop_state() const;
|
||||
bool is_in_loop_state() const { return (this->component_state_ & COMPONENT_STATE_MASK) == COMPONENT_STATE_LOOP; }
|
||||
|
||||
/** Check if this component is idle.
|
||||
* Being idle means being in LOOP_DONE state.
|
||||
@@ -162,7 +162,7 @@ class Component {
|
||||
*
|
||||
* @return True if the component is idle
|
||||
*/
|
||||
bool is_idle() const;
|
||||
bool is_idle() const { return (this->component_state_ & COMPONENT_STATE_MASK) == COMPONENT_STATE_LOOP_DONE; }
|
||||
|
||||
/** Mark this component as failed. Any future timeouts/intervals/setup/loop will no longer be called.
|
||||
*
|
||||
@@ -230,15 +230,15 @@ class Component {
|
||||
*/
|
||||
void enable_loop_soon_any_context();
|
||||
|
||||
bool is_failed() const;
|
||||
bool is_failed() const { return (this->component_state_ & COMPONENT_STATE_MASK) == COMPONENT_STATE_FAILED; }
|
||||
|
||||
bool is_ready() const;
|
||||
|
||||
virtual bool can_proceed();
|
||||
|
||||
bool status_has_warning() const;
|
||||
bool status_has_warning() const { return this->component_state_ & STATUS_LED_WARNING; }
|
||||
|
||||
bool status_has_error() const;
|
||||
bool status_has_error() const { return this->component_state_ & STATUS_LED_ERROR; }
|
||||
|
||||
void status_set_warning(const char *message = nullptr);
|
||||
void status_set_warning(const LogString *message);
|
||||
|
||||
@@ -44,7 +44,9 @@
|
||||
#define USE_DEEP_SLEEP
|
||||
#define USE_DEVICES
|
||||
#define USE_DISPLAY
|
||||
#define USE_ENTITY_DEVICE_CLASS
|
||||
#define USE_ENTITY_ICON
|
||||
#define USE_ENTITY_UNIT_OF_MEASUREMENT
|
||||
#define USE_ESP32_CAMERA_JPEG_CONVERSION
|
||||
#define USE_ESP32_HOSTED
|
||||
#define USE_ESP32_IMPROV_STATE_CALLBACK
|
||||
@@ -107,6 +109,7 @@
|
||||
#define SERIAL_PROXY_COUNT 2
|
||||
#define SNTP_SERVER_COUNT 3
|
||||
#define USE_MEDIA_PLAYER
|
||||
#define USE_MEDIA_SOURCE
|
||||
#define USE_NEXTION_TFT_UPLOAD
|
||||
#define USE_NUMBER
|
||||
#define USE_OUTPUT
|
||||
@@ -234,6 +237,7 @@
|
||||
#define USE_LWIP_FAST_SELECT
|
||||
#define USE_WAKE_LOOP_THREADSAFE
|
||||
#define USE_SPEAKER
|
||||
#define USE_SPEAKER_MEDIA_PLAYER_ON_OFF
|
||||
#define USE_SPI
|
||||
#define USE_VOICE_ASSISTANT
|
||||
#define USE_WEBSERVER
|
||||
|
||||
@@ -45,24 +45,42 @@ void EntityBase::set_name(const char *name, uint32_t object_id_hash) {
|
||||
}
|
||||
}
|
||||
|
||||
// Entity Icon
|
||||
std::string EntityBase::get_icon() const {
|
||||
#ifdef USE_ENTITY_ICON
|
||||
if (this->icon_c_str_ == nullptr) {
|
||||
return "";
|
||||
}
|
||||
return this->icon_c_str_;
|
||||
// Weak default lookup functions — overridden by generated code in main.cpp
|
||||
__attribute__((weak)) const char *entity_device_class_lookup(uint8_t) { return ""; }
|
||||
__attribute__((weak)) const char *entity_uom_lookup(uint8_t) { return ""; }
|
||||
__attribute__((weak)) const char *entity_icon_lookup(uint8_t) { return ""; }
|
||||
|
||||
// Entity device class (from index)
|
||||
StringRef EntityBase::get_device_class_ref() const {
|
||||
#ifdef USE_ENTITY_DEVICE_CLASS
|
||||
return StringRef(entity_device_class_lookup(this->device_class_idx_));
|
||||
#else
|
||||
return "";
|
||||
return StringRef(entity_device_class_lookup(0));
|
||||
#endif
|
||||
}
|
||||
void EntityBase::set_icon(const char *icon) {
|
||||
#ifdef USE_ENTITY_ICON
|
||||
this->icon_c_str_ = icon;
|
||||
std::string EntityBase::get_device_class() const { return std::string(this->get_device_class_ref().c_str()); }
|
||||
|
||||
// Entity unit of measurement (from index)
|
||||
StringRef EntityBase::get_unit_of_measurement_ref() const {
|
||||
#ifdef USE_ENTITY_UNIT_OF_MEASUREMENT
|
||||
return StringRef(entity_uom_lookup(this->uom_idx_));
|
||||
#else
|
||||
// No-op when USE_ENTITY_ICON is not defined
|
||||
return StringRef(entity_uom_lookup(0));
|
||||
#endif
|
||||
}
|
||||
std::string EntityBase::get_unit_of_measurement() const {
|
||||
return std::string(this->get_unit_of_measurement_ref().c_str());
|
||||
}
|
||||
|
||||
// Entity icon (from index)
|
||||
StringRef EntityBase::get_icon_ref() const {
|
||||
#ifdef USE_ENTITY_ICON
|
||||
return StringRef(entity_icon_lookup(this->icon_idx_));
|
||||
#else
|
||||
return StringRef(entity_icon_lookup(0));
|
||||
#endif
|
||||
}
|
||||
std::string EntityBase::get_icon() const { return std::string(this->get_icon_ref().c_str()); }
|
||||
|
||||
// Entity Object ID - computed on-demand from name
|
||||
std::string EntityBase::get_object_id() const {
|
||||
@@ -134,24 +152,6 @@ ESPPreferenceObject EntityBase::make_entity_preference_(size_t size, uint32_t ve
|
||||
return global_preferences->make_preference(size, key);
|
||||
}
|
||||
|
||||
std::string EntityBase_DeviceClass::get_device_class() {
|
||||
if (this->device_class_ == nullptr) {
|
||||
return "";
|
||||
}
|
||||
return this->device_class_;
|
||||
}
|
||||
|
||||
void EntityBase_DeviceClass::set_device_class(const char *device_class) { this->device_class_ = device_class; }
|
||||
|
||||
std::string EntityBase_UnitOfMeasurement::get_unit_of_measurement() {
|
||||
if (this->unit_of_measurement_ == nullptr)
|
||||
return "";
|
||||
return this->unit_of_measurement_;
|
||||
}
|
||||
void EntityBase_UnitOfMeasurement::set_unit_of_measurement(const char *unit_of_measurement) {
|
||||
this->unit_of_measurement_ = unit_of_measurement;
|
||||
}
|
||||
|
||||
#ifdef USE_ENTITY_ICON
|
||||
void log_entity_icon(const char *tag, const char *prefix, const EntityBase &obj) {
|
||||
if (!obj.get_icon_ref().empty()) {
|
||||
@@ -160,13 +160,13 @@ void log_entity_icon(const char *tag, const char *prefix, const EntityBase &obj)
|
||||
}
|
||||
#endif
|
||||
|
||||
void log_entity_device_class(const char *tag, const char *prefix, const EntityBase_DeviceClass &obj) {
|
||||
void log_entity_device_class(const char *tag, const char *prefix, const EntityBase &obj) {
|
||||
if (!obj.get_device_class_ref().empty()) {
|
||||
ESP_LOGCONFIG(tag, "%s Device Class: '%s'", prefix, obj.get_device_class_ref().c_str());
|
||||
}
|
||||
}
|
||||
|
||||
void log_entity_unit_of_measurement(const char *tag, const char *prefix, const EntityBase_UnitOfMeasurement &obj) {
|
||||
void log_entity_unit_of_measurement(const char *tag, const char *prefix, const EntityBase &obj) {
|
||||
if (!obj.get_unit_of_measurement_ref().empty()) {
|
||||
ESP_LOGCONFIG(tag, "%s Unit of Measurement: '%s'", prefix, obj.get_unit_of_measurement_ref().c_str());
|
||||
}
|
||||
|
||||
@@ -14,6 +14,12 @@
|
||||
|
||||
namespace esphome {
|
||||
|
||||
// Extern lookup functions for entity string tables.
|
||||
// Generated code provides strong definitions; weak defaults return "".
|
||||
extern const char *entity_device_class_lookup(uint8_t index);
|
||||
extern const char *entity_uom_lookup(uint8_t index);
|
||||
extern const char *entity_icon_lookup(uint8_t index);
|
||||
|
||||
// Maximum device name length - keep in sync with validate_hostname() in esphome/core/config.py
|
||||
static constexpr size_t ESPHOME_DEVICE_NAME_MAX_LEN = 31;
|
||||
|
||||
@@ -89,20 +95,41 @@ class EntityBase {
|
||||
this->flags_.entity_category = static_cast<uint8_t>(entity_category);
|
||||
}
|
||||
|
||||
// Set entity string table indices — one call per entity from codegen.
|
||||
// Packed: [23..16] icon | [15..8] UoM | [7..0] device_class (each 8 bits)
|
||||
void set_entity_strings([[maybe_unused]] uint32_t packed) {
|
||||
#ifdef USE_ENTITY_DEVICE_CLASS
|
||||
this->device_class_idx_ = packed & 0xFF;
|
||||
#endif
|
||||
#ifdef USE_ENTITY_UNIT_OF_MEASUREMENT
|
||||
this->uom_idx_ = (packed >> 8) & 0xFF;
|
||||
#endif
|
||||
#ifdef USE_ENTITY_ICON
|
||||
this->icon_idx_ = (packed >> 16) & 0xFF;
|
||||
#endif
|
||||
}
|
||||
|
||||
// Get device class as StringRef (from packed index)
|
||||
StringRef get_device_class_ref() const;
|
||||
/// Get the device class as std::string (deprecated, prefer get_device_class_ref())
|
||||
ESPDEPRECATED("Use get_device_class_ref() instead for better performance (avoids string copy). Will be removed in "
|
||||
"ESPHome 2026.9.0",
|
||||
"2026.3.0")
|
||||
std::string get_device_class() const;
|
||||
// Get unit of measurement as StringRef (from packed index)
|
||||
StringRef get_unit_of_measurement_ref() const;
|
||||
/// Get the unit of measurement as std::string (deprecated, prefer get_unit_of_measurement_ref())
|
||||
ESPDEPRECATED("Use get_unit_of_measurement_ref() instead for better performance (avoids string copy). Will be "
|
||||
"removed in ESPHome 2026.9.0",
|
||||
"2026.3.0")
|
||||
std::string get_unit_of_measurement() const;
|
||||
|
||||
// Get/set this entity's icon
|
||||
ESPDEPRECATED(
|
||||
"Use get_icon_ref() instead for better performance (avoids string copy). Will be removed in ESPHome 2026.5.0",
|
||||
"2025.11.0")
|
||||
std::string get_icon() const;
|
||||
void set_icon(const char *icon);
|
||||
StringRef get_icon_ref() const {
|
||||
static constexpr auto EMPTY_STRING = StringRef::from_lit("");
|
||||
#ifdef USE_ENTITY_ICON
|
||||
return this->icon_c_str_ == nullptr ? EMPTY_STRING : StringRef(this->icon_c_str_);
|
||||
#else
|
||||
return EMPTY_STRING;
|
||||
#endif
|
||||
}
|
||||
StringRef get_icon_ref() const;
|
||||
|
||||
#ifdef USE_DEVICES
|
||||
// Get/set this entity's device id
|
||||
@@ -173,9 +200,6 @@ class EntityBase {
|
||||
void calc_object_id_();
|
||||
|
||||
StringRef name_;
|
||||
#ifdef USE_ENTITY_ICON
|
||||
const char *icon_c_str_{nullptr};
|
||||
#endif
|
||||
uint32_t object_id_hash_{};
|
||||
#ifdef USE_DEVICES
|
||||
Device *device_{};
|
||||
@@ -190,44 +214,16 @@ class EntityBase {
|
||||
uint8_t entity_category : 2; // Supports up to 4 categories
|
||||
uint8_t reserved : 2; // Reserved for future use
|
||||
} flags_{};
|
||||
};
|
||||
|
||||
class EntityBase_DeviceClass { // NOLINT(readability-identifier-naming)
|
||||
public:
|
||||
/// Get the device class, using the manual override if set.
|
||||
ESPDEPRECATED("Use get_device_class_ref() instead for better performance (avoids string copy). Will be removed in "
|
||||
"ESPHome 2026.5.0",
|
||||
"2025.11.0")
|
||||
std::string get_device_class();
|
||||
/// Manually set the device class.
|
||||
void set_device_class(const char *device_class);
|
||||
/// Get the device class as StringRef
|
||||
StringRef get_device_class_ref() const {
|
||||
static constexpr auto EMPTY_STRING = StringRef::from_lit("");
|
||||
return this->device_class_ == nullptr ? EMPTY_STRING : StringRef(this->device_class_);
|
||||
}
|
||||
|
||||
protected:
|
||||
const char *device_class_{nullptr}; ///< Device class override
|
||||
};
|
||||
|
||||
class EntityBase_UnitOfMeasurement { // NOLINT(readability-identifier-naming)
|
||||
public:
|
||||
/// Get the unit of measurement, using the manual override if set.
|
||||
ESPDEPRECATED("Use get_unit_of_measurement_ref() instead for better performance (avoids string copy). Will be "
|
||||
"removed in ESPHome 2026.5.0",
|
||||
"2025.11.0")
|
||||
std::string get_unit_of_measurement();
|
||||
/// Manually set the unit of measurement.
|
||||
void set_unit_of_measurement(const char *unit_of_measurement);
|
||||
/// Get the unit of measurement as StringRef
|
||||
StringRef get_unit_of_measurement_ref() const {
|
||||
static constexpr auto EMPTY_STRING = StringRef::from_lit("");
|
||||
return this->unit_of_measurement_ == nullptr ? EMPTY_STRING : StringRef(this->unit_of_measurement_);
|
||||
}
|
||||
|
||||
protected:
|
||||
const char *unit_of_measurement_{nullptr}; ///< Unit of measurement override
|
||||
// String table indices — packed into the 3 padding bytes after flags_
|
||||
#ifdef USE_ENTITY_DEVICE_CLASS
|
||||
uint8_t device_class_idx_{};
|
||||
#endif
|
||||
#ifdef USE_ENTITY_UNIT_OF_MEASUREMENT
|
||||
uint8_t uom_idx_{};
|
||||
#endif
|
||||
#ifdef USE_ENTITY_ICON
|
||||
uint8_t icon_idx_{};
|
||||
#endif
|
||||
};
|
||||
|
||||
/// Log entity icon if set (for use in dump_config)
|
||||
@@ -240,10 +236,10 @@ inline void log_entity_icon(const char *, const char *, const EntityBase &) {}
|
||||
#endif
|
||||
/// Log entity device class if set (for use in dump_config)
|
||||
#define LOG_ENTITY_DEVICE_CLASS(tag, prefix, obj) log_entity_device_class(tag, prefix, obj)
|
||||
void log_entity_device_class(const char *tag, const char *prefix, const EntityBase_DeviceClass &obj);
|
||||
void log_entity_device_class(const char *tag, const char *prefix, const EntityBase &obj);
|
||||
/// Log entity unit of measurement if set (for use in dump_config)
|
||||
#define LOG_ENTITY_UNIT_OF_MEASUREMENT(tag, prefix, obj) log_entity_unit_of_measurement(tag, prefix, obj)
|
||||
void log_entity_unit_of_measurement(const char *tag, const char *prefix, const EntityBase_UnitOfMeasurement &obj);
|
||||
void log_entity_unit_of_measurement(const char *tag, const char *prefix, const EntityBase &obj);
|
||||
|
||||
/**
|
||||
* An entity that has a state.
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
from collections.abc import Callable
|
||||
from dataclasses import dataclass, field
|
||||
import functools
|
||||
import logging
|
||||
|
||||
import esphome.codegen as cg
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import (
|
||||
CONF_DEVICE_CLASS,
|
||||
CONF_DEVICE_ID,
|
||||
CONF_DISABLED_BY_DEFAULT,
|
||||
CONF_ENTITY_CATEGORY,
|
||||
@@ -11,15 +14,184 @@ from esphome.const import (
|
||||
CONF_ID,
|
||||
CONF_INTERNAL,
|
||||
CONF_NAME,
|
||||
CONF_UNIT_OF_MEASUREMENT,
|
||||
)
|
||||
from esphome.core import CORE, ID
|
||||
from esphome.cpp_generator import MockObj, add, get_variable
|
||||
from esphome.core import CORE, ID, CoroPriority, coroutine_with_priority
|
||||
from esphome.cpp_generator import MockObj, RawStatement, add, get_variable
|
||||
import esphome.final_validate as fv
|
||||
from esphome.helpers import fnv1_hash_object_id, sanitize, snake_case
|
||||
from esphome.helpers import cpp_string_escape, fnv1_hash_object_id, sanitize, snake_case
|
||||
from esphome.types import ConfigType, EntityMetadata
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
DOMAIN = "entity_string_pool"
|
||||
|
||||
# Private config keys for storing registered string indices
|
||||
_KEY_DC_IDX = "_entity_dc_idx"
|
||||
_KEY_UOM_IDX = "_entity_uom_idx"
|
||||
_KEY_ICON_IDX = "_entity_icon_idx"
|
||||
|
||||
# Bit layout for set_entity_strings(packed) — must match C++ setter in entity_base.h:
|
||||
# [23..16] icon (8 bits) | [15..8] UoM (8 bits) | [7..0] device_class (8 bits)
|
||||
_DC_SHIFT = 0
|
||||
_UOM_SHIFT = 8
|
||||
_ICON_SHIFT = 16
|
||||
|
||||
# Maximum unique strings per category (8-bit index, 0 = not set)
|
||||
_MAX_DEVICE_CLASSES = 0xFF # 255
|
||||
_MAX_UNITS = 0xFF # 255
|
||||
_MAX_ICONS = 0xFF # 255
|
||||
|
||||
|
||||
@dataclass
|
||||
class EntityStringPool:
|
||||
"""Pool of entity string properties for PROGMEM pointer tables.
|
||||
|
||||
Strings are registered during to_code() and assigned 1-based indices.
|
||||
Index 0 means "not set" (empty string). At render time, the pool
|
||||
generates C++ PROGMEM pointer table + lookup function per category.
|
||||
"""
|
||||
|
||||
device_classes: dict[str, int] = field(default_factory=dict)
|
||||
units: dict[str, int] = field(default_factory=dict)
|
||||
icons: dict[str, int] = field(default_factory=dict)
|
||||
tables_registered: bool = False
|
||||
|
||||
|
||||
def _get_pool() -> EntityStringPool:
|
||||
"""Get or create the entity string pool from CORE.data."""
|
||||
if DOMAIN not in CORE.data:
|
||||
CORE.data[DOMAIN] = EntityStringPool()
|
||||
return CORE.data[DOMAIN]
|
||||
|
||||
|
||||
def _ensure_tables_registered() -> None:
|
||||
"""Schedule the table generation job (once)."""
|
||||
pool = _get_pool()
|
||||
if pool.tables_registered:
|
||||
return
|
||||
pool.tables_registered = True
|
||||
CORE.add_job(_generate_tables_job)
|
||||
|
||||
|
||||
def _generate_category_code(
|
||||
table_var: str,
|
||||
lookup_fn: str,
|
||||
strings: dict[str, int],
|
||||
) -> str:
|
||||
"""Generate C++ code for one string category (PROGMEM pointer table + lookup).
|
||||
|
||||
Uses a PROGMEM array of string pointers. On ESP8266, pointers are stored
|
||||
in flash (via PROGMEM) and read with progmem_read_ptr(). String literals
|
||||
themselves remain in RAM but benefit from linker string deduplication.
|
||||
Index 0 means "not set" and returns empty string.
|
||||
"""
|
||||
if not strings:
|
||||
return ""
|
||||
|
||||
sorted_strings = sorted(strings.items(), key=lambda x: x[1])
|
||||
entries = ", ".join(cpp_string_escape(s) for s, _ in sorted_strings)
|
||||
count = len(sorted_strings)
|
||||
|
||||
return (
|
||||
f"static const char *const {table_var}[] PROGMEM = {{{entries}}};\n"
|
||||
f"const char *{lookup_fn}(uint8_t index) {{\n"
|
||||
f' if (index == 0 || index > {count}) return "";\n'
|
||||
f" return progmem_read_ptr(&{table_var}[index - 1]);\n"
|
||||
f"}}\n"
|
||||
)
|
||||
|
||||
|
||||
_CATEGORY_CONFIGS = (
|
||||
("ENTITY_DC_TABLE", "entity_device_class_lookup", "device_classes"),
|
||||
("ENTITY_UOM_TABLE", "entity_uom_lookup", "units"),
|
||||
("ENTITY_ICON_TABLE", "entity_icon_lookup", "icons"),
|
||||
)
|
||||
|
||||
|
||||
@coroutine_with_priority(CoroPriority.FINAL)
|
||||
async def _generate_tables_job() -> None:
|
||||
"""Generate all entity string table C++ code as a FINAL-priority job.
|
||||
|
||||
Runs after all component to_code() calls have registered their strings.
|
||||
"""
|
||||
pool = _get_pool()
|
||||
parts = ["namespace esphome {"]
|
||||
for table_var, lookup_fn, attr in _CATEGORY_CONFIGS:
|
||||
code = _generate_category_code(table_var, lookup_fn, getattr(pool, attr))
|
||||
if code:
|
||||
parts.append(code)
|
||||
parts.append("} // namespace esphome")
|
||||
cg.add_global(RawStatement("\n".join(parts)))
|
||||
|
||||
|
||||
def _register_string(
|
||||
value: str, category: dict[str, int], max_count: int, category_name: str
|
||||
) -> int:
|
||||
"""Register a string in a category dict and return its 1-based index.
|
||||
|
||||
Returns 0 if value is empty/None (meaning "not set").
|
||||
"""
|
||||
if not value:
|
||||
return 0
|
||||
if value in category:
|
||||
return category[value]
|
||||
idx = len(category) + 1
|
||||
if idx > max_count:
|
||||
raise ValueError(
|
||||
f"Too many unique {category_name} values (max {max_count}), got {idx}: '{value}'"
|
||||
)
|
||||
category[value] = idx
|
||||
_ensure_tables_registered()
|
||||
return idx
|
||||
|
||||
|
||||
def register_device_class(value: str) -> int:
|
||||
"""Register a device_class string and return its 1-based index."""
|
||||
return _register_string(
|
||||
value, _get_pool().device_classes, _MAX_DEVICE_CLASSES, "device_class"
|
||||
)
|
||||
|
||||
|
||||
def register_unit_of_measurement(value: str) -> int:
|
||||
"""Register a unit_of_measurement string and return its 1-based index."""
|
||||
return _register_string(value, _get_pool().units, _MAX_UNITS, "unit_of_measurement")
|
||||
|
||||
|
||||
def register_icon(value: str) -> int:
|
||||
"""Register an icon string and return its 1-based index."""
|
||||
return _register_string(value, _get_pool().icons, _MAX_ICONS, "icon")
|
||||
|
||||
|
||||
def setup_device_class(config: ConfigType) -> None:
|
||||
"""Register config's device_class and store its index for finalize_entity_strings."""
|
||||
idx = register_device_class(config.get(CONF_DEVICE_CLASS, ""))
|
||||
if idx:
|
||||
cg.add_define("USE_ENTITY_DEVICE_CLASS")
|
||||
config[_KEY_DC_IDX] = idx
|
||||
|
||||
|
||||
def setup_unit_of_measurement(config: ConfigType) -> None:
|
||||
"""Register config's unit_of_measurement and store its index for finalize_entity_strings."""
|
||||
idx = register_unit_of_measurement(config.get(CONF_UNIT_OF_MEASUREMENT, ""))
|
||||
if idx:
|
||||
cg.add_define("USE_ENTITY_UNIT_OF_MEASUREMENT")
|
||||
config[_KEY_UOM_IDX] = idx
|
||||
|
||||
|
||||
def finalize_entity_strings(var: MockObj, config: ConfigType) -> None:
|
||||
"""Emit a single set_entity_strings() call with all packed indices.
|
||||
|
||||
Call this at the end of each component's setup function, after
|
||||
setup_entity() and any register_device_class/register_unit_of_measurement calls.
|
||||
"""
|
||||
dc_idx = config.get(_KEY_DC_IDX, 0)
|
||||
uom_idx = config.get(_KEY_UOM_IDX, 0)
|
||||
icon_idx = config.get(_KEY_ICON_IDX, 0)
|
||||
packed = (dc_idx << _DC_SHIFT) | (uom_idx << _UOM_SHIFT) | (icon_idx << _ICON_SHIFT)
|
||||
if packed != 0:
|
||||
add(var.set_entity_strings(packed))
|
||||
|
||||
|
||||
def get_base_entity_object_id(
|
||||
name: str, friendly_name: str | None, device_name: str | None = None
|
||||
@@ -64,8 +236,48 @@ def get_base_entity_object_id(
|
||||
return sanitize(snake_case(base_str))
|
||||
|
||||
|
||||
async def setup_entity(var: MockObj, config: ConfigType, platform: str) -> None:
|
||||
"""Set up generic properties of an Entity.
|
||||
def setup_entity(var_or_platform, config=None, platform=None):
|
||||
"""Set up entity properties — works as both decorator and direct call.
|
||||
|
||||
Decorator mode::
|
||||
|
||||
@setup_entity("sensor")
|
||||
async def setup_sensor_core_(var, config):
|
||||
setup_device_class(config)
|
||||
setup_unit_of_measurement(config)
|
||||
...
|
||||
|
||||
Direct call mode (for entities with no extra string properties)::
|
||||
|
||||
await setup_entity(var, config, "camera")
|
||||
"""
|
||||
if isinstance(var_or_platform, str) and config is None:
|
||||
# Decorator mode: @setup_entity("sensor")
|
||||
platform = var_or_platform
|
||||
|
||||
def decorator(func: Callable) -> Callable:
|
||||
@functools.wraps(func)
|
||||
async def wrapper(
|
||||
var: MockObj, config: ConfigType, *args, **kwargs
|
||||
) -> None:
|
||||
await _setup_entity_impl(var, config, platform)
|
||||
await func(var, config, *args, **kwargs)
|
||||
finalize_entity_strings(var, config)
|
||||
|
||||
return wrapper
|
||||
|
||||
return decorator
|
||||
|
||||
# Direct call mode: await setup_entity(var, config, "camera")
|
||||
async def _do() -> None:
|
||||
await _setup_entity_impl(var_or_platform, config, platform)
|
||||
finalize_entity_strings(var_or_platform, config)
|
||||
|
||||
return _do()
|
||||
|
||||
|
||||
async def _setup_entity_impl(var: MockObj, config: ConfigType, platform: str) -> None:
|
||||
"""Set up generic properties of an Entity (internal implementation).
|
||||
|
||||
This function sets up the common entity properties like name, icon,
|
||||
entity category, etc.
|
||||
@@ -92,12 +304,15 @@ async def setup_entity(var: MockObj, config: ConfigType, platform: str) -> None:
|
||||
add(var.set_disabled_by_default(True))
|
||||
if CONF_INTERNAL in config:
|
||||
add(var.set_internal(config[CONF_INTERNAL]))
|
||||
icon_idx = 0
|
||||
if CONF_ICON in config:
|
||||
# Add USE_ENTITY_ICON define when icons are used
|
||||
cg.add_define("USE_ENTITY_ICON")
|
||||
add(var.set_icon(config[CONF_ICON]))
|
||||
icon_idx = register_icon(config[CONF_ICON])
|
||||
if CONF_ENTITY_CATEGORY in config:
|
||||
add(var.set_entity_category(config[CONF_ENTITY_CATEGORY]))
|
||||
# Store icon index for finalize_entity_strings
|
||||
config[_KEY_ICON_IDX] = icon_idx
|
||||
|
||||
|
||||
def inherit_property_from(property_to_inherit, parent_id_property, transform=None):
|
||||
|
||||
@@ -42,6 +42,7 @@ void arch_feed_wdt();
|
||||
uint32_t arch_get_cpu_cycle_count();
|
||||
uint32_t arch_get_cpu_freq_hz();
|
||||
uint8_t progmem_read_byte(const uint8_t *addr);
|
||||
const char *progmem_read_ptr(const char *const *addr);
|
||||
uint16_t progmem_read_uint16(const uint16_t *addr);
|
||||
|
||||
} // namespace esphome
|
||||
|
||||
@@ -706,7 +706,7 @@ float gamma_correct(float value, float gamma) {
|
||||
if (gamma <= 0.0f)
|
||||
return value;
|
||||
|
||||
return powf(value, gamma);
|
||||
return powf(value, gamma); // NOLINT - deprecated, removal 2026.9.0
|
||||
}
|
||||
float gamma_uncorrect(float value, float gamma) {
|
||||
if (value <= 0.0f)
|
||||
@@ -714,7 +714,7 @@ float gamma_uncorrect(float value, float gamma) {
|
||||
if (gamma <= 0.0f)
|
||||
return value;
|
||||
|
||||
return powf(value, 1 / gamma);
|
||||
return powf(value, 1 / gamma); // NOLINT - deprecated, removal 2026.9.0
|
||||
}
|
||||
|
||||
void rgb_to_hsv(float red, float green, float blue, int &hue, float &saturation, float &value) {
|
||||
@@ -794,7 +794,6 @@ void HighFrequencyLoopRequester::stop() {
|
||||
num_requests--;
|
||||
this->started_ = false;
|
||||
}
|
||||
bool HighFrequencyLoopRequester::is_high_frequency() { return num_requests > 0; }
|
||||
|
||||
std::string get_mac_address() {
|
||||
uint8_t mac[6];
|
||||
|
||||
@@ -512,8 +512,8 @@ template<size_t STACK_SIZE, typename T = uint8_t> class SmallBufferWithHeapFallb
|
||||
///@{
|
||||
|
||||
/// Compute 10^exp using iterative multiplication/division.
|
||||
/// Avoids pulling in powf/__ieee754_powf (~2.3KB flash) for small integer exponents.
|
||||
/// Matches powf(10, exp) for the int8_t exponent range used by sensor accuracy_decimals.
|
||||
/// Avoids pulling in powf/__ieee754_powf (~2.3KB flash) for small integer exponents. // NOLINT
|
||||
/// Matches powf(10, exp) for the int8_t exponent range used by sensor accuracy_decimals. // NOLINT
|
||||
inline float pow10_int(int8_t exp) {
|
||||
float result = 1.0f;
|
||||
if (exp >= 0) {
|
||||
@@ -599,6 +599,44 @@ template<std::integral T> constexpr uint32_t fnv1a_hash_extend(uint32_t hash, T
|
||||
constexpr uint32_t fnv1a_hash(const char *str) { return fnv1a_hash_extend(FNV1_OFFSET_BASIS, str); }
|
||||
inline uint32_t fnv1a_hash(const std::string &str) { return fnv1a_hash(str.c_str()); }
|
||||
|
||||
/// Convert a 64-bit microsecond count to milliseconds without calling
|
||||
/// __udivdi3 (software 64-bit divide, ~1200 ns on Xtensa @ 240 MHz).
|
||||
///
|
||||
/// Returns uint32_t by default (for millis()), or uint64_t when requested
|
||||
/// (for millis_64()). The only difference is whether hi * Q is truncated
|
||||
/// to 32 bits or widened to 64.
|
||||
///
|
||||
/// On 32-bit targets, GCC does not optimize 64-bit constant division into a
|
||||
/// multiply-by-reciprocal. Since 1000 = 8 * 125, we first right-shift by 3
|
||||
/// (free divide-by-8), then use the Euclidean division identity to decompose
|
||||
/// the remaining 64-bit divide-by-125 into a single 32-bit division:
|
||||
///
|
||||
/// floor(us / 1000) = floor(floor(us / 8) / 125) [exact for integers]
|
||||
/// 2^32 = Q * 125 + R (34359738 * 125 + 46)
|
||||
/// (hi * 2^32 + lo) / 125 = hi * Q + (hi * R + lo) / 125
|
||||
///
|
||||
/// GCC optimizes the remaining 32-bit "/ 125U" into a multiply-by-reciprocal
|
||||
/// (mulhu + shift), so no division instruction is emitted.
|
||||
///
|
||||
/// Safe for us up to ~3.2e18 (~101,700 years of microseconds).
|
||||
///
|
||||
/// See: https://en.wikipedia.org/wiki/Euclidean_division
|
||||
/// See: https://ridiculousfish.com/blog/posts/labor-of-division-episode-iii.html
|
||||
template<typename ReturnT = uint32_t> inline constexpr ESPHOME_ALWAYS_INLINE ReturnT micros_to_millis(uint64_t us) {
|
||||
constexpr uint32_t d = 125U;
|
||||
constexpr uint32_t q = static_cast<uint32_t>((1ULL << 32) / d); // 34359738
|
||||
constexpr uint32_t r = static_cast<uint32_t>((1ULL << 32) % d); // 46
|
||||
// 1000 = 8 * 125; divide-by-8 is a free shift
|
||||
uint64_t x = us >> 3;
|
||||
uint32_t lo = static_cast<uint32_t>(x);
|
||||
uint32_t hi = static_cast<uint32_t>(x >> 32);
|
||||
// Combine remainder term: hi * (2^32 % 125) + lo
|
||||
uint32_t adj = hi * r + lo;
|
||||
// If adj overflowed, the true value is 2^32 + adj; apply the identity again
|
||||
// static_cast<ReturnT>(hi) widens to 64-bit when ReturnT=uint64_t, preserving upper bits of hi*q
|
||||
return static_cast<ReturnT>(hi) * q + (adj < lo ? (adj + r) / d + q : adj / d);
|
||||
}
|
||||
|
||||
/// Return a random 32-bit unsigned integer.
|
||||
uint32_t random_uint32();
|
||||
/// Return a random float between 0 and 1.
|
||||
@@ -1694,7 +1732,7 @@ class HighFrequencyLoopRequester {
|
||||
void stop();
|
||||
|
||||
/// Check whether the loop is running continuously.
|
||||
static bool is_high_frequency();
|
||||
static bool is_high_frequency() { return num_requests > 0; }
|
||||
|
||||
protected:
|
||||
bool started_{false};
|
||||
|
||||
@@ -140,8 +140,10 @@
|
||||
_Static_assert(sizeof(TaskHandle_t) <= 4, "TaskHandle_t must be <= 4 bytes for atomic access");
|
||||
_Static_assert(sizeof(netconn_callback) <= 4, "netconn_callback must be <= 4 bytes for atomic access");
|
||||
|
||||
// rcvevent must fit in a single atomic read
|
||||
_Static_assert(sizeof(((struct lwip_sock *) 0)->rcvevent) <= 4, "rcvevent must be <= 4 bytes for atomic access");
|
||||
// rcvevent must be exactly 2 bytes (s16_t) — the inline in lwip_fast_select.h reads it as int16_t.
|
||||
// If lwIP changes this to int or similar, the offset assert would still pass but the load width would be wrong.
|
||||
_Static_assert(sizeof(((struct lwip_sock *) 0)->rcvevent) == 2,
|
||||
"rcvevent size changed — update int16_t cast in esphome_lwip_socket_has_data() in lwip_fast_select.h");
|
||||
|
||||
// Struct member alignment — natural alignment guarantees atomicity on Xtensa/RISC-V/ARM.
|
||||
// Misaligned access would not be atomic even if the size is <= 4 bytes.
|
||||
@@ -150,6 +152,10 @@ _Static_assert(offsetof(struct netconn, callback) % sizeof(netconn_callback) ==
|
||||
_Static_assert(offsetof(struct lwip_sock, rcvevent) % sizeof(((struct lwip_sock *) 0)->rcvevent) == 0,
|
||||
"lwip_sock.rcvevent must be naturally aligned for atomic access");
|
||||
|
||||
// Verify the hardcoded offset used in the header's inline esphome_lwip_socket_has_data().
|
||||
_Static_assert(offsetof(struct lwip_sock, rcvevent) == ESPHOME_LWIP_SOCK_RCVEVENT_OFFSET,
|
||||
"lwip_sock.rcvevent offset changed — update ESPHOME_LWIP_SOCK_RCVEVENT_OFFSET in lwip_fast_select.h");
|
||||
|
||||
// Task handle for the main loop — written once in init(), read from TCP/IP and background tasks.
|
||||
static TaskHandle_t s_main_loop_task = NULL;
|
||||
|
||||
@@ -194,23 +200,11 @@ static inline struct lwip_sock *get_sock(int fd) {
|
||||
return sock;
|
||||
}
|
||||
|
||||
bool esphome_lwip_socket_has_data(int fd) {
|
||||
struct lwip_sock *sock = get_sock(fd);
|
||||
if (sock == NULL)
|
||||
return false;
|
||||
// volatile prevents the compiler from caching/reordering this cross-thread read.
|
||||
// The write side (TCP/IP thread) commits via SYS_ARCH_UNPROTECT which releases a
|
||||
// FreeRTOS mutex (ESP32) or resumes the scheduler (LibreTiny), ensuring the value
|
||||
// is visible. Aligned 16-bit reads are single-instruction loads (L16SI/LH/LDRH) on
|
||||
// Xtensa/RISC-V/ARM and cannot produce torn values.
|
||||
return *(volatile s16_t *) &sock->rcvevent > 0;
|
||||
struct lwip_sock *esphome_lwip_get_sock(int fd) {
|
||||
return get_sock(fd);
|
||||
}
|
||||
|
||||
void esphome_lwip_hook_socket(int fd) {
|
||||
struct lwip_sock *sock = get_sock(fd);
|
||||
if (sock == NULL)
|
||||
return;
|
||||
|
||||
void esphome_lwip_hook_socket(struct lwip_sock *sock) {
|
||||
// Save original callback once — all LwIP sockets share the same static event_callback
|
||||
// (DEFAULT_SOCKET_EVENTCB in sockets.c, used for SOCK_RAW, SOCK_DGRAM, and SOCK_STREAM).
|
||||
if (s_original_callback == NULL) {
|
||||
|
||||
@@ -4,6 +4,17 @@
|
||||
// Replaces lwip_select() with direct rcvevent reads and FreeRTOS task notifications.
|
||||
|
||||
#include <stdbool.h>
|
||||
#include <stdint.h>
|
||||
|
||||
// Forward declare lwip_sock for C++ callers that store cached pointers.
|
||||
// The full definition is only available in the .c file (lwip/priv/sockets_priv.h
|
||||
// conflicts with C++ compilation units).
|
||||
struct lwip_sock;
|
||||
|
||||
// Byte offset of rcvevent (s16_t) within struct lwip_sock.
|
||||
// Verified at compile time in lwip_fast_select.c via _Static_assert.
|
||||
// Anonymous enum for a compile-time constant that works in both C and C++.
|
||||
enum { ESPHOME_LWIP_SOCK_RCVEVENT_OFFSET = 8 };
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
@@ -13,16 +24,38 @@ extern "C" {
|
||||
/// Saves the current task handle for xTaskNotifyGive() wake notifications.
|
||||
void esphome_lwip_fast_select_init(void);
|
||||
|
||||
/// Check if a LwIP socket has data ready via direct rcvevent read (~215 ns per socket).
|
||||
/// Uses lwip_socket_dbg_get_socket() — a direct array lookup without the refcount that
|
||||
/// get_socket()/done_socket() uses. Safe because the caller owns the socket lifetime:
|
||||
/// both has_data reads and socket close/unregister happen on the main loop thread.
|
||||
bool esphome_lwip_socket_has_data(int fd);
|
||||
/// Look up a LwIP socket struct from a file descriptor.
|
||||
/// Returns NULL if fd is invalid or the socket/netconn is not initialized.
|
||||
/// Use this at registration time to cache the pointer for esphome_lwip_socket_has_data().
|
||||
struct lwip_sock *esphome_lwip_get_sock(int fd);
|
||||
|
||||
/// Check if a cached LwIP socket has data ready via unlocked hint read of rcvevent.
|
||||
/// This avoids lwIP core lock contention between the main loop (CPU0) and
|
||||
/// streaming/networking work (CPU1). Correctness is preserved because callers
|
||||
/// already handle EWOULDBLOCK on nonblocking sockets — a stale hint simply causes
|
||||
/// a harmless retry on the next loop iteration. In practice, stale reads have not
|
||||
/// been observed across multi-day testing, but the design does not depend on that.
|
||||
///
|
||||
/// The sock pointer must have been obtained from esphome_lwip_get_sock() and must
|
||||
/// remain valid (caller owns socket lifetime — no concurrent close).
|
||||
/// Hot path: inlined volatile 16-bit load — no function call overhead.
|
||||
/// Uses offset-based access because lwip/priv/sockets_priv.h conflicts with C++.
|
||||
/// The offset and size are verified at compile time in lwip_fast_select.c.
|
||||
static inline bool esphome_lwip_socket_has_data(struct lwip_sock *sock) {
|
||||
// Unlocked hint read — no lwIP core lock needed.
|
||||
// volatile prevents the compiler from caching/reordering this cross-thread read.
|
||||
// The write side (TCP/IP thread) commits via SYS_ARCH_UNPROTECT which releases a
|
||||
// FreeRTOS mutex (ESP32) or resumes the scheduler (LibreTiny), ensuring the value
|
||||
// is visible. Aligned 16-bit reads are single-instruction loads (L16SI/LH/LDRH) on
|
||||
// Xtensa/RISC-V/ARM and cannot produce torn values.
|
||||
return *(volatile int16_t *) ((char *) sock + (int) ESPHOME_LWIP_SOCK_RCVEVENT_OFFSET) > 0;
|
||||
}
|
||||
|
||||
/// Hook a socket's netconn callback to notify the main loop task on receive events.
|
||||
/// Wraps the original event_callback with one that also calls xTaskNotifyGive().
|
||||
/// Must be called from the main loop after socket creation.
|
||||
void esphome_lwip_hook_socket(int fd);
|
||||
/// The sock pointer must have been obtained from esphome_lwip_get_sock().
|
||||
void esphome_lwip_hook_socket(struct lwip_sock *sock);
|
||||
|
||||
/// Wake the main loop task from another FreeRTOS task — costs <1 us.
|
||||
/// NOT ISR-safe — must only be called from task context.
|
||||
|
||||
@@ -841,6 +841,54 @@ def lint_no_scanf(fname, match):
|
||||
)
|
||||
|
||||
|
||||
# Base entity platforms - these are linked into most builds and should not
|
||||
# pull in powf/__ieee754_powf (~2.3KB flash).
|
||||
BASE_ENTITY_PLATFORMS = [
|
||||
"alarm_control_panel",
|
||||
"binary_sensor",
|
||||
"button",
|
||||
"climate",
|
||||
"cover",
|
||||
"datetime",
|
||||
"event",
|
||||
"fan",
|
||||
"light",
|
||||
"lock",
|
||||
"media_player",
|
||||
"number",
|
||||
"select",
|
||||
"sensor",
|
||||
"switch",
|
||||
"text",
|
||||
"text_sensor",
|
||||
"update",
|
||||
"valve",
|
||||
"water_heater",
|
||||
]
|
||||
|
||||
# Directories protected from powf: core + all base entity platforms
|
||||
POWF_PROTECTED_DIRS = ["esphome/core"] + [
|
||||
f"esphome/components/{p}" for p in BASE_ENTITY_PLATFORMS
|
||||
]
|
||||
|
||||
|
||||
@lint_re_check(
|
||||
r"[^\w]powf\s*\(" + CPP_RE_EOL,
|
||||
include=[
|
||||
f"{d}/*.{ext}" for d in POWF_PROTECTED_DIRS for ext in ["h", "cpp", "tcc"]
|
||||
],
|
||||
)
|
||||
def lint_no_powf_in_core(fname, match):
|
||||
return (
|
||||
f"{highlight('powf()')} pulls in __ieee754_powf (~2.3KB flash) and is not allowed in "
|
||||
f"core or base entity platform code. These files are linked into every build.\n"
|
||||
f"Please use alternatives:\n"
|
||||
f" - {highlight('pow10_int(exp)')} for integer powers of 10 (from helpers.h)\n"
|
||||
f" - Precomputed lookup tables for gamma/non-integer exponents\n"
|
||||
f"(If powf is strictly necessary, add `// NOLINT` to the line)"
|
||||
)
|
||||
|
||||
|
||||
LOG_MULTILINE_RE = re.compile(r"ESP_LOG\w+\s*\(.*?;", re.DOTALL)
|
||||
LOG_BAD_CONTINUATION_RE = re.compile(r'\\n(?:[^ \\"\r\n\t]|"\s*\n\s*"[^ \\])')
|
||||
LOG_PERCENT_S_CONTINUATION_RE = re.compile(r'\\n(?:%s|"\s*\n\s*"%s)')
|
||||
|
||||
@@ -79,6 +79,7 @@ def clang_options(idedata):
|
||||
"-Dpgm_read_byte_near(s)=(*(const uint8_t *)(s))",
|
||||
"-Dpgm_read_word(s)=(*(const uint16_t *)(s))",
|
||||
"-Dpgm_read_dword(s)=(*(const uint32_t *)(s))",
|
||||
"-Dpgm_read_ptr(s)=(*(const void *const *)(s))",
|
||||
"-DPROGMEM=",
|
||||
"-DPGM_P=const char *",
|
||||
"-DPSTR(s)=(s)",
|
||||
|
||||
@@ -11,4 +11,4 @@ def test_sensor_device_class_set(generate_main):
|
||||
main_cpp = generate_main("tests/component_tests/sensor/test_sensor.yaml")
|
||||
|
||||
# Then
|
||||
assert 's_1->set_device_class("voltage");' in main_cpp
|
||||
assert "s_1->set_entity_strings(" in main_cpp
|
||||
|
||||
@@ -54,5 +54,5 @@ def test_text_sensor_device_class_set(generate_main):
|
||||
main_cpp = generate_main("tests/component_tests/text_sensor/test_text_sensor.yaml")
|
||||
|
||||
# Then
|
||||
assert 'ts_2->set_device_class("timestamp");' in main_cpp
|
||||
assert 'ts_3->set_device_class("date");' in main_cpp
|
||||
assert "ts_2->set_entity_strings(" in main_cpp
|
||||
assert "ts_3->set_entity_strings(" in main_cpp
|
||||
|
||||
@@ -27,3 +27,14 @@ globals:
|
||||
type: bool
|
||||
restore_value: false
|
||||
initial_value: "false"
|
||||
# Test restore_value with string "false" - should be converted to bool false
|
||||
- id: glob_no_restore_string_false
|
||||
type: int
|
||||
restore_value: "false"
|
||||
initial_value: "42"
|
||||
# Test restore_value with string "true" - should be converted to bool true
|
||||
- id: glob_restore_string_true
|
||||
type: int
|
||||
restore_value: "true"
|
||||
initial_value: "99"
|
||||
update_interval: 5s
|
||||
|
||||
@@ -13,3 +13,4 @@ openthread:
|
||||
force_dataset: true
|
||||
use_address: open-thread-test.local
|
||||
poll_period: 20sec
|
||||
output_power: 1dBm
|
||||
|
||||
18
tests/components/speaker/common-media_player_off_on.yaml
Normal file
18
tests/components/speaker/common-media_player_off_on.yaml
Normal file
@@ -0,0 +1,18 @@
|
||||
<<: !include common.yaml
|
||||
|
||||
media_player:
|
||||
- platform: speaker
|
||||
id: speaker_media_player_id
|
||||
announcement_pipeline:
|
||||
speaker: speaker_id
|
||||
buffer_size: 1000000
|
||||
volume_increment: 0.02
|
||||
volume_max: 0.95
|
||||
volume_min: 0.0
|
||||
task_stack_in_psram: true
|
||||
on_turn_on:
|
||||
then:
|
||||
- logger.log: "Turn On Media Player"
|
||||
on_turn_off:
|
||||
then:
|
||||
- logger.log: "Turn Off Media Player"
|
||||
@@ -0,0 +1,9 @@
|
||||
substitutions:
|
||||
scl_pin: GPIO16
|
||||
sda_pin: GPIO17
|
||||
i2s_bclk_pin: GPIO27
|
||||
i2s_lrclk_pin: GPIO26
|
||||
i2s_mclk_pin: GPIO25
|
||||
i2s_dout_pin: GPIO23
|
||||
|
||||
<<: !include common-media_player_off_on.yaml
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user