mirror of
https://github.com/esphome/esphome.git
synced 2026-07-04 06:03:35 +00:00
Compare commits
44 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 5e881738da | |||
| 5a250cc74f | |||
| 02f828fcbf | |||
| ab64916c37 | |||
| 5608aa10a5 | |||
| daa68a2a60 | |||
| 8754bbfa89 | |||
| 6d92cc3d2b | |||
| 2f684bf4f3 | |||
| 45af21bf38 | |||
| e6318a2d16 | |||
| bef4c8a86c | |||
| 6e67864510 | |||
| c2af4874f9 | |||
| 2001b91280 | |||
| 5460ee7edd | |||
| 40081e5ae7 | |||
| a7c5b0ab46 | |||
| e1a813e11f | |||
| 1dfeef0265 | |||
| 395610c117 | |||
| ae96f82b82 | |||
| 2c610abcd0 | |||
| d3591c8d9e | |||
| ec420d5792 | |||
| 17209df7b5 | |||
| 9cf9b02ba2 | |||
| c90fa2378a | |||
| c04dfa922e | |||
| 668007707d | |||
| ab71f5276f | |||
| d062f62656 | |||
| 03db32d045 | |||
| 8f6d489a9a | |||
| dd07fba943 | |||
| 6f5d642a31 | |||
| 2721f08bcc | |||
| eafc5df3f2 | |||
| 46d0c29be5 | |||
| abdbbf4dd2 | |||
| 4dc0599a7d | |||
| 52c35ec09c | |||
| 76490e45bc | |||
| 0a8130858c |
+1
-1
@@ -1 +1 @@
|
||||
10c432ae818f9ed7fd4a0176a04467b1f2634363f5ec985045a6d72747f60b90
|
||||
d48687d988ae2a94a9973226df773478a7db1d52133545f07aa05e34fc678dcf
|
||||
|
||||
@@ -47,7 +47,7 @@ runs:
|
||||
|
||||
- name: Build and push to ghcr by digest
|
||||
id: build-ghcr
|
||||
uses: docker/build-push-action@d08e5c354a6adb9ed34480a06d141179aa583294 # v7.0.0
|
||||
uses: docker/build-push-action@bcafcacb16a39f128d818304e6c9c0c18556b85f # v7.1.0
|
||||
env:
|
||||
DOCKER_BUILD_SUMMARY: false
|
||||
DOCKER_BUILD_RECORD_UPLOAD: false
|
||||
@@ -73,7 +73,7 @@ runs:
|
||||
|
||||
- name: Build and push to dockerhub by digest
|
||||
id: build-dockerhub
|
||||
uses: docker/build-push-action@d08e5c354a6adb9ed34480a06d141179aa583294 # v7.0.0
|
||||
uses: docker/build-push-action@bcafcacb16a39f128d818304e6c9c0c18556b85f # v7.1.0
|
||||
env:
|
||||
DOCKER_BUILD_SUMMARY: false
|
||||
DOCKER_BUILD_RECORD_UPLOAD: false
|
||||
|
||||
@@ -4,6 +4,7 @@ module.exports = {
|
||||
CODEOWNERS_MARKER: '<!-- codeowners-request -->',
|
||||
TOO_BIG_MARKER: '<!-- too-big-request -->',
|
||||
DEPRECATED_COMPONENT_MARKER: '<!-- deprecated-component-request -->',
|
||||
ORG_FORK_MARKER: '<!-- maintainer-access-warning -->',
|
||||
|
||||
MANAGED_LABELS: [
|
||||
'new-component',
|
||||
|
||||
@@ -281,6 +281,24 @@ async function detectDeprecatedComponents(github, context, changedFiles) {
|
||||
return { labels, deprecatedInfo };
|
||||
}
|
||||
|
||||
// Strategy: Detect when maintainers cannot modify the PR branch
|
||||
function detectMaintainerAccess(context) {
|
||||
const pr = context.payload.pull_request;
|
||||
|
||||
// Only relevant for cross-repo PRs (forks)
|
||||
if (!pr.head.repo || pr.head.repo.full_name === pr.base.repo.full_name) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (pr.maintainer_can_modify) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const isOrgFork = pr.head.repo.owner.type === 'Organization';
|
||||
console.log(`Maintainer cannot modify PR branch (${isOrgFork ? 'org fork: ' + pr.head.repo.owner.login : 'user disabled'})`);
|
||||
return { isOrgFork, orgName: pr.head.repo.owner.login };
|
||||
}
|
||||
|
||||
// Strategy: Requirements detection
|
||||
async function detectRequirements(allLabels, prFiles, context) {
|
||||
const labels = new Set();
|
||||
@@ -329,5 +347,6 @@ module.exports = {
|
||||
detectTests,
|
||||
detectPRTemplateCheckboxes,
|
||||
detectDeprecatedComponents,
|
||||
detectMaintainerAccess,
|
||||
detectRequirements
|
||||
};
|
||||
|
||||
@@ -12,9 +12,10 @@ const {
|
||||
detectTests,
|
||||
detectPRTemplateCheckboxes,
|
||||
detectDeprecatedComponents,
|
||||
detectMaintainerAccess,
|
||||
detectRequirements
|
||||
} = require('./detectors');
|
||||
const { handleReviews } = require('./reviews');
|
||||
const { handleReviews, handleMaintainerAccessComment } = require('./reviews');
|
||||
const { applyLabels, removeOldLabels } = require('./labels');
|
||||
|
||||
// Fetch API data
|
||||
@@ -114,7 +115,8 @@ module.exports = async ({ github, context }) => {
|
||||
codeOwnerLabels,
|
||||
testLabels,
|
||||
checkboxLabels,
|
||||
deprecatedResult
|
||||
deprecatedResult,
|
||||
maintainerAccess
|
||||
] = await Promise.all([
|
||||
detectMergeBranch(context),
|
||||
detectComponentPlatforms(changedFiles, apiData),
|
||||
@@ -127,7 +129,8 @@ module.exports = async ({ github, context }) => {
|
||||
detectCodeOwner(github, context, changedFiles),
|
||||
detectTests(changedFiles),
|
||||
detectPRTemplateCheckboxes(context),
|
||||
detectDeprecatedComponents(github, context, changedFiles)
|
||||
detectDeprecatedComponents(github, context, changedFiles),
|
||||
detectMaintainerAccess(context)
|
||||
]);
|
||||
|
||||
// Extract deprecated component info
|
||||
@@ -177,8 +180,11 @@ module.exports = async ({ github, context }) => {
|
||||
|
||||
console.log('Computed labels:', finalLabels.join(', '));
|
||||
|
||||
// Handle reviews
|
||||
await handleReviews(github, context, finalLabels, originalLabelCount, deprecatedInfo, prFiles, totalAdditions, totalDeletions, MAX_LABELS, TOO_BIG_THRESHOLD);
|
||||
// Handle reviews and org fork comment
|
||||
await Promise.all([
|
||||
handleReviews(github, context, finalLabels, originalLabelCount, deprecatedInfo, prFiles, totalAdditions, totalDeletions, MAX_LABELS, TOO_BIG_THRESHOLD),
|
||||
handleMaintainerAccessComment(github, context, maintainerAccess)
|
||||
]);
|
||||
|
||||
// Apply labels
|
||||
await applyLabels(github, context, finalLabels);
|
||||
|
||||
@@ -2,7 +2,8 @@ const {
|
||||
BOT_COMMENT_MARKER,
|
||||
CODEOWNERS_MARKER,
|
||||
TOO_BIG_MARKER,
|
||||
DEPRECATED_COMPONENT_MARKER
|
||||
DEPRECATED_COMPONENT_MARKER,
|
||||
ORG_FORK_MARKER
|
||||
} = require('./constants');
|
||||
|
||||
// Generate review messages
|
||||
@@ -136,6 +137,63 @@ async function handleReviews(github, context, finalLabels, originalLabelCount, d
|
||||
}
|
||||
}
|
||||
|
||||
// Handle maintainer access warning comment
|
||||
async function handleMaintainerAccessComment(github, context, maintainerAccess) {
|
||||
if (!maintainerAccess) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { owner, repo } = context.repo;
|
||||
const pr_number = context.issue.number;
|
||||
const prAuthor = context.payload.pull_request.user.login;
|
||||
|
||||
// Check if we already posted the warning (iterate pages to exit early)
|
||||
let existingComment;
|
||||
for await (const { data: comments } of github.paginate.iterator(
|
||||
github.rest.issues.listComments,
|
||||
{ owner, repo, issue_number: pr_number }
|
||||
)) {
|
||||
existingComment = comments.find(comment =>
|
||||
comment.user.type === 'Bot' &&
|
||||
comment.body && comment.body.includes(ORG_FORK_MARKER)
|
||||
);
|
||||
if (existingComment) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (existingComment) {
|
||||
console.log('Maintainer access warning comment already exists, skipping');
|
||||
return;
|
||||
}
|
||||
|
||||
let body;
|
||||
if (maintainerAccess.isOrgFork) {
|
||||
body = `${ORG_FORK_MARKER}\n### ⚠️ Organization Fork Detected\n\n` +
|
||||
`Hey there @${prAuthor},\n` +
|
||||
`It looks like this PR was submitted from a fork owned by the **${maintainerAccess.orgName}** organization. ` +
|
||||
`GitHub does not allow maintainers to push changes to pull request branches when the fork is owned by an organization. ` +
|
||||
`This means we won't be able to make small adjustments or fixups to your PR directly.\n\n` +
|
||||
`To allow maintainer collaboration, please re-submit this PR from a personal fork instead.\n\n` +
|
||||
`See: [Setting up the local repository](https://developers.esphome.io/contributing/development-environment/?h=org#set-up-the-local-repository) for more details.`;
|
||||
} else {
|
||||
body = `${ORG_FORK_MARKER}\n### ⚠️ Maintainer Access Disabled\n\n` +
|
||||
`Hey there @${prAuthor},\n` +
|
||||
`It looks like this PR does not have the "Allow edits from maintainers" option enabled. ` +
|
||||
`This means we won't be able to make small adjustments or fixups to your PR directly.\n\n` +
|
||||
`Please enable this option in the PR sidebar to allow maintainer collaboration.`;
|
||||
}
|
||||
|
||||
await github.rest.issues.createComment({
|
||||
owner,
|
||||
repo,
|
||||
issue_number: pr_number,
|
||||
body
|
||||
});
|
||||
console.log('Created maintainer access warning comment');
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
handleReviews
|
||||
handleReviews,
|
||||
handleMaintainerAccessComment
|
||||
};
|
||||
|
||||
@@ -20,7 +20,7 @@ env:
|
||||
jobs:
|
||||
label:
|
||||
runs-on: ubuntu-latest
|
||||
if: github.event.action != 'labeled' || github.event.sender.type != 'Bot'
|
||||
if: github.event.pull_request.state == 'open' && (github.event.action != 'labeled' || github.event.sender.type != 'Bot')
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
@@ -33,7 +33,7 @@ jobs:
|
||||
private-key: ${{ secrets.ESPHOME_GITHUB_APP_PRIVATE_KEY }}
|
||||
|
||||
- name: Auto Label PR
|
||||
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
|
||||
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
|
||||
with:
|
||||
github-token: ${{ steps.generate-token.outputs.token }}
|
||||
script: |
|
||||
|
||||
@@ -47,7 +47,7 @@ jobs:
|
||||
fi
|
||||
- if: failure()
|
||||
name: Review PR
|
||||
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
|
||||
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
|
||||
with:
|
||||
script: |
|
||||
await github.rest.pulls.createReview({
|
||||
@@ -62,7 +62,7 @@ jobs:
|
||||
run: git diff
|
||||
- if: failure()
|
||||
name: Archive artifacts
|
||||
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
with:
|
||||
name: generated-proto-files
|
||||
path: |
|
||||
@@ -70,7 +70,7 @@ jobs:
|
||||
esphome/components/api/api_pb2_service.*
|
||||
- if: success()
|
||||
name: Dismiss review
|
||||
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
|
||||
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
|
||||
with:
|
||||
script: |
|
||||
let reviews = await github.rest.pulls.listReviews({
|
||||
|
||||
@@ -42,7 +42,7 @@ jobs:
|
||||
|
||||
- if: failure() && github.event.pull_request.head.repo.full_name == github.repository
|
||||
name: Request changes
|
||||
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
|
||||
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
|
||||
with:
|
||||
script: |
|
||||
await github.rest.pulls.createReview({
|
||||
@@ -55,7 +55,7 @@ jobs:
|
||||
|
||||
- if: success() && github.event.pull_request.head.repo.full_name == github.repository
|
||||
name: Dismiss review
|
||||
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
|
||||
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
|
||||
with:
|
||||
script: |
|
||||
let reviews = await github.rest.pulls.listReviews({
|
||||
|
||||
@@ -868,7 +868,8 @@ jobs:
|
||||
python script/test_build_components.py \
|
||||
-e compile \
|
||||
-c "$component_list" \
|
||||
-t "$platform" 2>&1 | \
|
||||
-t "$platform" \
|
||||
--base-only 2>&1 | \
|
||||
tee /dev/stderr | \
|
||||
python script/ci_memory_impact_extract.py \
|
||||
--output-env \
|
||||
@@ -903,7 +904,7 @@ jobs:
|
||||
fi
|
||||
|
||||
- name: Upload memory analysis JSON
|
||||
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
with:
|
||||
name: memory-analysis-target
|
||||
path: memory-analysis-target.json
|
||||
@@ -954,7 +955,8 @@ jobs:
|
||||
python script/test_build_components.py \
|
||||
-e compile \
|
||||
-c "$component_list" \
|
||||
-t "$platform" 2>&1 | \
|
||||
-t "$platform" \
|
||||
--base-only 2>&1 | \
|
||||
tee /dev/stderr | \
|
||||
python script/ci_memory_impact_extract.py \
|
||||
--output-env \
|
||||
@@ -967,7 +969,7 @@ jobs:
|
||||
--platform "$platform"
|
||||
|
||||
- name: Upload memory analysis JSON
|
||||
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
with:
|
||||
name: memory-analysis-pr
|
||||
path: memory-analysis-pr.json
|
||||
|
||||
@@ -34,7 +34,7 @@ jobs:
|
||||
CODEOWNERS
|
||||
|
||||
- name: Check codeowner approval and update label
|
||||
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
|
||||
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
|
||||
env:
|
||||
PR_NUMBER: ${{ github.event.pull_request.number }}
|
||||
with:
|
||||
|
||||
@@ -33,7 +33,7 @@ jobs:
|
||||
ref: ${{ github.event.pull_request.base.sha }}
|
||||
|
||||
- name: Request reviews from component codeowners
|
||||
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
|
||||
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
|
||||
with:
|
||||
script: |
|
||||
const { loadCodeowners, getEffectiveOwners } = require('./.github/scripts/codeowners.js');
|
||||
|
||||
@@ -15,7 +15,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Add external component comment
|
||||
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
|
||||
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
|
||||
with:
|
||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
script: |
|
||||
|
||||
@@ -19,7 +19,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Notify codeowners for component issues
|
||||
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
|
||||
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
|
||||
with:
|
||||
script: |
|
||||
const owner = context.repo.owner;
|
||||
|
||||
@@ -18,7 +18,7 @@ jobs:
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
|
||||
- uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
|
||||
- uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
|
||||
with:
|
||||
script: |
|
||||
const {
|
||||
|
||||
@@ -138,7 +138,7 @@ jobs:
|
||||
# version: ${{ needs.init.outputs.tag }}
|
||||
|
||||
- name: Upload digests
|
||||
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
with:
|
||||
name: digests-${{ matrix.platform.arch }}
|
||||
path: /tmp/digests
|
||||
@@ -229,7 +229,7 @@ jobs:
|
||||
repositories: home-assistant-addon
|
||||
|
||||
- name: Trigger Workflow
|
||||
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
|
||||
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
|
||||
with:
|
||||
github-token: ${{ steps.generate-token.outputs.token }}
|
||||
script: |
|
||||
@@ -264,7 +264,7 @@ jobs:
|
||||
repositories: esphome-schema
|
||||
|
||||
- name: Trigger Workflow
|
||||
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
|
||||
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
|
||||
with:
|
||||
github-token: ${{ steps.generate-token.outputs.token }}
|
||||
script: |
|
||||
@@ -295,7 +295,7 @@ jobs:
|
||||
repositories: version-notifier
|
||||
|
||||
- name: Trigger Workflow
|
||||
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
|
||||
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
|
||||
with:
|
||||
github-token: ${{ steps.generate-token.outputs.token }}
|
||||
script: |
|
||||
|
||||
@@ -2,30 +2,29 @@ name: Status check labels
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
types: [labeled, unlabeled]
|
||||
types: [opened, reopened, labeled, unlabeled, synchronize]
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.event.pull_request.number }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
check:
|
||||
name: Check ${{ matrix.label }}
|
||||
name: Check blocking labels
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
label:
|
||||
- needs-docs
|
||||
- merge-after-release
|
||||
- chained-pr
|
||||
steps:
|
||||
- name: Check for ${{ matrix.label }} label
|
||||
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
|
||||
- name: Check for blocking labels
|
||||
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
|
||||
with:
|
||||
script: |
|
||||
const blockingLabels = ['needs-docs', 'merge-after-release', 'chained-pr'];
|
||||
const { data: labels } = await github.rest.issues.listLabelsOnIssue({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issue_number: context.issue.number
|
||||
});
|
||||
const hasLabel = labels.find(label => label.name === '${{ matrix.label }}');
|
||||
if (hasLabel) {
|
||||
core.setFailed('Pull request cannot be merged, it is labeled as ${{ matrix.label }}');
|
||||
const labelNames = labels.map(l => l.name);
|
||||
const found = blockingLabels.filter(bl => labelNames.includes(bl));
|
||||
if (found.length > 0) {
|
||||
core.setFailed(`Pull request cannot be merged, it has blocking label(s): ${found.join(', ')}`);
|
||||
}
|
||||
|
||||
@@ -41,7 +41,7 @@ jobs:
|
||||
python script/run-in-env.py pre-commit run --all-files
|
||||
|
||||
- name: Commit changes
|
||||
uses: peter-evans/create-pull-request@c0f553fe549906ede9cf27b5156039d195d2ece0 # v8.1.0
|
||||
uses: peter-evans/create-pull-request@5f6978faf089d4d20b00c7766989d076bb2fc7f1 # v8.1.1
|
||||
with:
|
||||
commit-message: "Synchronise Device Classes from Home Assistant"
|
||||
committer: esphomebot <esphome@openhomefoundation.org>
|
||||
|
||||
@@ -11,7 +11,7 @@ ci:
|
||||
repos:
|
||||
- repo: https://github.com/astral-sh/ruff-pre-commit
|
||||
# Ruff version.
|
||||
rev: v0.15.9
|
||||
rev: v0.15.10
|
||||
hooks:
|
||||
# Run the linter.
|
||||
- id: ruff
|
||||
|
||||
@@ -48,7 +48,7 @@ PROJECT_NAME = ESPHome
|
||||
# could be handy for archiving the generated documentation or if some version
|
||||
# control system is used.
|
||||
|
||||
PROJECT_NUMBER = 2026.4.0
|
||||
PROJECT_NUMBER = 2026.5.0-dev
|
||||
|
||||
# Using the PROJECT_BRIEF tag one can provide an optional one line description
|
||||
# for a project that appears at the top of each page and should give viewer a
|
||||
|
||||
+1
-8
@@ -750,15 +750,8 @@ def upload_using_esptool(
|
||||
platformio_api.FlashImage(
|
||||
path=idedata.firmware_bin_path, offset=firmware_offset
|
||||
),
|
||||
*idedata.extra_flash_images,
|
||||
]
|
||||
for image in idedata.extra_flash_images:
|
||||
if not image.path.is_file():
|
||||
_LOGGER.warning(
|
||||
"Skipping missing flash image declared by platform: %s",
|
||||
image.path,
|
||||
)
|
||||
continue
|
||||
flash_images.append(image)
|
||||
|
||||
mcu = "esp8266"
|
||||
if CORE.is_esp32:
|
||||
|
||||
@@ -2,11 +2,7 @@ import logging
|
||||
|
||||
import esphome.codegen as cg
|
||||
from esphome.components import sensor, voltage_sampler
|
||||
from esphome.components.esp32 import (
|
||||
get_esp32_variant,
|
||||
include_builtin_idf_component,
|
||||
require_adc_oneshot_iram,
|
||||
)
|
||||
from esphome.components.esp32 import get_esp32_variant, include_builtin_idf_component
|
||||
from esphome.components.nrf52.const import AIN_TO_GPIO, EXTRA_ADC
|
||||
from esphome.components.zephyr import (
|
||||
zephyr_add_overlay,
|
||||
@@ -28,7 +24,6 @@ from esphome.const import (
|
||||
PlatformFramework,
|
||||
)
|
||||
from esphome.core import CORE
|
||||
from esphome.types import ConfigType
|
||||
|
||||
from . import (
|
||||
ATTENUATION_MODES,
|
||||
@@ -70,13 +65,6 @@ def validate_config(config):
|
||||
return config
|
||||
|
||||
|
||||
def _require_adc_iram(config: ConfigType) -> ConfigType:
|
||||
"""Register ADC oneshot IRAM requirement during config validation."""
|
||||
if CORE.is_esp32:
|
||||
require_adc_oneshot_iram()
|
||||
return config
|
||||
|
||||
|
||||
ADCSensor = adc_ns.class_(
|
||||
"ADCSensor", sensor.Sensor, cg.PollingComponent, voltage_sampler.VoltageSampler
|
||||
)
|
||||
@@ -107,7 +95,6 @@ CONFIG_SCHEMA = cv.All(
|
||||
)
|
||||
.extend(cv.polling_component_schema("60s")),
|
||||
validate_config,
|
||||
_require_adc_iram,
|
||||
)
|
||||
|
||||
CONF_ADC_CHANNEL_ID = "adc_channel_id"
|
||||
|
||||
@@ -8,6 +8,9 @@ namespace ade7953_base {
|
||||
|
||||
static const char *const TAG = "ade7953";
|
||||
|
||||
constexpr uint16_t CONFIG_DEFAULT = 0x8004u;
|
||||
constexpr uint16_t CONFIG_LOCK_BIT = 0x8000u;
|
||||
|
||||
static const float ADE_POWER_FACTOR = 154.0f;
|
||||
static const float ADE_WATTSEC_POWER_FACTOR = ADE_POWER_FACTOR * ADE_POWER_FACTOR / 3600;
|
||||
|
||||
@@ -18,7 +21,12 @@ void ADE7953::setup() {
|
||||
|
||||
// The chip might take up to 100ms to initialise
|
||||
this->set_timeout(100, [this]() {
|
||||
// this->ade_write_8(0x0010, 0x04);
|
||||
// Lock communication interface (SPI or I2C)
|
||||
uint16_t config_v = CONFIG_DEFAULT;
|
||||
this->ade_read_16(CONFIG_16, &config_v);
|
||||
config_v &= static_cast<uint16_t>(~CONFIG_LOCK_BIT); // Clear the lock bit
|
||||
this->ade_write_16(CONFIG_16, config_v);
|
||||
// Configure optimum settings according to datasheet
|
||||
this->ade_write_8(0x00FE, 0xAD);
|
||||
this->ade_write_16(0x0120, 0x0030);
|
||||
// Set gains
|
||||
|
||||
@@ -9,31 +9,35 @@
|
||||
namespace esphome {
|
||||
namespace ade7953_base {
|
||||
|
||||
static const uint8_t PGA_V_8 =
|
||||
static constexpr uint8_t PGA_V_8 =
|
||||
0x007; // PGA_V, (R/W) Default: 0x00, Unsigned, Voltage channel gain configuration (Bits[2:0])
|
||||
static const uint8_t PGA_IA_8 =
|
||||
static constexpr uint8_t PGA_IA_8 =
|
||||
0x008; // PGA_IA, (R/W) Default: 0x00, Unsigned, Current Channel A gain configuration (Bits[2:0])
|
||||
static const uint8_t PGA_IB_8 =
|
||||
static constexpr uint8_t PGA_IB_8 =
|
||||
0x009; // PGA_IB, (R/W) Default: 0x00, Unsigned, Current Channel B gain configuration (Bits[2:0])
|
||||
|
||||
static const uint32_t AIGAIN_32 =
|
||||
static constexpr uint16_t CONFIG_16 = 0x102; // CONFIG, (R/W) Default: 0x8004, Unsigned, Configuration register
|
||||
|
||||
static constexpr uint16_t AIGAIN_32 =
|
||||
0x380; // AIGAIN, (R/W) Default: 0x400000, Unsigned,Current channel gain (Current Channel A)(32 bit)
|
||||
static const uint32_t AVGAIN_32 = 0x381; // AVGAIN, (R/W) Default: 0x400000, Unsigned,Voltage channel gain(32 bit)
|
||||
static const uint32_t AWGAIN_32 =
|
||||
static constexpr uint16_t AVGAIN_32 =
|
||||
0x381; // AVGAIN, (R/W) Default: 0x400000, Unsigned,Voltage channel gain(32 bit)
|
||||
static constexpr uint16_t AWGAIN_32 =
|
||||
0x382; // AWGAIN, (R/W) Default: 0x400000, Unsigned,Active power gain (Current Channel A)(32 bit)
|
||||
static const uint32_t AVARGAIN_32 =
|
||||
static constexpr uint16_t AVARGAIN_32 =
|
||||
0x383; // AVARGAIN, (R/W) Default: 0x400000, Unsigned, Reactive power gain (Current Channel A)(32 bit)
|
||||
static const uint32_t AVAGAIN_32 =
|
||||
static constexpr uint16_t AVAGAIN_32 =
|
||||
0x384; // AVAGAIN, (R/W) Default: 0x400000, Unsigned,Apparent power gain (Current Channel A)(32 bit)
|
||||
|
||||
static const uint32_t BIGAIN_32 =
|
||||
static constexpr uint16_t BIGAIN_32 =
|
||||
0x38C; // BIGAIN, (R/W) Default: 0x400000, Unsigned,Current channel gain (Current Channel B)(32 bit)
|
||||
static const uint32_t BVGAIN_32 = 0x38D; // BVGAIN, (R/W) Default: 0x400000, Unsigned,Voltage channel gain(32 bit)
|
||||
static const uint32_t BWGAIN_32 =
|
||||
static constexpr uint16_t BVGAIN_32 =
|
||||
0x38D; // BVGAIN, (R/W) Default: 0x400000, Unsigned,Voltage channel gain(32 bit)
|
||||
static constexpr uint16_t BWGAIN_32 =
|
||||
0x38E; // BWGAIN, (R/W) Default: 0x400000, Unsigned,Active power gain (Current Channel B)(32 bit)
|
||||
static const uint32_t BVARGAIN_32 =
|
||||
static constexpr uint16_t BVARGAIN_32 =
|
||||
0x38F; // BVARGAIN, (R/W) Default: 0x400000, Unsigned, Reactive power gain (Current Channel B)(32 bit)
|
||||
static const uint32_t BVAGAIN_32 =
|
||||
static constexpr uint16_t BVAGAIN_32 =
|
||||
0x390; // BVAGAIN, (R/W) Default: 0x400000, Unsigned,Apparent power gain (Current Channel B)(32 bit)
|
||||
|
||||
class ADE7953 : public PollingComponent, public sensor::Sensor {
|
||||
|
||||
@@ -7,6 +7,9 @@ namespace ade7953_spi {
|
||||
|
||||
static const char *const TAG = "ade7953";
|
||||
|
||||
// Datasheet requires at least 1.2µs after clearing CONFIG LOCK_BIT before raising CS
|
||||
constexpr uint8_t CONFIG_LOCK_SETTLE_US = 2;
|
||||
|
||||
void AdE7953Spi::setup() {
|
||||
this->spi_setup();
|
||||
ade7953_base::ADE7953::setup();
|
||||
@@ -32,6 +35,9 @@ bool AdE7953Spi::ade_write_16(uint16_t reg, uint16_t value) {
|
||||
this->write_byte16(reg);
|
||||
this->transfer_byte(0);
|
||||
this->write_byte16(value);
|
||||
if (reg == ade7953_base::CONFIG_16) {
|
||||
delayMicroseconds(CONFIG_LOCK_SETTLE_US);
|
||||
}
|
||||
this->disable();
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -12,7 +12,7 @@ namespace esphome {
|
||||
namespace ade7953_spi {
|
||||
|
||||
class AdE7953Spi : public ade7953_base::ADE7953,
|
||||
public spi::SPIDevice<spi::BIT_ORDER_MSB_FIRST, spi::CLOCK_POLARITY_HIGH, spi::CLOCK_PHASE_LEADING,
|
||||
public spi::SPIDevice<spi::BIT_ORDER_MSB_FIRST, spi::CLOCK_POLARITY_HIGH, spi::CLOCK_PHASE_TRAILING,
|
||||
spi::DATA_RATE_1MHZ> {
|
||||
public:
|
||||
void setup() override;
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import base64
|
||||
import logging
|
||||
import pathlib
|
||||
|
||||
from esphome import automation
|
||||
from esphome.automation import Condition
|
||||
@@ -458,6 +459,10 @@ async def to_code(config: ConfigType) -> None:
|
||||
# Enable optimized memzero/memcmp in libsodium instead of volatile byte loops
|
||||
cg.add_build_flag("-DHAVE_WEAK_SYMBOLS=1")
|
||||
cg.add_build_flag("-DHAVE_INLINE_ASM=1")
|
||||
# Compile crypto libraries with -O2 for speed instead of -Os.
|
||||
# Crypto is CPU-bound and benefits significantly from speed optimization.
|
||||
# GCC uses the last -O flag, so appending -O2 overrides the global -Os.
|
||||
_write_crypto_optimize_script()
|
||||
else:
|
||||
cg.add_define("USE_API_PLAINTEXT")
|
||||
|
||||
@@ -465,6 +470,17 @@ async def to_code(config: ConfigType) -> None:
|
||||
cg.add_global(api_ns.using)
|
||||
|
||||
|
||||
_CRYPTO_OPTIMIZE_SCRIPT = "crypto_optimize.py"
|
||||
|
||||
|
||||
def _write_crypto_optimize_script() -> None:
|
||||
from esphome.helpers import copy_file_if_changed
|
||||
|
||||
script_src = pathlib.Path(__file__).parent / f"{_CRYPTO_OPTIMIZE_SCRIPT}.script"
|
||||
copy_file_if_changed(script_src, CORE.relative_build_path(_CRYPTO_OPTIMIZE_SCRIPT))
|
||||
cg.add_platformio_option("extra_scripts", [f"post:{_CRYPTO_OPTIMIZE_SCRIPT}"])
|
||||
|
||||
|
||||
KEY_VALUE_SCHEMA = cv.Schema({cv.string: cv.templatable(cv.string_strict)})
|
||||
|
||||
|
||||
|
||||
@@ -778,10 +778,9 @@ message SubscribeLogsResponse {
|
||||
option (source) = SOURCE_SERVER;
|
||||
option (log) = false;
|
||||
option (no_delay) = false;
|
||||
option (speed_optimized) = true;
|
||||
|
||||
LogLevel level = 1 [(force) = true];
|
||||
bytes message = 3 [(force) = true];
|
||||
LogLevel level = 1;
|
||||
bytes message = 3;
|
||||
}
|
||||
|
||||
// ==================== NOISE ENCRYPTION ====================
|
||||
|
||||
@@ -195,7 +195,10 @@ class APIFrameHelper {
|
||||
}
|
||||
// Get the frame footer size required by this protocol
|
||||
uint8_t frame_footer_size() const { return frame_footer_size_; }
|
||||
// Check if socket has data ready to read
|
||||
// Check if socket has buffered data ready to read.
|
||||
// Contract: callers must read until it would block (EAGAIN/EWOULDBLOCK)
|
||||
// or track that they stopped early and retry without this check.
|
||||
// See Socket::ready() for details.
|
||||
bool is_socket_ready() const { return socket_ != nullptr && socket_->ready(); }
|
||||
// Release excess memory from internal buffers after initial sync
|
||||
void release_buffers() {
|
||||
|
||||
@@ -745,9 +745,8 @@ uint32_t ListEntitiesSensorResponse::calculate_size() const {
|
||||
#endif
|
||||
return size;
|
||||
}
|
||||
__attribute__((optimize("O2"))) // NOLINT(clang-diagnostic-unknown-attributes)
|
||||
uint8_t *
|
||||
SensorStateResponse::encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const {
|
||||
__attribute__((optimize("O2"))) uint8_t *SensorStateResponse::encode(
|
||||
ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const {
|
||||
uint8_t *__restrict__ pos = buffer.get_pos();
|
||||
ProtoEncode::write_tag_and_fixed32(pos PROTO_ENCODE_DEBUG_ARG, 13, this->key);
|
||||
ProtoEncode::encode_float(pos PROTO_ENCODE_DEBUG_ARG, 2, this->state);
|
||||
@@ -757,9 +756,7 @@ SensorStateResponse::encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) c
|
||||
#endif
|
||||
return pos;
|
||||
}
|
||||
__attribute__((optimize("O2"))) // NOLINT(clang-diagnostic-unknown-attributes)
|
||||
uint32_t
|
||||
SensorStateResponse::calculate_size() const {
|
||||
__attribute__((optimize("O2"))) uint32_t SensorStateResponse::calculate_size() const {
|
||||
uint32_t size = 0;
|
||||
size += 5;
|
||||
size += ProtoSize::calc_float(1, this->state);
|
||||
@@ -916,22 +913,16 @@ bool SubscribeLogsRequest::decode_varint(uint32_t field_id, proto_varint_value_t
|
||||
}
|
||||
return true;
|
||||
}
|
||||
__attribute__((optimize("O2"))) // NOLINT(clang-diagnostic-unknown-attributes)
|
||||
uint8_t *
|
||||
SubscribeLogsResponse::encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const {
|
||||
uint8_t *SubscribeLogsResponse::encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const {
|
||||
uint8_t *__restrict__ pos = buffer.get_pos();
|
||||
ProtoEncode::encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, 1, static_cast<uint32_t>(this->level), true);
|
||||
ProtoEncode::write_raw_byte(pos PROTO_ENCODE_DEBUG_ARG, 26);
|
||||
ProtoEncode::encode_varint_raw(pos PROTO_ENCODE_DEBUG_ARG, this->message_len_);
|
||||
ProtoEncode::encode_raw(pos PROTO_ENCODE_DEBUG_ARG, this->message_ptr_, this->message_len_);
|
||||
ProtoEncode::encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, 1, static_cast<uint32_t>(this->level));
|
||||
ProtoEncode::encode_bytes(pos PROTO_ENCODE_DEBUG_ARG, 3, this->message_ptr_, this->message_len_);
|
||||
return pos;
|
||||
}
|
||||
__attribute__((optimize("O2"))) // NOLINT(clang-diagnostic-unknown-attributes)
|
||||
uint32_t
|
||||
SubscribeLogsResponse::calculate_size() const {
|
||||
uint32_t SubscribeLogsResponse::calculate_size() const {
|
||||
uint32_t size = 0;
|
||||
size += 2;
|
||||
size += ProtoSize::calc_length_force(1, this->message_len_);
|
||||
size += this->level ? 2 : 0;
|
||||
size += ProtoSize::calc_length(1, this->message_len_);
|
||||
return size;
|
||||
}
|
||||
#ifdef USE_API_NOISE
|
||||
@@ -2338,9 +2329,8 @@ bool SubscribeBluetoothLEAdvertisementsRequest::decode_varint(uint32_t field_id,
|
||||
}
|
||||
return true;
|
||||
}
|
||||
__attribute__((optimize("O2"))) // NOLINT(clang-diagnostic-unknown-attributes)
|
||||
uint8_t *
|
||||
BluetoothLERawAdvertisementsResponse::encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const {
|
||||
__attribute__((optimize("O2"))) uint8_t *BluetoothLERawAdvertisementsResponse::encode(
|
||||
ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const {
|
||||
uint8_t *__restrict__ pos = buffer.get_pos();
|
||||
for (uint16_t i = 0; i < this->advertisements_len; i++) {
|
||||
auto &sub_msg = this->advertisements[i];
|
||||
@@ -2362,9 +2352,7 @@ BluetoothLERawAdvertisementsResponse::encode(ProtoWriteBuffer &buffer PROTO_ENCO
|
||||
}
|
||||
return pos;
|
||||
}
|
||||
__attribute__((optimize("O2"))) // NOLINT(clang-diagnostic-unknown-attributes)
|
||||
uint32_t
|
||||
BluetoothLERawAdvertisementsResponse::calculate_size() const {
|
||||
__attribute__((optimize("O2"))) uint32_t BluetoothLERawAdvertisementsResponse::calculate_size() const {
|
||||
uint32_t size = 0;
|
||||
for (uint16_t i = 0; i < this->advertisements_len; i++) {
|
||||
auto &sub_msg = this->advertisements[i];
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
# Compile crypto libraries with -O2 for speed instead of the default -Os.
|
||||
# Crypto is CPU-bound and benefits significantly from speed optimization.
|
||||
# GCC uses the last -O flag, so appending -O2 overrides the global -Os
|
||||
# for these libraries only.
|
||||
Import("env")
|
||||
|
||||
for lb in env.GetLibBuilders():
|
||||
if lb.name in ("noise-c", "libsodium"):
|
||||
lb.env.Append(CCFLAGS=["-O2"])
|
||||
@@ -111,14 +111,14 @@ class ATM90E32Component : public PollingComponent,
|
||||
#endif
|
||||
float get_reference_voltage(uint8_t phase) {
|
||||
#ifdef USE_NUMBER
|
||||
return (phase < 3 && ref_voltages_[phase]) ? ref_voltages_[phase]->state : 120.0; // Default voltage
|
||||
return (phase >= 0 && phase < 3 && ref_voltages_[phase]) ? ref_voltages_[phase]->state : 120.0; // Default voltage
|
||||
#else
|
||||
return 120.0; // Default voltage
|
||||
#endif
|
||||
}
|
||||
float get_reference_current(uint8_t phase) {
|
||||
#ifdef USE_NUMBER
|
||||
return (phase < 3 && ref_currents_[phase]) ? ref_currents_[phase]->state : 5.0f; // Default current
|
||||
return (phase >= 0 && phase < 3 && ref_currents_[phase]) ? ref_currents_[phase]->state : 5.0f; // Default current
|
||||
#else
|
||||
return 5.0f; // Default current
|
||||
#endif
|
||||
|
||||
@@ -106,6 +106,30 @@ void IRAM_ATTR CC1101Component::gpio_intr(CC1101Component *arg) { arg->enable_lo
|
||||
|
||||
void CC1101Component::setup() {
|
||||
this->spi_setup();
|
||||
|
||||
if (this->gdo0_pin_ != nullptr) {
|
||||
this->gdo0_pin_->setup();
|
||||
}
|
||||
|
||||
this->configure();
|
||||
if (this->is_failed()) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Defer pin mode setup until after all components have completed setup()
|
||||
// This handles the case where remote_transmitter runs after CC1101 and changes pin mode
|
||||
if (this->gdo0_pin_ != nullptr) {
|
||||
this->defer([this]() {
|
||||
this->gdo0_pin_->pin_mode(gpio::FLAG_INPUT);
|
||||
if (this->state_.PKT_FORMAT == static_cast<uint8_t>(PacketFormat::PACKET_FORMAT_FIFO)) {
|
||||
this->gdo0_pin_->attach_interrupt(&CC1101Component::gpio_intr, this, gpio::INTERRUPT_RISING_EDGE);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
void CC1101Component::configure() {
|
||||
// Manual reset sequence per CC1101 datasheet section 19.1.2
|
||||
this->cs_->digital_write(true);
|
||||
delayMicroseconds(1);
|
||||
this->cs_->digital_write(false);
|
||||
@@ -128,11 +152,6 @@ void CC1101Component::setup() {
|
||||
return;
|
||||
}
|
||||
|
||||
// Setup GDO0 pin if configured
|
||||
if (this->gdo0_pin_ != nullptr) {
|
||||
this->gdo0_pin_->setup();
|
||||
}
|
||||
|
||||
this->initialized_ = true;
|
||||
|
||||
for (uint8_t i = 0; i <= static_cast<uint8_t>(Register::TEST0); i++) {
|
||||
@@ -142,21 +161,11 @@ void CC1101Component::setup() {
|
||||
this->write_(static_cast<Register>(i));
|
||||
}
|
||||
this->set_output_power(this->output_power_requested_);
|
||||
|
||||
if (!this->enter_rx_()) {
|
||||
this->mark_failed();
|
||||
return;
|
||||
}
|
||||
|
||||
// Defer pin mode setup until after all components have completed setup()
|
||||
// This handles the case where remote_transmitter runs after CC1101 and changes pin mode
|
||||
if (this->gdo0_pin_ != nullptr) {
|
||||
this->defer([this]() {
|
||||
this->gdo0_pin_->pin_mode(gpio::FLAG_INPUT);
|
||||
if (this->state_.PKT_FORMAT == static_cast<uint8_t>(PacketFormat::PACKET_FORMAT_FIFO)) {
|
||||
this->gdo0_pin_->attach_interrupt(&CC1101Component::gpio_intr, this, gpio::INTERRUPT_RISING_EDGE);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
void CC1101Component::call_listeners_(const std::vector<uint8_t> &packet, float freq_offset, float rssi, uint8_t lqi) {
|
||||
@@ -273,7 +282,7 @@ void CC1101Component::begin_rx() {
|
||||
|
||||
void CC1101Component::reset() {
|
||||
this->strobe_(Command::RES);
|
||||
this->setup();
|
||||
this->configure();
|
||||
}
|
||||
|
||||
void CC1101Component::set_idle() {
|
||||
|
||||
@@ -25,6 +25,7 @@ class CC1101Component : public Component,
|
||||
void setup() override;
|
||||
void loop() override;
|
||||
void dump_config() override;
|
||||
void configure();
|
||||
|
||||
// Actions
|
||||
void begin_tx();
|
||||
|
||||
@@ -43,3 +43,11 @@ wave_4_26.extend(
|
||||
},
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
ssd1677.extend(
|
||||
"waveshare-3.97in",
|
||||
width=800,
|
||||
height=480,
|
||||
mirror_x=True,
|
||||
)
|
||||
|
||||
@@ -676,7 +676,7 @@ ARDUINO_FRAMEWORK_VERSION_LOOKUP = {
|
||||
"dev": cv.Version(3, 3, 8),
|
||||
}
|
||||
ARDUINO_PLATFORM_VERSION_LOOKUP = {
|
||||
cv.Version(3, 3, 8): cv.Version(55, 3, 38, "1"),
|
||||
cv.Version(3, 3, 8): cv.Version(55, 3, 38),
|
||||
cv.Version(3, 3, 7): cv.Version(55, 3, 37),
|
||||
cv.Version(3, 3, 6): cv.Version(55, 3, 36),
|
||||
cv.Version(3, 3, 5): cv.Version(55, 3, 35),
|
||||
@@ -724,7 +724,7 @@ ESP_IDF_PLATFORM_VERSION_LOOKUP = {
|
||||
cv.Version(
|
||||
6, 0, 0
|
||||
): "https://github.com/pioarduino/platform-espressif32.git#prep_IDF6",
|
||||
cv.Version(5, 5, 4): cv.Version(55, 3, 38, "1"),
|
||||
cv.Version(5, 5, 4): cv.Version(55, 3, 38),
|
||||
cv.Version(5, 5, 3, "1"): cv.Version(55, 3, 37),
|
||||
cv.Version(5, 5, 3): cv.Version(55, 3, 37),
|
||||
cv.Version(5, 5, 2): cv.Version(55, 3, 37),
|
||||
@@ -744,8 +744,8 @@ ESP_IDF_PLATFORM_VERSION_LOOKUP = {
|
||||
# The platform-espressif32 version
|
||||
# - https://github.com/pioarduino/platform-espressif32/releases
|
||||
PLATFORM_VERSION_LOOKUP = {
|
||||
"recommended": cv.Version(55, 3, 38, "1"),
|
||||
"latest": cv.Version(55, 3, 38, "1"),
|
||||
"recommended": cv.Version(55, 3, 38),
|
||||
"latest": cv.Version(55, 3, 38),
|
||||
"dev": "https://github.com/pioarduino/platform-espressif32.git#develop",
|
||||
}
|
||||
|
||||
@@ -1058,7 +1058,6 @@ CONF_DISABLE_MBEDTLS_PEER_CERT = "disable_mbedtls_peer_cert"
|
||||
CONF_DISABLE_MBEDTLS_PKCS7 = "disable_mbedtls_pkcs7"
|
||||
CONF_DISABLE_REGI2C_IN_IRAM = "disable_regi2c_in_iram"
|
||||
CONF_DISABLE_FATFS = "disable_fatfs"
|
||||
CONF_ADC_ONESHOT_IN_IRAM = "adc_oneshot_in_iram"
|
||||
|
||||
# VFS requirement tracking
|
||||
# Components that need VFS features can call require_vfs_*() functions
|
||||
@@ -1072,7 +1071,6 @@ KEY_MBEDTLS_PEER_CERT_REQUIRED = "mbedtls_peer_cert_required"
|
||||
KEY_MBEDTLS_PKCS7_REQUIRED = "mbedtls_pkcs7_required"
|
||||
KEY_FATFS_REQUIRED = "fatfs_required"
|
||||
KEY_MBEDTLS_SHA512_REQUIRED = "mbedtls_sha512_required"
|
||||
KEY_ADC_ONESHOT_IRAM_REQUIRED = "adc_oneshot_iram_required"
|
||||
|
||||
|
||||
def require_vfs_select() -> None:
|
||||
@@ -1170,17 +1168,6 @@ def require_fatfs() -> None:
|
||||
CORE.data[KEY_ESP32][KEY_FATFS_REQUIRED] = True
|
||||
|
||||
|
||||
def require_adc_oneshot_iram() -> None:
|
||||
"""Mark that ADC oneshot IRAM safety is required by a component.
|
||||
|
||||
Call this from components that use the ADC oneshot driver. When flash cache is
|
||||
disabled (e.g., during NVS writes by WiFi, BLE, Zigbee, or power management),
|
||||
the ADC oneshot read function must be in IRAM to avoid crashes.
|
||||
This sets CONFIG_ADC_ONESHOT_CTRL_FUNC_IN_IRAM.
|
||||
"""
|
||||
CORE.data[KEY_ESP32][KEY_ADC_ONESHOT_IRAM_REQUIRED] = True
|
||||
|
||||
|
||||
def _parse_idf_component(value: str) -> ConfigType:
|
||||
"""Parse IDF component shorthand syntax like 'owner/component^version'"""
|
||||
# Match operator followed by version-like string (digit or *)
|
||||
@@ -1281,7 +1268,6 @@ FRAMEWORK_SCHEMA = cv.Schema(
|
||||
cv.Optional(CONF_DISABLE_MBEDTLS_PEER_CERT, default=True): cv.boolean,
|
||||
cv.Optional(CONF_DISABLE_MBEDTLS_PKCS7, default=True): cv.boolean,
|
||||
cv.Optional(CONF_DISABLE_REGI2C_IN_IRAM, default=True): cv.boolean,
|
||||
cv.Optional(CONF_ADC_ONESHOT_IN_IRAM, default=False): cv.boolean,
|
||||
cv.Optional(CONF_DISABLE_FATFS, default=True): cv.boolean,
|
||||
}
|
||||
),
|
||||
@@ -2082,16 +2068,6 @@ async def to_code(config):
|
||||
if advanced[CONF_DISABLE_REGI2C_IN_IRAM]:
|
||||
add_idf_sdkconfig_option("CONFIG_ESP_REGI2C_CTRL_FUNC_IN_IRAM", False)
|
||||
|
||||
# Place ADC oneshot control functions in IRAM for cache safety
|
||||
# When flash cache is disabled (during NVS writes by WiFi, BLE, Zigbee, Thread,
|
||||
# power management, etc.), ADC reads will crash if these functions are in flash.
|
||||
# Components using ADC call require_adc_oneshot_iram() to force this.
|
||||
if (
|
||||
CORE.data[KEY_ESP32].get(KEY_ADC_ONESHOT_IRAM_REQUIRED, False)
|
||||
or advanced[CONF_ADC_ONESHOT_IN_IRAM]
|
||||
):
|
||||
add_idf_sdkconfig_option("CONFIG_ADC_ONESHOT_CTRL_FUNC_IN_IRAM", True)
|
||||
|
||||
# Disable FATFS support
|
||||
# Components that need FATFS (SD card, etc.) can call require_fatfs()
|
||||
if CORE.data[KEY_ESP32].get(KEY_FATFS_REQUIRED, False):
|
||||
|
||||
@@ -108,13 +108,8 @@ async def globals_set_to_code(config, action_id, template_arg, args):
|
||||
full_id, paren = await cg.get_variable_with_full_id(config[CONF_ID])
|
||||
template_arg = cg.TemplateArguments(full_id.type, *template_arg)
|
||||
var = cg.new_Pvariable(action_id, template_arg, paren)
|
||||
# Use the global's value_type alias as the lambda return type so
|
||||
# TemplatableFn stores a direct function pointer instead of going through
|
||||
# the deprecated converting trampoline when the value expression deduces
|
||||
# to a different type (e.g. int literal assigned to a float global).
|
||||
value_type = cg.RawExpression(f"{full_id.type}::value_type")
|
||||
templ = await cg.templatable(
|
||||
config[CONF_VALUE], args, value_type, to_exp=cg.RawExpression
|
||||
config[CONF_VALUE], args, None, to_exp=cg.RawExpression, wrap_constant=True
|
||||
)
|
||||
cg.add(var.set_value(templ))
|
||||
return var
|
||||
|
||||
@@ -36,7 +36,7 @@ I2SAudioMicrophone = i2s_audio_ns.class_(
|
||||
)
|
||||
|
||||
INTERNAL_ADC_VARIANTS = [esp32.VARIANT_ESP32]
|
||||
PDM_VARIANTS = [esp32.VARIANT_ESP32, esp32.VARIANT_ESP32S3, esp32.VARIANT_ESP32P4]
|
||||
PDM_VARIANTS = [esp32.VARIANT_ESP32, esp32.VARIANT_ESP32S3]
|
||||
|
||||
|
||||
def _validate_esp32_variant(config):
|
||||
|
||||
@@ -58,12 +58,6 @@ void AddressableLightTransformer::start() {
|
||||
// our transition will handle brightness, disable brightness in correction.
|
||||
this->light_.correction_.set_local_brightness(255);
|
||||
this->target_color_ *= to_uint8_scale(end_values.get_brightness() * end_values.get_state());
|
||||
|
||||
// Uniformity scan is deferred to the first apply() call. start() can run before the underlying
|
||||
// LED output's setup() has allocated its frame buffer (e.g. on_boot at priority > HARDWARE
|
||||
// triggering a transition), and reading through ESPColorView would deref a null buffer.
|
||||
this->uniform_start_scanned_ = false;
|
||||
this->uniform_start_is_uniform_ = false;
|
||||
}
|
||||
|
||||
inline constexpr uint8_t subtract_scaled_difference(uint8_t a, uint8_t b, int32_t scale) {
|
||||
@@ -103,57 +97,12 @@ optional<LightColorValues> AddressableLightTransformer::apply() {
|
||||
// non-linear when applying small deltas.
|
||||
|
||||
if (smoothed_progress > this->last_transition_progress_ && this->last_transition_progress_ < 1.f) {
|
||||
// Lazy uniformity scan: deferred from start() so the LED output's setup() has run and the
|
||||
// frame buffer is valid. When every LED already has the same color (the common case: plain
|
||||
// turn_on/turn_off on a uniform strip), interpolate math-only against a single start color.
|
||||
// Avoiding the per-step read-back through the 8-bit stored byte prevents gamma round-trip
|
||||
// quantization from stalling the fade at low values (e.g. gamma 2.8 pre-gamma values <27
|
||||
// round to stored 0, freezing progress).
|
||||
if (!this->uniform_start_scanned_) {
|
||||
this->uniform_start_scanned_ = true;
|
||||
if (this->light_.size() > 0) {
|
||||
Color first = this->light_[0].get();
|
||||
bool uniform = true;
|
||||
for (int32_t i = 1; i < this->light_.size(); i++) {
|
||||
if (this->light_[i].get() != first) {
|
||||
uniform = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (uniform) {
|
||||
this->uniform_start_color_ = first;
|
||||
this->uniform_start_is_uniform_ = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (this->uniform_start_is_uniform_) {
|
||||
// All LEDs started at the same color: compute the interpolated value once and write it to
|
||||
// every LED. No read-back, so each LED's stored byte advances through every gamma threshold
|
||||
// as smoothed_progress crosses it, instead of stalling at 0 for low pre-gamma values.
|
||||
//
|
||||
// Trade-off: any mid-transition writes to individual LEDs (e.g. from a user lambda) will be
|
||||
// overwritten on the next apply() here. The fallback path below would have respected them
|
||||
// via its read-back. Concurrent per-LED mutation during a transition isn't a pattern we
|
||||
// support, so this is acceptable.
|
||||
// lerp(start, target, progress) via existing helper: target - (target-start)*(1-progress).
|
||||
const Color &start = this->uniform_start_color_;
|
||||
int32_t remaining = int32_t(256.f * (1.f - smoothed_progress));
|
||||
uint8_t r = subtract_scaled_difference(this->target_color_.red, start.red, remaining);
|
||||
uint8_t g = subtract_scaled_difference(this->target_color_.green, start.green, remaining);
|
||||
uint8_t b = subtract_scaled_difference(this->target_color_.blue, start.blue, remaining);
|
||||
uint8_t w = subtract_scaled_difference(this->target_color_.white, start.white, remaining);
|
||||
for (auto led : this->light_) {
|
||||
led.set_rgbw(r, g, b, w);
|
||||
}
|
||||
} else {
|
||||
int32_t scale =
|
||||
int32_t(256.f * std::max((1.f - smoothed_progress) / (1.f - this->last_transition_progress_), 0.f));
|
||||
for (auto led : this->light_) {
|
||||
led.set_rgbw(subtract_scaled_difference(this->target_color_.red, led.get_red(), scale),
|
||||
subtract_scaled_difference(this->target_color_.green, led.get_green(), scale),
|
||||
subtract_scaled_difference(this->target_color_.blue, led.get_blue(), scale),
|
||||
subtract_scaled_difference(this->target_color_.white, led.get_white(), scale));
|
||||
}
|
||||
int32_t scale = int32_t(256.f * std::max((1.f - smoothed_progress) / (1.f - this->last_transition_progress_), 0.f));
|
||||
for (auto led : this->light_) {
|
||||
led.set_rgbw(subtract_scaled_difference(this->target_color_.red, led.get_red(), scale),
|
||||
subtract_scaled_difference(this->target_color_.green, led.get_green(), scale),
|
||||
subtract_scaled_difference(this->target_color_.blue, led.get_blue(), scale),
|
||||
subtract_scaled_difference(this->target_color_.white, led.get_white(), scale));
|
||||
}
|
||||
this->last_transition_progress_ = smoothed_progress;
|
||||
this->light_.schedule_show();
|
||||
|
||||
@@ -115,9 +115,6 @@ class AddressableLightTransformer : public LightTransformer {
|
||||
AddressableLight &light_;
|
||||
float last_transition_progress_{0.0f};
|
||||
Color target_color_{};
|
||||
Color uniform_start_color_{};
|
||||
bool uniform_start_scanned_{false};
|
||||
bool uniform_start_is_uniform_{false};
|
||||
};
|
||||
|
||||
} // namespace esphome::light
|
||||
|
||||
@@ -452,7 +452,7 @@ async def to_code(config):
|
||||
|
||||
esp32.add_idf_component(name="espressif/esp-tflite-micro", ref="1.3.3~1")
|
||||
# Pin esp-nn for stable future builds (esp-tflite-micro depends on esp-nn)
|
||||
esp32.add_idf_component(name="espressif/esp-nn", ref="1.1.2")
|
||||
esp32.add_idf_component(name="espressif/esp-nn", ref="1.2.1")
|
||||
|
||||
cg.add_build_flag("-DTF_LITE_STATIC_MEMORY")
|
||||
cg.add_build_flag("-DTF_LITE_DISABLE_X86_NEON")
|
||||
|
||||
@@ -28,8 +28,7 @@ void AirConditioner::on_status_change() {
|
||||
if (this->base_.getAutoconfStatus() == dudanov::midea::AUTOCONF_OK &&
|
||||
this->base_.getCapabilities().supportFrostProtectionPreset() && !this->frost_protection_set_) {
|
||||
// Read existing presets (set by codegen), append frost protection, write back
|
||||
auto traits = this->get_traits();
|
||||
const auto &existing = traits.get_supported_custom_presets();
|
||||
const auto &existing = this->get_traits().get_supported_custom_presets();
|
||||
bool found = false;
|
||||
for (const char *p : existing) {
|
||||
if (strcmp(p, Constants::FREEZE_PROTECTION) == 0) {
|
||||
|
||||
@@ -234,9 +234,9 @@ class MipiSpi : public display::Display,
|
||||
}
|
||||
|
||||
void dump_config() override {
|
||||
internal_dump_config(this->model_, this->get_width(), this->get_height(), OFFSET_WIDTH, OFFSET_HEIGHT,
|
||||
(uint8_t) MADCTL, this->invert_colors_, DISPLAYPIXEL * 8, IS_BIG_ENDIAN, this->brightness_,
|
||||
this->cs_, this->reset_pin_, this->dc_pin_, this->mode_, this->data_rate_, BUS_TYPE,
|
||||
internal_dump_config(this->model_, this->get_width(), this->get_height(), OFFSET_WIDTH, OFFSET_HEIGHT, MADCTL,
|
||||
this->invert_colors_, DISPLAYPIXEL * 8, IS_BIG_ENDIAN, this->brightness_, this->cs_,
|
||||
this->reset_pin_, this->dc_pin_, this->mode_, this->data_rate_, BUS_TYPE,
|
||||
HAS_HARDWARE_ROTATION);
|
||||
}
|
||||
|
||||
@@ -305,7 +305,7 @@ class MipiSpi : public display::Display,
|
||||
this->write_command_(BRIGHTNESS, this->brightness_.value());
|
||||
|
||||
// calculate new madctl value from base value adjusted for rotation
|
||||
uint8_t madctl = (uint8_t) MADCTL; // lower 8 bits only
|
||||
uint8_t madctl = MADCTL; // lower 8 bits only
|
||||
constexpr bool use_flips = (MADCTL & MADCTL_FLIP_FLAG) != 0;
|
||||
constexpr uint8_t x_mask = use_flips ? MADCTL_XFLIP : MADCTL_MX;
|
||||
constexpr uint8_t y_mask = use_flips ? MADCTL_YFLIP : MADCTL_MY;
|
||||
|
||||
@@ -36,9 +36,8 @@ bool Nextion::send_command_(const std::string &command) {
|
||||
}
|
||||
|
||||
#ifdef USE_NEXTION_COMMAND_SPACING
|
||||
const uint32_t now = App.get_loop_component_start_time();
|
||||
if (!this->connection_state_.ignore_is_setup_ && !this->command_pacer_.can_send(now)) {
|
||||
ESP_LOGN(TAG, "Command spacing: delaying '%s'", command.c_str());
|
||||
if (!this->connection_state_.ignore_is_setup_ && !this->command_pacer_.can_send()) {
|
||||
ESP_LOGN(TAG, "Command spacing: delaying command '%s'", command.c_str());
|
||||
return false;
|
||||
}
|
||||
#endif // USE_NEXTION_COMMAND_SPACING
|
||||
@@ -49,16 +48,6 @@ bool Nextion::send_command_(const std::string &command) {
|
||||
const uint8_t to_send[3] = {0xFF, 0xFF, 0xFF};
|
||||
this->write_array(to_send, sizeof(to_send));
|
||||
|
||||
#ifdef USE_NEXTION_COMMAND_SPACING
|
||||
// Mark sent immediately after writing to UART. The pacer enforces inter-command
|
||||
// spacing from the transmit side. Marking on ACK (0x01) would leave last_command_time_
|
||||
// at zero indefinitely, making can_send() always return true and spacing a no-op.
|
||||
// ignore_is_setup_ commands (setup/init sequence) bypass spacing intentionally.
|
||||
if (!this->connection_state_.ignore_is_setup_) {
|
||||
this->command_pacer_.mark_sent(now);
|
||||
}
|
||||
#endif // USE_NEXTION_COMMAND_SPACING
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -264,8 +253,11 @@ bool Nextion::send_command(const char *command) {
|
||||
if ((!this->is_setup() && !this->connection_state_.ignore_is_setup_) || this->is_sleeping())
|
||||
return false;
|
||||
|
||||
this->add_no_result_to_queue_with_command_("command", command);
|
||||
return true;
|
||||
if (this->send_command_(command)) {
|
||||
this->add_no_result_to_queue_("command");
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
bool Nextion::send_command_printf(const char *format, ...) {
|
||||
@@ -282,8 +274,11 @@ bool Nextion::send_command_printf(const char *format, ...) {
|
||||
return false;
|
||||
}
|
||||
|
||||
this->add_no_result_to_queue_with_command_("command_printf", buffer);
|
||||
return true;
|
||||
if (this->send_command_(buffer)) {
|
||||
this->add_no_result_to_queue_("command_printf");
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
#ifdef NEXTION_PROTOCOL_LOG
|
||||
@@ -354,43 +349,25 @@ void Nextion::loop() {
|
||||
}
|
||||
|
||||
#ifdef USE_NEXTION_COMMAND_SPACING
|
||||
// Try to send any pending commands if spacing allows
|
||||
this->process_pending_in_queue_();
|
||||
#ifdef USE_NEXTION_WAVEFORM
|
||||
if (!this->waveform_queue_.empty()) {
|
||||
this->check_pending_waveform_();
|
||||
}
|
||||
#endif // USE_NEXTION_WAVEFORM
|
||||
#endif // USE_NEXTION_COMMAND_SPACING
|
||||
}
|
||||
|
||||
#ifdef USE_NEXTION_COMMAND_SPACING
|
||||
void Nextion::process_pending_in_queue_() {
|
||||
#ifdef USE_NEXTION_MAX_COMMANDS_PER_LOOP
|
||||
size_t commands_sent = 0;
|
||||
#endif // USE_NEXTION_MAX_COMMANDS_PER_LOOP
|
||||
if (this->nextion_queue_.empty() || !this->command_pacer_.can_send()) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (auto *item : this->nextion_queue_) {
|
||||
if (item == nullptr || item->pending_command.empty()) {
|
||||
continue; // Already sent, waiting for ACK — skip, don't stop
|
||||
// Check if first item in queue has a pending command
|
||||
auto *front_item = this->nextion_queue_.front();
|
||||
if (front_item && !front_item->pending_command.empty()) {
|
||||
if (this->send_command_(front_item->pending_command)) {
|
||||
// Command sent successfully, clear the pending command
|
||||
front_item->pending_command.clear();
|
||||
ESP_LOGVV(TAG, "Pending command sent: %s", front_item->component->get_variable_name().c_str());
|
||||
}
|
||||
|
||||
#ifdef USE_NEXTION_MAX_COMMANDS_PER_LOOP
|
||||
if (++commands_sent > this->max_commands_per_loop_) {
|
||||
ESP_LOGV(TAG, "Pending cmds: loop limit reached, deferring");
|
||||
break;
|
||||
}
|
||||
#endif // USE_NEXTION_MAX_COMMANDS_PER_LOOP
|
||||
|
||||
const uint32_t now = App.get_loop_component_start_time();
|
||||
if (!this->command_pacer_.can_send(now)) {
|
||||
break; // Spacing not elapsed, stop for this loop iteration
|
||||
}
|
||||
|
||||
if (!this->send_command_(item->pending_command)) {
|
||||
break; // Unexpected send failure, stop
|
||||
}
|
||||
item->pending_command.clear();
|
||||
ESP_LOGVV(TAG, "Pending cmd sent: %s", item->component->get_variable_name().c_str());
|
||||
}
|
||||
}
|
||||
#endif // USE_NEXTION_COMMAND_SPACING
|
||||
@@ -493,6 +470,10 @@ void Nextion::process_nextion_commands_() {
|
||||
this->setup_callback_.call();
|
||||
}
|
||||
}
|
||||
#ifdef USE_NEXTION_COMMAND_SPACING
|
||||
this->command_pacer_.mark_sent(); // Here is where we should mark the command as sent
|
||||
ESP_LOGN(TAG, "Command spacing: marked command sent");
|
||||
#endif
|
||||
break;
|
||||
case 0x02: // invalid Component ID or name was used
|
||||
ESP_LOGW(TAG, "Invalid component ID/name");
|
||||
@@ -1098,18 +1079,10 @@ void Nextion::add_no_result_to_queue_(const std::string &variable_name) {
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Send a command and enqueue it for response tracking.
|
||||
* @brief
|
||||
*
|
||||
* Callers are responsible for checking is_sleeping() before calling this
|
||||
* method. The sleep guard is deliberately absent here because some callers
|
||||
* (e.g. add_no_result_to_queue_with_ignore_sleep_printf_()) are explicitly
|
||||
* sleep-safe and must bypass it.
|
||||
*
|
||||
* If USE_NEXTION_COMMAND_SPACING is enabled and the pacer is not ready,
|
||||
* the command is saved in the queue entry for retry rather than dropped.
|
||||
*
|
||||
* @param variable_name Name of the variable or component associated with the command.
|
||||
* @param command The raw command string to send.
|
||||
* @param variable_name Variable name for the queue
|
||||
* @param command
|
||||
*/
|
||||
void Nextion::add_no_result_to_queue_with_command_(const std::string &variable_name, const std::string &command) {
|
||||
if ((!this->is_setup() && !this->connection_state_.ignore_is_setup_) || command.empty())
|
||||
@@ -1290,22 +1263,9 @@ void Nextion::add_to_get_queue(NextionComponentBase *component) {
|
||||
|
||||
std::string command = "get " + component->get_variable_name_to_send();
|
||||
|
||||
#ifdef USE_NEXTION_COMMAND_SPACING
|
||||
// Always enqueue first so the response handler is present when the command
|
||||
// is eventually sent. Store the command for retry if spacing blocked it;
|
||||
// process_pending_in_queue_() will transmit it when the pacer allows.
|
||||
nextion_queue->pending_command = command;
|
||||
this->nextion_queue_.push_back(nextion_queue);
|
||||
if (this->send_command_(command)) {
|
||||
nextion_queue->pending_command.clear();
|
||||
}
|
||||
#else // USE_NEXTION_COMMAND_SPACING
|
||||
if (this->send_command_(command)) {
|
||||
this->nextion_queue_.push_back(nextion_queue);
|
||||
} else {
|
||||
delete nextion_queue; // NOLINT(cppcoreguidelines-owning-memory)
|
||||
}
|
||||
#endif // USE_NEXTION_COMMAND_SPACING
|
||||
}
|
||||
|
||||
#ifdef USE_NEXTION_WAVEFORM
|
||||
@@ -1349,10 +1309,10 @@ void Nextion::check_pending_waveform_() {
|
||||
char command[24]; // "addt " + uint8 + "," + uint8 + "," + uint8 + null = max 17 chars
|
||||
buf_append_printf(command, sizeof(command), 0, "addt %u,%u,%zu", component->get_component_id(),
|
||||
component->get_wave_channel_id(), buffer_to_send);
|
||||
// If spacing or setup state blocks the send, leave the entry at the front
|
||||
// of waveform_queue_ for retry on the next loop iteration via
|
||||
// check_pending_waveform_(). Only pop on a successful send.
|
||||
this->send_command_(command);
|
||||
if (!this->send_command_(command)) {
|
||||
delete nb; // NOLINT(cppcoreguidelines-owning-memory)
|
||||
this->waveform_queue_.pop();
|
||||
}
|
||||
}
|
||||
#endif // USE_NEXTION_WAVEFORM
|
||||
|
||||
|
||||
@@ -55,20 +55,15 @@ class NextionCommandPacer {
|
||||
uint8_t get_spacing() const { return spacing_ms_; }
|
||||
|
||||
/**
|
||||
* @brief Check if enough time has passed to send the next command.
|
||||
* @param now Current timestamp in milliseconds (use App.get_loop_component_start_time()
|
||||
* for consistency with the rest of the queue timing).
|
||||
* @return true if the spacing interval has elapsed since the last command was sent.
|
||||
* @brief Check if enough time has passed to send next command
|
||||
* @return true if enough time has passed since last command
|
||||
*/
|
||||
bool can_send(uint32_t now) const { return (now - last_command_time_) >= spacing_ms_; }
|
||||
bool can_send() const { return (millis() - last_command_time_) >= spacing_ms_; }
|
||||
|
||||
/**
|
||||
* @brief Record the transmit timestamp for the most recently sent command.
|
||||
* @param now Current timestamp in milliseconds, as returned by
|
||||
* App.get_loop_component_start_time(). Must use the same clock
|
||||
* source as can_send() to avoid unsigned underflow.
|
||||
* @brief Mark a command as sent, updating the timing
|
||||
*/
|
||||
void mark_sent(uint32_t now) { last_command_time_ = now; }
|
||||
void mark_sent() { last_command_time_ = millis(); }
|
||||
|
||||
private:
|
||||
uint8_t spacing_ms_;
|
||||
|
||||
@@ -55,11 +55,13 @@ void SafeModeComponent::dump_config() {
|
||||
#if defined(USE_ESP32) && defined(USE_OTA_ROLLBACK)
|
||||
const esp_partition_t *last_invalid = esp_ota_get_last_invalid_partition();
|
||||
if (last_invalid != nullptr) {
|
||||
ESP_LOGW(TAG, "OTA rollback detected! Rolled back from partition '%s'", last_invalid->label);
|
||||
ESP_LOGW(TAG, "The device reset before the boot was marked successful");
|
||||
ESP_LOGW(TAG,
|
||||
"OTA rollback detected! Rolled back from partition '%s'\n"
|
||||
" The device reset before the boot was marked successful",
|
||||
last_invalid->label);
|
||||
if (esp_reset_reason() == ESP_RST_BROWNOUT) {
|
||||
ESP_LOGW(TAG, "Last reset was due to brownout - check your power supply!");
|
||||
ESP_LOGW(TAG, "See https://esphome.io/guides/faq.html#brownout-detector-was-triggered");
|
||||
ESP_LOGW(TAG, "Last reset was due to brownout - check your power supply!\n"
|
||||
" See https://esphome.io/guides/faq.html#brownout-detector-was-triggered");
|
||||
}
|
||||
}
|
||||
#endif
|
||||
@@ -86,7 +88,8 @@ void SafeModeComponent::mark_successful() {
|
||||
}
|
||||
|
||||
void SafeModeComponent::loop() {
|
||||
if (!this->boot_successful_ && (millis() - this->safe_mode_start_time_) > this->safe_mode_boot_is_good_after_) {
|
||||
if (!this->boot_successful_ &&
|
||||
(App.get_loop_component_start_time() - this->safe_mode_start_time_) > this->safe_mode_boot_is_good_after_) {
|
||||
// successful boot, reset counter
|
||||
ESP_LOGI(TAG, "Boot seems successful; resetting boot loop counter");
|
||||
this->mark_successful();
|
||||
|
||||
@@ -112,6 +112,8 @@ class BSDSocketImpl {
|
||||
int setblocking(bool blocking);
|
||||
int loop() { return 0; }
|
||||
|
||||
/// Check if the socket has buffered data ready to read.
|
||||
/// See the ready() contract in socket.h — callers must drain or track remaining data.
|
||||
bool ready() const;
|
||||
|
||||
int get_fd() const { return this->fd_; }
|
||||
|
||||
@@ -96,6 +96,8 @@ class LWIPRawImpl : public LWIPRawCommon {
|
||||
errno = ENOSYS;
|
||||
return -1;
|
||||
}
|
||||
// Check if the socket has buffered data ready to read.
|
||||
// See the ready() contract in socket.h — callers must drain or track remaining data.
|
||||
// Intentionally unlocked — this is a polling check called every loop iteration.
|
||||
// A stale read at worst delays processing by one loop tick; the actual I/O in
|
||||
// read() holds the lwip lock and re-checks properly. See esphome#10681.
|
||||
|
||||
@@ -78,6 +78,8 @@ class LwIPSocketImpl {
|
||||
int setblocking(bool blocking);
|
||||
int loop() { return 0; }
|
||||
|
||||
/// Check if the socket has buffered data ready to read.
|
||||
/// See the ready() contract in socket.h — callers must drain or track remaining data.
|
||||
bool ready() const;
|
||||
|
||||
int get_fd() const { return this->fd_; }
|
||||
|
||||
@@ -53,6 +53,19 @@ bool socket_ready_fd(int fd, bool loop_monitored);
|
||||
|
||||
// Inline ready() — defined here because it depends on socket_ready/socket_ready_fd
|
||||
// declared above, while the impl headers are included before those declarations.
|
||||
//
|
||||
// Contract (applies to ALL socket implementations — each platform implements
|
||||
// ready() differently, but this contract holds regardless of the mechanism):
|
||||
// ready() checks if the socket has buffered data ready to read. When it returns
|
||||
// true, the caller MUST read until it would block (EAGAIN/EWOULDBLOCK), or until
|
||||
// read() returns 0 to indicate EOF / connection closed, or track that it stopped
|
||||
// early and retry without calling ready(). The next call to ready() will only
|
||||
// report new data correctly if all callers fulfill this contract. Failing to
|
||||
// drain the socket may cause ready() to return false while data remains readable.
|
||||
//
|
||||
// In practice each socket is owned by a single component, so this contract is
|
||||
// straightforward to fulfill — but the owning component must be aware of it,
|
||||
// especially if it limits how many messages it processes per loop iteration.
|
||||
#if defined(USE_SOCKET_IMPL_BSD_SOCKETS) || defined(USE_SOCKET_IMPL_LWIP_SOCKETS)
|
||||
inline bool Socket::ready() const {
|
||||
#ifdef USE_LWIP_FAST_SELECT
|
||||
|
||||
@@ -315,7 +315,7 @@ void TCS34725Component::set_integration_time(TCS34725IntegrationTime integration
|
||||
my_integration_time_regval = integration_time;
|
||||
this->integration_time_auto_ = false;
|
||||
}
|
||||
this->integration_time_ = (256.f - (float) my_integration_time_regval) * 2.4f;
|
||||
this->integration_time_ = (256.f - my_integration_time_regval) * 2.4f;
|
||||
ESP_LOGI(TAG, "TCS34725I Integration time set to: %.1fms", this->integration_time_);
|
||||
}
|
||||
void TCS34725Component::set_gain(TCS34725Gain gain) {
|
||||
|
||||
@@ -114,25 +114,7 @@ void OTARequestHandler::handleUpload(AsyncWebServerRequest *request, const Platf
|
||||
uint8_t *data, size_t len, bool final) {
|
||||
ota::OTAResponseTypes error_code = ota::OTA_RESPONSE_OK;
|
||||
|
||||
// First byte of a new upload: index==0 with actual data. (web_server_idf
|
||||
// fires a separate start-marker call with data==nullptr/len==0 before the
|
||||
// first real chunk; gate on len>0 so we only trigger once per upload.)
|
||||
if (index == 0 && len > 0) {
|
||||
// If a previous upload was interrupted (e.g. client closed the tab, TCP
|
||||
// reset) the backend from that session may still be open. Tear it down
|
||||
// so flash state doesn't get concatenated with the new image (which can
|
||||
// produce a technically-valid-sized but corrupted firmware that bricks
|
||||
// the device once it reboots).
|
||||
if (this->ota_backend_) {
|
||||
ESP_LOGW(TAG, "New OTA upload received while previous session was still open; aborting previous session");
|
||||
this->ota_backend_->abort();
|
||||
#ifdef USE_OTA_STATE_LISTENER
|
||||
// Notify listeners that the previous session was aborted before the new one starts.
|
||||
this->parent_->notify_state_deferred_(ota::OTA_ABORT, 0.0f, 0);
|
||||
#endif
|
||||
this->ota_backend_.reset();
|
||||
}
|
||||
|
||||
if (index == 0 && !this->ota_backend_) {
|
||||
// Initialize OTA on first call
|
||||
this->ota_init_(filename.c_str());
|
||||
|
||||
|
||||
+1
-1
@@ -4,7 +4,7 @@ from enum import Enum
|
||||
|
||||
from esphome.enum import StrEnum
|
||||
|
||||
__version__ = "2026.4.0"
|
||||
__version__ = "2026.5.0-dev"
|
||||
|
||||
ALLOWED_NAME_CHARS = "abcdefghijklmnopqrstuvwxyz0123456789-_"
|
||||
VALID_SUBSTITUTIONS_CHARACTERS = (
|
||||
|
||||
@@ -113,8 +113,7 @@ def _generate_source_table_code(
|
||||
entries = ", ".join(var_names)
|
||||
lines.append(f"static const char *const {table_var}[] PROGMEM = {{{entries}}};")
|
||||
lines.append(f"const LogString *{lookup_fn}(uint8_t index) {{")
|
||||
cond = "index == 0" if count >= 255 else f"index == 0 || index > {count}"
|
||||
lines.append(f' if ({cond}) return LOG_STR("<unknown>");')
|
||||
lines.append(f' if (index == 0 || index > {count}) return LOG_STR("<unknown>");')
|
||||
lines.append(" return reinterpret_cast<const LogString *>(")
|
||||
lines.append(f" progmem_read_ptr(&{table_var}[index - 1]));")
|
||||
lines.append("}")
|
||||
|
||||
@@ -65,6 +65,9 @@ def run_platformio_cli(*args, **kwargs) -> str | int:
|
||||
os.environ.setdefault("UV_HTTP_RETRIES", "10")
|
||||
cmd = [sys.executable, "-m", "esphome.platformio_runner"] + list(args)
|
||||
|
||||
if not CORE.verbose:
|
||||
kwargs["filter_lines"] = FILTER_PLATFORMIO_LINES
|
||||
|
||||
return run_external_process(*cmd, **kwargs)
|
||||
|
||||
|
||||
|
||||
@@ -105,36 +105,6 @@ def main() -> int:
|
||||
patch_structhash()
|
||||
patch_file_downloader()
|
||||
|
||||
# Wrap stdout/stderr with RedirectText before PlatformIO runs:
|
||||
#
|
||||
# 1. RedirectText.isatty() unconditionally returns True. Click, tqdm, and
|
||||
# PlatformIO's own progress-bar code check ``stream.isatty()`` to
|
||||
# decide whether to emit TTY-format output (``\r`` cursor moves, ANSI
|
||||
# colors, fancy progress bars). With the wrapper in place they always
|
||||
# emit TTY format, even when our real stdout is a pipe to the parent
|
||||
# process. Downstream consumers (local terminals and the Home
|
||||
# Assistant dashboard log viewer) render the TTY control sequences
|
||||
# correctly, so the user sees real progress bars.
|
||||
#
|
||||
# 2. FILTER_PLATFORMIO_LINES is applied inside RedirectText.write() in
|
||||
# this subprocess, so noisy PlatformIO output is dropped before it
|
||||
# ever leaves the runner. This replaces the parent-side filtering
|
||||
# that was lost when we switched from in-process to subprocess — the
|
||||
# parent's ``subprocess.run`` uses ``.fileno()`` on RedirectText and
|
||||
# bypasses its ``write()`` path entirely.
|
||||
#
|
||||
# Filtering is disabled when the user passed -v / --verbose to
|
||||
# ``esphome compile``, preserving the previous in-process behavior where
|
||||
# verbose mode let all PlatformIO output through unfiltered.
|
||||
from esphome.platformio_api import FILTER_PLATFORMIO_LINES
|
||||
from esphome.util import RedirectText
|
||||
|
||||
is_verbose = any(arg in ("-v", "--verbose") for arg in sys.argv[1:])
|
||||
filter_lines = None if is_verbose else FILTER_PLATFORMIO_LINES
|
||||
|
||||
sys.stdout = RedirectText(sys.stdout, filter_lines=filter_lines)
|
||||
sys.stderr = RedirectText(sys.stderr, filter_lines=filter_lines)
|
||||
|
||||
import platformio.__main__
|
||||
|
||||
return platformio.__main__.main() or 0
|
||||
|
||||
+2
-2
@@ -133,7 +133,7 @@ extra_scripts = post:esphome/components/esp8266/post_build.py.script
|
||||
; This are common settings for the ESP32 (all variants) using Arduino.
|
||||
[common:esp32-arduino]
|
||||
extends = common:arduino
|
||||
platform = https://github.com/pioarduino/platform-espressif32/releases/download/55.03.38-1/platform-espressif32.zip
|
||||
platform = https://github.com/pioarduino/platform-espressif32/releases/download/55.03.38/platform-espressif32.zip
|
||||
platform_packages =
|
||||
pioarduino/framework-arduinoespressif32@https://github.com/espressif/arduino-esp32/releases/download/3.3.8/esp32-core-3.3.8.tar.xz
|
||||
pioarduino/framework-espidf@https://github.com/pioarduino/esp-idf/releases/download/v5.5.4/esp-idf-v5.5.4.tar.xz
|
||||
@@ -169,7 +169,7 @@ extra_scripts = post:esphome/components/esp32/post_build.py.script
|
||||
; This are common settings for the ESP32 (all variants) using IDF.
|
||||
[common:esp32-idf]
|
||||
extends = common:idf
|
||||
platform = https://github.com/pioarduino/platform-espressif32/releases/download/55.03.38-1/platform-espressif32.zip
|
||||
platform = https://github.com/pioarduino/platform-espressif32/releases/download/55.03.38/platform-espressif32.zip
|
||||
platform_packages =
|
||||
pioarduino/framework-espidf@https://github.com/pioarduino/esp-idf/releases/download/v5.5.4/esp-idf-v5.5.4.tar.xz
|
||||
|
||||
|
||||
+2
-2
@@ -12,14 +12,14 @@ platformio==6.1.19
|
||||
esptool==5.2.0
|
||||
click==8.3.2
|
||||
esphome-dashboard==20260408.1
|
||||
aioesphomeapi==44.15.0
|
||||
aioesphomeapi==44.13.3
|
||||
zeroconf==0.148.0
|
||||
puremagic==1.30
|
||||
ruamel.yaml==0.19.1 # dashboard_import
|
||||
ruamel.yaml.clib==0.2.15 # dashboard_import
|
||||
esphome-glyphsets==0.2.0
|
||||
pillow==12.2.0
|
||||
resvg-py==0.2.6
|
||||
resvg-py==0.3.1
|
||||
freetype-py==2.5.1
|
||||
jinja2==3.1.6
|
||||
bleak==2.1.1
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
pylint==4.0.5
|
||||
flake8==7.3.0 # also change in .pre-commit-config.yaml when updating
|
||||
ruff==0.15.9 # also change in .pre-commit-config.yaml when updating
|
||||
ruff==0.15.10 # also change in .pre-commit-config.yaml when updating
|
||||
pyupgrade==3.21.2 # also change in .pre-commit-config.yaml when updating
|
||||
pre-commit
|
||||
|
||||
|
||||
@@ -1028,8 +1028,7 @@ class BytesType(TypeInfo):
|
||||
)
|
||||
|
||||
def get_size_calculation(self, name: str, force: bool = False) -> str:
|
||||
calc_fn = "calc_length_force" if force else "calc_length"
|
||||
return f"size += ProtoSize::{calc_fn}({self.calculate_field_id_size()}, this->{self.field_name}_len_);"
|
||||
return f"size += ProtoSize::calc_length({self.calculate_field_id_size()}, this->{self.field_name}_len_);"
|
||||
|
||||
def get_estimated_size(self) -> int:
|
||||
return self.calculate_field_id_size() + 8 # field ID + 8 bytes typical bytes
|
||||
@@ -1110,8 +1109,7 @@ class PointerToBytesBufferType(PointerToBufferTypeBase):
|
||||
)
|
||||
|
||||
def get_size_calculation(self, name: str, force: bool = False) -> str:
|
||||
calc_fn = "calc_length_force" if force else "calc_length"
|
||||
return f"size += ProtoSize::{calc_fn}({self.calculate_field_id_size()}, this->{self.field_name}_len);"
|
||||
return f"size += ProtoSize::calc_length({self.calculate_field_id_size()}, this->{self.field_name}_len);"
|
||||
|
||||
|
||||
class PointerToStringBufferType(PointerToBufferTypeBase):
|
||||
@@ -2684,12 +2682,9 @@ def build_message_type(
|
||||
# Check if this message wants speed-optimized encode/calculate_size.
|
||||
# When set, __attribute__((optimize("O2"))) is added to the definitions
|
||||
# so GCC inlines the small ProtoEncode helpers even under -Os.
|
||||
is_speed_optimized = get_opt(desc, pb.speed_optimized, False)
|
||||
speed_attr = (
|
||||
'__attribute__((optimize("O2"))) // NOLINT(clang-diagnostic-unknown-attributes)\n'
|
||||
if is_speed_optimized
|
||||
else ""
|
||||
)
|
||||
speed_opt = getattr(pb, "speed_optimized", None)
|
||||
is_speed_optimized = speed_opt is not None and get_opt(desc, speed_opt, False)
|
||||
speed_attr = '__attribute__((optimize("O2"))) ' if is_speed_optimized else ""
|
||||
|
||||
# Only generate encode method if this message needs encoding and has fields
|
||||
if needs_encode and encode and not is_inline_only:
|
||||
|
||||
@@ -26,12 +26,11 @@ CORE_BENCHMARKS_DIR: Path = Path(root_path) / "tests" / "benchmarks" / "core"
|
||||
STUBS_DIR: Path = Path(root_path) / "tests" / "benchmarks" / "stubs"
|
||||
|
||||
PLATFORMIO_OPTIONS = {
|
||||
"build_unflags": [
|
||||
"-Os", # remove default size-opt
|
||||
],
|
||||
"build_flags": [
|
||||
"-O2", # optimize for speed (CodSpeed recommends RelWithDebInfo)
|
||||
"-Os", # match firmware optimization level (detects inlining regressions)
|
||||
"-g", # debug symbols for profiling
|
||||
"-ffunction-sections", # required for dead-code stripping with -Os
|
||||
"-fdata-sections", # required for dead-code stripping with -Os
|
||||
"-DUSE_BENCHMARK", # disable WarnIfComponentBlockingGuard in finish()
|
||||
f"-I{STUBS_DIR}", # stub headers for ESP32-only components
|
||||
],
|
||||
|
||||
@@ -10,7 +10,6 @@ namespace esphome::benchmarks {
|
||||
static constexpr int kInnerIterations = 2000;
|
||||
|
||||
// --- random_float() ---
|
||||
// Ported from ol.yaml:148 "Random Float Benchmark"
|
||||
|
||||
static void RandomFloat(benchmark::State &state) {
|
||||
for (auto _ : state) {
|
||||
@@ -38,4 +37,274 @@ static void RandomUint32(benchmark::State &state) {
|
||||
}
|
||||
BENCHMARK(RandomUint32);
|
||||
|
||||
// --- format_hex_to() - 6 bytes (MAC address sized) ---
|
||||
|
||||
static void FormatHexTo_6Bytes(benchmark::State &state) {
|
||||
const uint8_t data[] = {0xAB, 0xCD, 0xEF, 0x01, 0x23, 0x45};
|
||||
char buffer[13]; // 6 * 2 + 1
|
||||
for (auto _ : state) {
|
||||
for (int i = 0; i < kInnerIterations; i++) {
|
||||
format_hex_to(buffer, data, 6);
|
||||
}
|
||||
benchmark::DoNotOptimize(buffer);
|
||||
}
|
||||
state.SetItemsProcessed(state.iterations() * kInnerIterations);
|
||||
}
|
||||
BENCHMARK(FormatHexTo_6Bytes);
|
||||
|
||||
// --- format_hex_to() - 16 bytes (UUID sized) ---
|
||||
|
||||
static void FormatHexTo_16Bytes(benchmark::State &state) {
|
||||
const uint8_t data[] = {0xAB, 0xCD, 0xEF, 0x01, 0x23, 0x45, 0x67, 0x89,
|
||||
0xFE, 0xDC, 0xBA, 0x98, 0x76, 0x54, 0x32, 0x10};
|
||||
char buffer[33]; // 16 * 2 + 1
|
||||
for (auto _ : state) {
|
||||
for (int i = 0; i < kInnerIterations; i++) {
|
||||
format_hex_to(buffer, data, 16);
|
||||
}
|
||||
benchmark::DoNotOptimize(buffer);
|
||||
}
|
||||
state.SetItemsProcessed(state.iterations() * kInnerIterations);
|
||||
}
|
||||
BENCHMARK(FormatHexTo_16Bytes);
|
||||
|
||||
// --- format_hex_to() - 100 bytes (large payload) ---
|
||||
|
||||
static void FormatHexTo_100Bytes(benchmark::State &state) {
|
||||
uint8_t data[100];
|
||||
for (int i = 0; i < 100; i++) {
|
||||
data[i] = static_cast<uint8_t>(i);
|
||||
}
|
||||
char buffer[201]; // 100 * 2 + 1
|
||||
for (auto _ : state) {
|
||||
for (int i = 0; i < kInnerIterations; i++) {
|
||||
format_hex_to(buffer, data, 100);
|
||||
}
|
||||
benchmark::DoNotOptimize(buffer);
|
||||
}
|
||||
state.SetItemsProcessed(state.iterations() * kInnerIterations);
|
||||
}
|
||||
BENCHMARK(FormatHexTo_100Bytes);
|
||||
|
||||
// --- format_hex_pretty_to() - 6 bytes with ':' separator ---
|
||||
|
||||
static void FormatHexPrettyTo_6Bytes(benchmark::State &state) {
|
||||
const uint8_t data[] = {0xAB, 0xCD, 0xEF, 0x01, 0x23, 0x45};
|
||||
char buffer[18]; // 6 * 3
|
||||
for (auto _ : state) {
|
||||
for (int i = 0; i < kInnerIterations; i++) {
|
||||
format_hex_pretty_to(buffer, data, 6);
|
||||
}
|
||||
benchmark::DoNotOptimize(buffer);
|
||||
}
|
||||
state.SetItemsProcessed(state.iterations() * kInnerIterations);
|
||||
}
|
||||
BENCHMARK(FormatHexPrettyTo_6Bytes);
|
||||
|
||||
// --- format_mac_addr_upper() ---
|
||||
|
||||
static void FormatMacAddrUpper(benchmark::State &state) {
|
||||
const uint8_t mac[] = {0xAA, 0xBB, 0xCC, 0xDD, 0xEE, 0xFF};
|
||||
char buffer[MAC_ADDRESS_PRETTY_BUFFER_SIZE];
|
||||
for (auto _ : state) {
|
||||
for (int i = 0; i < kInnerIterations; i++) {
|
||||
format_mac_addr_upper(mac, buffer);
|
||||
}
|
||||
benchmark::DoNotOptimize(buffer);
|
||||
}
|
||||
state.SetItemsProcessed(state.iterations() * kInnerIterations);
|
||||
}
|
||||
BENCHMARK(FormatMacAddrUpper);
|
||||
|
||||
// --- fnv1_hash() - short string ---
|
||||
|
||||
static void Fnv1Hash_Short(benchmark::State &state) {
|
||||
const char *str = "sensor.temperature";
|
||||
for (auto _ : state) {
|
||||
uint32_t result = 0;
|
||||
for (int i = 0; i < kInnerIterations; i++) {
|
||||
result ^= fnv1_hash(str);
|
||||
}
|
||||
benchmark::DoNotOptimize(result);
|
||||
}
|
||||
state.SetItemsProcessed(state.iterations() * kInnerIterations);
|
||||
}
|
||||
BENCHMARK(Fnv1Hash_Short);
|
||||
|
||||
// --- fnv1_hash() - long string ---
|
||||
|
||||
static void Fnv1Hash_Long(benchmark::State &state) {
|
||||
const char *str = "binary_sensor.living_room_motion_sensor_occupancy_detected";
|
||||
for (auto _ : state) {
|
||||
uint32_t result = 0;
|
||||
for (int i = 0; i < kInnerIterations; i++) {
|
||||
result ^= fnv1_hash(str);
|
||||
}
|
||||
benchmark::DoNotOptimize(result);
|
||||
}
|
||||
state.SetItemsProcessed(state.iterations() * kInnerIterations);
|
||||
}
|
||||
BENCHMARK(Fnv1Hash_Long);
|
||||
|
||||
// --- fnv1a_hash() - short string ---
|
||||
// Use DoNotOptimize on the input pointer to prevent constexpr evaluation
|
||||
|
||||
static void Fnv1aHash_Short(benchmark::State &state) {
|
||||
const char *str = "sensor.temperature";
|
||||
benchmark::DoNotOptimize(str);
|
||||
for (auto _ : state) {
|
||||
uint32_t result = 0;
|
||||
for (int i = 0; i < kInnerIterations; i++) {
|
||||
result ^= fnv1a_hash(str);
|
||||
benchmark::ClobberMemory();
|
||||
}
|
||||
benchmark::DoNotOptimize(result);
|
||||
}
|
||||
state.SetItemsProcessed(state.iterations() * kInnerIterations);
|
||||
}
|
||||
BENCHMARK(Fnv1aHash_Short);
|
||||
|
||||
// --- fnv1a_hash() - long string ---
|
||||
|
||||
static void Fnv1aHash_Long(benchmark::State &state) {
|
||||
const char *str = "binary_sensor.living_room_motion_sensor_occupancy_detected";
|
||||
benchmark::DoNotOptimize(str);
|
||||
for (auto _ : state) {
|
||||
uint32_t result = 0;
|
||||
for (int i = 0; i < kInnerIterations; i++) {
|
||||
result ^= fnv1a_hash(str);
|
||||
benchmark::ClobberMemory();
|
||||
}
|
||||
benchmark::DoNotOptimize(result);
|
||||
}
|
||||
state.SetItemsProcessed(state.iterations() * kInnerIterations);
|
||||
}
|
||||
BENCHMARK(Fnv1aHash_Long);
|
||||
|
||||
// --- fnv1_hash_object_id() - typical entity name ---
|
||||
|
||||
static void Fnv1HashObjectId(benchmark::State &state) {
|
||||
char name[] = "Living Room Temperature Sensor";
|
||||
size_t len = sizeof(name) - 1;
|
||||
benchmark::DoNotOptimize(name);
|
||||
for (auto _ : state) {
|
||||
uint32_t result = 0;
|
||||
for (int i = 0; i < kInnerIterations; i++) {
|
||||
result ^= fnv1_hash_object_id(name, len);
|
||||
benchmark::ClobberMemory();
|
||||
}
|
||||
benchmark::DoNotOptimize(result);
|
||||
}
|
||||
state.SetItemsProcessed(state.iterations() * kInnerIterations);
|
||||
}
|
||||
BENCHMARK(Fnv1HashObjectId);
|
||||
|
||||
// --- parse_hex() - 6 bytes from string ---
|
||||
|
||||
static void ParseHex_6Bytes(benchmark::State &state) {
|
||||
const char *hex_str = "ABCDEF012345";
|
||||
uint8_t data[6];
|
||||
for (auto _ : state) {
|
||||
for (int i = 0; i < kInnerIterations; i++) {
|
||||
parse_hex(hex_str, data, 6);
|
||||
}
|
||||
benchmark::DoNotOptimize(data);
|
||||
}
|
||||
state.SetItemsProcessed(state.iterations() * kInnerIterations);
|
||||
}
|
||||
BENCHMARK(ParseHex_6Bytes);
|
||||
|
||||
// --- parse_hex() - 16 bytes from string ---
|
||||
|
||||
static void ParseHex_16Bytes(benchmark::State &state) {
|
||||
const char *hex_str = "ABCDEF0123456789FEDCBA9876543210";
|
||||
uint8_t data[16];
|
||||
for (auto _ : state) {
|
||||
for (int i = 0; i < kInnerIterations; i++) {
|
||||
parse_hex(hex_str, data, 16);
|
||||
}
|
||||
benchmark::DoNotOptimize(data);
|
||||
}
|
||||
state.SetItemsProcessed(state.iterations() * kInnerIterations);
|
||||
}
|
||||
BENCHMARK(ParseHex_16Bytes);
|
||||
|
||||
// --- crc8() - 8 bytes ---
|
||||
|
||||
static void CRC8_8Bytes(benchmark::State &state) {
|
||||
const uint8_t data[] = {0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08};
|
||||
for (auto _ : state) {
|
||||
uint8_t result = 0;
|
||||
for (int i = 0; i < kInnerIterations; i++) {
|
||||
result ^= crc8(data, 8);
|
||||
}
|
||||
benchmark::DoNotOptimize(result);
|
||||
}
|
||||
state.SetItemsProcessed(state.iterations() * kInnerIterations);
|
||||
}
|
||||
BENCHMARK(CRC8_8Bytes);
|
||||
|
||||
// --- crc16() - 8 bytes ---
|
||||
|
||||
static void CRC16_8Bytes(benchmark::State &state) {
|
||||
const uint8_t data[] = {0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08};
|
||||
for (auto _ : state) {
|
||||
uint16_t result = 0;
|
||||
for (int i = 0; i < kInnerIterations; i++) {
|
||||
result ^= crc16(data, 8);
|
||||
}
|
||||
benchmark::DoNotOptimize(result);
|
||||
}
|
||||
state.SetItemsProcessed(state.iterations() * kInnerIterations);
|
||||
}
|
||||
BENCHMARK(CRC16_8Bytes);
|
||||
|
||||
// --- value_accuracy_to_buf() - typical sensor value ---
|
||||
|
||||
static void ValueAccuracyToBuf(benchmark::State &state) {
|
||||
char raw_buf[VALUE_ACCURACY_MAX_LEN] = {};
|
||||
std::span<char, VALUE_ACCURACY_MAX_LEN> buf(raw_buf);
|
||||
float value = 23.456f;
|
||||
for (auto _ : state) {
|
||||
for (int i = 0; i < kInnerIterations; i++) {
|
||||
value_accuracy_to_buf(buf, value, 2);
|
||||
}
|
||||
benchmark::DoNotOptimize(raw_buf);
|
||||
}
|
||||
state.SetItemsProcessed(state.iterations() * kInnerIterations);
|
||||
}
|
||||
BENCHMARK(ValueAccuracyToBuf);
|
||||
|
||||
// --- int8_to_str() ---
|
||||
|
||||
static void Int8ToStr(benchmark::State &state) {
|
||||
char buffer[5] = {};
|
||||
for (auto _ : state) {
|
||||
for (int i = 0; i < kInnerIterations; i++) {
|
||||
int8_to_str(buffer, static_cast<int8_t>(i & 0xFF));
|
||||
benchmark::DoNotOptimize(buffer);
|
||||
benchmark::ClobberMemory();
|
||||
}
|
||||
}
|
||||
state.SetItemsProcessed(state.iterations() * kInnerIterations);
|
||||
}
|
||||
BENCHMARK(Int8ToStr);
|
||||
|
||||
// --- base64_decode() - into pre-allocated buffer ---
|
||||
|
||||
static void Base64Decode_32Bytes(benchmark::State &state) {
|
||||
// 32 bytes encoded = 44 base64 chars
|
||||
const uint8_t encoded[] = "AAECAwQFBgcICQoLDA0ODxAREhMUFRYXGBkaGx0eHw==";
|
||||
size_t encoded_len = 44;
|
||||
uint8_t output[32];
|
||||
for (auto _ : state) {
|
||||
for (int i = 0; i < kInnerIterations; i++) {
|
||||
base64_decode(encoded, encoded_len, output, sizeof(output));
|
||||
}
|
||||
benchmark::DoNotOptimize(output);
|
||||
}
|
||||
state.SetItemsProcessed(state.iterations() * kInnerIterations);
|
||||
}
|
||||
BENCHMARK(Base64Decode_32Bytes);
|
||||
|
||||
} // namespace esphome::benchmarks
|
||||
|
||||
@@ -4,14 +4,6 @@ esphome:
|
||||
- globals.set:
|
||||
id: glob_int
|
||||
value: "10"
|
||||
# Set a float global with an integer literal - must emit the correct
|
||||
# return type so TemplatableFn stores a direct function pointer.
|
||||
- globals.set:
|
||||
id: glob_float
|
||||
value: "102"
|
||||
- globals.set:
|
||||
id: glob_float
|
||||
value: !lambda "return 42;"
|
||||
|
||||
globals:
|
||||
- id: glob_int
|
||||
|
||||
@@ -1,29 +0,0 @@
|
||||
esphome:
|
||||
name: addr-light-transition
|
||||
host:
|
||||
api:
|
||||
logger:
|
||||
level: DEBUG
|
||||
|
||||
external_components:
|
||||
- source:
|
||||
type: local
|
||||
path: EXTERNAL_COMPONENT_PATH
|
||||
|
||||
light:
|
||||
- platform: mock_addressable_light
|
||||
output_id: strip_output
|
||||
id: strip
|
||||
name: "Test Strip"
|
||||
num_leds: 4
|
||||
gamma_correct: 2.8
|
||||
default_transition_length: 0s
|
||||
|
||||
sensor:
|
||||
- platform: template
|
||||
name: "led0_red_raw"
|
||||
id: led0_red_raw
|
||||
update_interval: 10ms
|
||||
accuracy_decimals: 0
|
||||
lambda: |-
|
||||
return (float) id(strip_output).get_raw_red(0);
|
||||
@@ -1 +0,0 @@
|
||||
CODEOWNERS = ["@esphome/tests"]
|
||||
@@ -1,23 +0,0 @@
|
||||
import esphome.codegen as cg
|
||||
from esphome.components import light
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import CONF_NUM_LEDS, CONF_OUTPUT_ID
|
||||
from esphome.types import ConfigType
|
||||
|
||||
mock_addressable_light_ns = cg.esphome_ns.namespace("mock_addressable_light")
|
||||
MockAddressableLight = mock_addressable_light_ns.class_(
|
||||
"MockAddressableLight", light.AddressableLight
|
||||
)
|
||||
|
||||
CONFIG_SCHEMA = light.ADDRESSABLE_LIGHT_SCHEMA.extend(
|
||||
{
|
||||
cv.GenerateID(CONF_OUTPUT_ID): cv.declare_id(MockAddressableLight),
|
||||
cv.Optional(CONF_NUM_LEDS, default=4): cv.positive_not_null_int,
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
async def to_code(config: ConfigType) -> None:
|
||||
var = cg.new_Pvariable(config[CONF_OUTPUT_ID], config[CONF_NUM_LEDS])
|
||||
await light.register_light(var, config)
|
||||
await cg.register_component(var, config)
|
||||
-52
@@ -1,52 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <memory>
|
||||
|
||||
#include "esphome/components/light/addressable_light.h"
|
||||
#include "esphome/core/component.h"
|
||||
|
||||
namespace esphome::mock_addressable_light {
|
||||
|
||||
// In-memory addressable light for host-mode integration tests. Exposes the raw
|
||||
// per-LED byte buffer (post-gamma-correction, as the hardware would see it)
|
||||
// so tests can observe transition behavior without real hardware.
|
||||
class MockAddressableLight : public light::AddressableLight {
|
||||
public:
|
||||
explicit MockAddressableLight(uint16_t num_leds)
|
||||
: num_leds_(num_leds), buf_(new uint8_t[num_leds * 4]()), effect_data_(new uint8_t[num_leds]()) {}
|
||||
|
||||
void setup() override {}
|
||||
void write_state(light::LightState *state) override {}
|
||||
int32_t size() const override { return this->num_leds_; }
|
||||
void clear_effect_data() override {
|
||||
for (uint16_t i = 0; i < this->num_leds_; i++)
|
||||
this->effect_data_[i] = 0;
|
||||
}
|
||||
light::LightTraits get_traits() override {
|
||||
auto traits = light::LightTraits();
|
||||
traits.set_supported_color_modes({light::ColorMode::RGB});
|
||||
return traits;
|
||||
}
|
||||
|
||||
// Accessors for tests: return the raw stored byte (post gamma correction),
|
||||
// which is what actual LED hardware would receive.
|
||||
uint8_t get_raw_red(uint16_t index) const { return this->buf_[index * 4 + 0]; }
|
||||
uint8_t get_raw_green(uint16_t index) const { return this->buf_[index * 4 + 1]; }
|
||||
uint8_t get_raw_blue(uint16_t index) const { return this->buf_[index * 4 + 2]; }
|
||||
uint8_t get_raw_white(uint16_t index) const { return this->buf_[index * 4 + 3]; }
|
||||
|
||||
protected:
|
||||
light::ESPColorView get_view_internal(int32_t index) const override {
|
||||
size_t pos = index * 4;
|
||||
return {this->buf_.get() + pos + 0, this->buf_.get() + pos + 1, this->buf_.get() + pos + 2,
|
||||
this->buf_.get() + pos + 3, this->effect_data_.get() + index, &this->correction_};
|
||||
}
|
||||
|
||||
uint16_t num_leds_;
|
||||
std::unique_ptr<uint8_t[]> buf_;
|
||||
std::unique_ptr<uint8_t[]> effect_data_;
|
||||
};
|
||||
|
||||
} // namespace esphome::mock_addressable_light
|
||||
@@ -1,119 +0,0 @@
|
||||
"""Integration test for addressable light transitions with gamma correction.
|
||||
|
||||
Regression test for a bug where a long turn-on transition on an addressable
|
||||
light with gamma correction (e.g. gamma_correct: 2.8) produced no visible
|
||||
output for ~90% of the transition duration, then jumped to the target in the
|
||||
final ~10%. Root cause: the transition algorithm read each LED's current value
|
||||
back through the 8-bit stored byte every step; at gamma 2.8 any pre-gamma value
|
||||
below ~27 rounds to stored byte 0, so the stored byte stalled at 0 until
|
||||
progress was high enough for a single step to produce a large-enough pre-gamma
|
||||
value to clear the gamma threshold.
|
||||
|
||||
The fix interpolates against a cached start color when all LEDs started at the
|
||||
same value (the common case for plain turn_on/turn_off), avoiding the round-trip.
|
||||
|
||||
This test uses a host-only mock addressable light that exposes the raw stored
|
||||
byte of each LED, so we can observe the transition directly.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
|
||||
from aioesphomeapi import LightInfo, SensorInfo, SensorState
|
||||
import pytest
|
||||
|
||||
from .state_utils import InitialStateHelper, require_entity
|
||||
from .types import APIClientConnectedFactory, RunCompiledFunction
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_addressable_light_transition(
|
||||
yaml_config: str,
|
||||
run_compiled: RunCompiledFunction,
|
||||
api_client_connected: APIClientConnectedFactory,
|
||||
) -> None:
|
||||
"""With gamma 2.8, the stored raw byte must rise visibly well before the end."""
|
||||
async with run_compiled(yaml_config), api_client_connected() as client:
|
||||
entities, _ = await client.list_entities_services()
|
||||
light = require_entity(entities, "test_strip", LightInfo)
|
||||
sensor = require_entity(entities, "led0_red_raw", SensorInfo)
|
||||
|
||||
# Track the raw-byte sensor. It polls every 10ms in the fixture, and
|
||||
# ESPHome sensors publish on every change, so we collect a time series.
|
||||
# Samples are stored as absolute (loop_time, value); we rebase to the
|
||||
# command-issue time after the run so pre-command samples are strictly
|
||||
# negative and reliably excluded.
|
||||
loop = asyncio.get_running_loop()
|
||||
samples: list[tuple[float, float]] = []
|
||||
|
||||
def on_state(state: object) -> None:
|
||||
if not isinstance(state, SensorState) or state.key != sensor.key:
|
||||
return
|
||||
samples.append((loop.time(), state.state))
|
||||
|
||||
# InitialStateHelper swallows the first state ESPHome sends per entity
|
||||
# on subscribe, so on_state only sees real post-subscribe updates.
|
||||
initial_state_helper = InitialStateHelper(entities)
|
||||
client.subscribe_states(initial_state_helper.on_state_wrapper(on_state))
|
||||
await initial_state_helper.wait_for_initial_states()
|
||||
|
||||
# Start transition: off -> full white over 1 second. This is the
|
||||
# scenario from the bug report, compressed in time.
|
||||
transition_s = 1.0
|
||||
command_time = loop.time()
|
||||
client.light_command(
|
||||
key=light.key,
|
||||
state=True,
|
||||
rgb=(1.0, 1.0, 1.0),
|
||||
brightness=1.0,
|
||||
transition_length=transition_s,
|
||||
)
|
||||
|
||||
# Let the full transition run, plus margin for the final sample.
|
||||
await asyncio.sleep(transition_s + 0.2)
|
||||
|
||||
# Rebase to command-issue time. Pre-command samples have t < 0 and are
|
||||
# excluded; everything else is in seconds since the command was issued.
|
||||
post_command = [
|
||||
(t - command_time, v) for (t, v) in samples if t >= command_time
|
||||
]
|
||||
assert post_command, "no sensor samples received after command was issued"
|
||||
|
||||
# Assertion 1: the transition is not stalled. With the bug, the raw
|
||||
# byte stays at 0 until ~90% of the transition duration. With the fix,
|
||||
# it becomes nonzero in the first ~30% (for gamma 2.8, pre-gamma 76
|
||||
# clears the gamma threshold at progress ~0.30). Require the first
|
||||
# nonzero sample to land well before 50% of the transition duration,
|
||||
# measured from the command-issue time. The 50% bound (rather than
|
||||
# 70%) leaves headroom for assertion 2's mid-window check.
|
||||
first_nonzero = next(((t, v) for (t, v) in post_command if v > 0), None)
|
||||
assert first_nonzero is not None, (
|
||||
"raw byte never rose above 0 during the transition — the fade stalled"
|
||||
)
|
||||
assert first_nonzero[0] < transition_s * 0.5, (
|
||||
f"raw byte only rose above 0 at t={first_nonzero[0]:.3f}s "
|
||||
f"(>{transition_s * 0.5:.3f}s after command) — transition is stalling"
|
||||
)
|
||||
|
||||
# Assertion 2: by mid-late transition, the raw byte should have reached
|
||||
# a substantial fraction of its final value. Bound the window to
|
||||
# [50%, 90%] of the transition so the post-transition settled value
|
||||
# (which always reaches 255) can't satisfy this assertion — that would
|
||||
# let "stays at 0 then jumps at 99%" regressions slip through.
|
||||
mid_window = [
|
||||
v
|
||||
for (t, v) in post_command
|
||||
if transition_s * 0.5 <= t <= transition_s * 0.9
|
||||
]
|
||||
assert mid_window, "no samples captured in mid-transition window"
|
||||
assert max(mid_window) >= 100, (
|
||||
f"raw byte peaked at only {max(mid_window)} between 50%–90% of "
|
||||
"transition (expected >= 100 for white target at gamma 2.8)"
|
||||
)
|
||||
|
||||
# Assertion 3: final value reaches target. Gamma 2.8 of 255 is 255.
|
||||
final_samples = [v for (_, v) in post_command[-5:]]
|
||||
assert max(final_samples) >= 250, (
|
||||
f"final raw byte was {max(final_samples)}, expected >= 250"
|
||||
)
|
||||
@@ -1231,48 +1231,6 @@ def test_upload_using_esptool_path_conversion(
|
||||
assert partitions_path.endswith("partitions.bin")
|
||||
|
||||
|
||||
def test_upload_using_esptool_skips_missing_extra_flash_images(
|
||||
tmp_path: Path,
|
||||
mock_run_external_command_main: Mock,
|
||||
mock_get_idedata: Mock,
|
||||
caplog: pytest.LogCaptureFixture,
|
||||
) -> None:
|
||||
"""A non-existent path in extra_flash_images must be filtered out with a
|
||||
warning, and must not appear in the esptool command line. Only the valid
|
||||
images are flashed. Regression test for
|
||||
https://github.com/esphome/esphome/issues/15634.
|
||||
"""
|
||||
setup_core(platform=PLATFORM_ESP32, tmp_path=tmp_path, name="test")
|
||||
CORE.data[KEY_ESP32] = {KEY_VARIANT: VARIANT_ESP32}
|
||||
|
||||
missing_path = tmp_path / "variants" / "tasmota" / "tinyuf2.bin"
|
||||
|
||||
mock_idedata = MagicMock(spec=platformio_api.IDEData)
|
||||
mock_idedata.firmware_bin_path = tmp_path / "firmware.bin"
|
||||
mock_idedata.extra_flash_images = [
|
||||
platformio_api.FlashImage(path=tmp_path / "bootloader.bin", offset="0x1000"),
|
||||
platformio_api.FlashImage(path=missing_path, offset="0x2d0000"),
|
||||
]
|
||||
mock_get_idedata.return_value = mock_idedata
|
||||
|
||||
(tmp_path / "firmware.bin").touch()
|
||||
(tmp_path / "bootloader.bin").touch()
|
||||
# Intentionally do NOT create missing_path
|
||||
|
||||
config = {CONF_ESPHOME: {"platformio_options": {}}}
|
||||
|
||||
with caplog.at_level(logging.WARNING, logger="esphome.__main__"):
|
||||
result = upload_using_esptool(config, "/dev/ttyUSB0", None, None)
|
||||
|
||||
assert result == 0
|
||||
assert "Skipping missing flash image" in caplog.text
|
||||
assert str(missing_path) in caplog.text
|
||||
|
||||
cmd_list = list(mock_run_external_command_main.call_args[0][1:])
|
||||
assert str(missing_path) not in cmd_list
|
||||
assert "0x2d0000" not in cmd_list
|
||||
|
||||
|
||||
def test_upload_using_esptool_with_file_path(
|
||||
tmp_path: Path,
|
||||
mock_run_external_command_main: Mock,
|
||||
|
||||
Reference in New Issue
Block a user