mirror of
https://github.com/esphome/esphome.git
synced 2026-06-29 03:05:28 +00:00
Compare commits
148 Commits
esp32-loop
...
dnm-memory
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5b1618331f | ||
|
|
3ad851993f | ||
|
|
0d7d091e71 | ||
|
|
0fcfd1e3d6 | ||
|
|
74a1ff9fc7 | ||
|
|
78d8a93fff | ||
|
|
92819d8658 | ||
|
|
7b8cbe2de1 | ||
|
|
3b0f669f47 | ||
|
|
87735d71a0 | ||
|
|
eba70dc193 | ||
|
|
712ef2ec0e | ||
|
|
2009f6cc5f | ||
|
|
89ddd34cb9 | ||
|
|
e4980713d1 | ||
|
|
997ab11687 | ||
|
|
792e1ff304 | ||
|
|
063770bcf4 | ||
|
|
6197282f1a | ||
|
|
9c0ffee020 | ||
|
|
1740e54105 | ||
|
|
070c14b04a | ||
|
|
559cfd1555 | ||
|
|
571a12ffe5 | ||
|
|
a4d247fa0a | ||
|
|
8e57894af7 | ||
|
|
f9aba18f8e | ||
|
|
a04f6da814 | ||
|
|
3f57117efd | ||
|
|
d7f809181a | ||
|
|
d7d20f4f6b | ||
|
|
ab46f8bd74 | ||
|
|
2454ad1645 | ||
|
|
4e48682468 | ||
|
|
805aa252d5 | ||
|
|
6116d10ab1 | ||
|
|
48844a68ba | ||
|
|
7865dc33bc | ||
|
|
bf62124032 | ||
|
|
95397948b9 | ||
|
|
f0202155b3 | ||
|
|
07a57d7557 | ||
|
|
091a05ccde | ||
|
|
dd961156d0 | ||
|
|
10abb0647c | ||
|
|
a85f8ad935 | ||
|
|
8945550c6c | ||
|
|
4b8e06b5bc | ||
|
|
f41866a9b8 | ||
|
|
5732d7135f | ||
|
|
ec597bfc03 | ||
|
|
9a6157b469 | ||
|
|
ac29fad120 | ||
|
|
e87190edb4 | ||
|
|
911e330c09 | ||
|
|
e64b6bc398 | ||
|
|
21e548f1d7 | ||
|
|
3cc875c40b | ||
|
|
7463a15c7e | ||
|
|
8d19c55be2 | ||
|
|
87d0e24d19 | ||
|
|
91ead4ff54 | ||
|
|
a6ef67aa65 | ||
|
|
e174c44b28 | ||
|
|
f728cb4373 | ||
|
|
6c4a8a3245 | ||
|
|
eb1196c6b2 | ||
|
|
fb0b73980b | ||
|
|
171ded35a5 | ||
|
|
b71d445e79 | ||
|
|
4d908798bc | ||
|
|
62b3b1cc75 | ||
|
|
52ead52ef2 | ||
|
|
96816e2491 | ||
|
|
bac62cb7de | ||
|
|
722cbfe843 | ||
|
|
88b12a1c45 | ||
|
|
ceb9d406e1 | ||
|
|
8b62cfded7 | ||
|
|
423b60c90c | ||
|
|
ae74920b81 | ||
|
|
ae814cff5c | ||
|
|
489cf483d0 | ||
|
|
dd0028c1b5 | ||
|
|
e492f8f8b6 | ||
|
|
b39b34bfe1 | ||
|
|
bbc24ab546 | ||
|
|
f1839489dd | ||
|
|
5172227931 | ||
|
|
97267105e1 | ||
|
|
8645f3672d | ||
|
|
a257edba62 | ||
|
|
61e8830a3c | ||
|
|
fc0a4e2201 | ||
|
|
0b780f1fd2 | ||
|
|
dcc30f8651 | ||
|
|
892e116680 | ||
|
|
1c7ae96e42 | ||
|
|
684bce8b9a | ||
|
|
7c494fd3ef | ||
|
|
cf1fabe6d4 | ||
|
|
cde52ef75e | ||
|
|
98e7213387 | ||
|
|
e7ab78366d | ||
|
|
e0167e9bdf | ||
|
|
62b0a93e5e | ||
|
|
1fb8c26704 | ||
|
|
3d1a614e55 | ||
|
|
917ffc3797 | ||
|
|
090f5a486a | ||
|
|
03e2eb4b4a | ||
|
|
ddd353d105 | ||
|
|
9a34a6aabb | ||
|
|
0babc52472 | ||
|
|
adde7681e8 | ||
|
|
8f6ea62628 | ||
|
|
4e7bc92061 | ||
|
|
1f4a061572 | ||
|
|
59db9a4673 | ||
|
|
7ae5566472 | ||
|
|
f247def4ac | ||
|
|
27d53ec117 | ||
|
|
0c94a173b6 | ||
|
|
ae2e372762 | ||
|
|
e6ed275746 | ||
|
|
878027ff50 | ||
|
|
858cfd5b94 | ||
|
|
5225416347 | ||
|
|
615d5aa827 | ||
|
|
e92a4c9472 | ||
|
|
32fa856bf0 | ||
|
|
cc88456ce7 | ||
|
|
79539cb85d | ||
|
|
16b6509a03 | ||
|
|
9fcb638f33 | ||
|
|
747787ae98 | ||
|
|
5cb7e62241 | ||
|
|
c17c4478ac | ||
|
|
750d52741a | ||
|
|
a37f27ee7f | ||
|
|
5f860ff5bd | ||
|
|
c951881eea | ||
|
|
5cb145a8c3 | ||
|
|
74001ccf05 | ||
|
|
58931f2610 | ||
|
|
f616103621 | ||
|
|
188ff7ebfd | ||
|
|
be99553fd4 |
@@ -1 +1 @@
|
||||
27aaab4e0ebfc10491720345aa746fc2dffa6a3985f73ec111b12dd99078d46f
|
||||
a30d2e50f2cac76e9c504eb7e5b250070dc92df23469c44a7eb8e52e26fd375d
|
||||
|
||||
@@ -29,7 +29,7 @@ Required fields:
|
||||
- **What does this implement/fix?**: Brief description of changes
|
||||
- **Types of changes**: Check ONE appropriate box (Bugfix, New feature, Breaking change, etc.)
|
||||
- **Related issue**: Use `fixes <link>` syntax if applicable
|
||||
- **Pull request in esphome-docs**: Link if docs are needed
|
||||
- **Pull request in esphome.io**: Link if docs are needed
|
||||
- **Test Environment**: Check platforms you tested on
|
||||
- **Example config.yaml**: Include working example YAML
|
||||
- **Checklist**: Verify code is tested and tests added
|
||||
@@ -54,9 +54,9 @@ Required fields:
|
||||
|
||||
- fixes https://github.com/esphome/esphome/issues/XXX
|
||||
|
||||
**Pull request in [esphome-docs](https://github.com/esphome/esphome-docs) with documentation (if applicable):**
|
||||
**Pull request in [esphome.io](https://github.com/esphome/esphome.io) with documentation (if applicable):**
|
||||
|
||||
- esphome/esphome-docs#XXX
|
||||
- esphome/esphome.io#XXX
|
||||
|
||||
## Test Environment
|
||||
|
||||
@@ -83,7 +83,7 @@ component_name:
|
||||
- [x] Tests have been added to verify that the new code works (under `tests/` folder).
|
||||
|
||||
If user exposed functionality or configuration variables are added/changed:
|
||||
- [ ] Documentation added/updated in [esphome-docs](https://github.com/esphome/esphome-docs).
|
||||
- [ ] Documentation added/updated in [esphome.io](https://github.com/esphome/esphome.io).
|
||||
```
|
||||
|
||||
## 5. Push and Create PR
|
||||
|
||||
2
.github/ISSUE_TEMPLATE/config.yml
vendored
2
.github/ISSUE_TEMPLATE/config.yml
vendored
@@ -2,7 +2,7 @@
|
||||
blank_issues_enabled: false
|
||||
contact_links:
|
||||
- name: Report an issue with the ESPHome documentation
|
||||
url: https://github.com/esphome/esphome-docs/issues/new/choose
|
||||
url: https://github.com/esphome/esphome.io/issues/new/choose
|
||||
about: Report an issue with the ESPHome documentation.
|
||||
- name: Report an issue with the ESPHome web server
|
||||
url: https://github.com/esphome/esphome-webserver/issues/new/choose
|
||||
|
||||
6
.github/PULL_REQUEST_TEMPLATE.md
vendored
6
.github/PULL_REQUEST_TEMPLATE.md
vendored
@@ -16,9 +16,9 @@
|
||||
|
||||
- fixes <link to issue>
|
||||
|
||||
**Pull request in [esphome-docs](https://github.com/esphome/esphome-docs) with documentation (if applicable):**
|
||||
**Pull request in [esphome.io](https://github.com/esphome/esphome.io) with documentation (if applicable):**
|
||||
|
||||
- esphome/esphome-docs#<esphome-docs PR number goes here>
|
||||
- esphome/esphome.io#<esphome.io PR number goes here>
|
||||
|
||||
## Test Environment
|
||||
|
||||
@@ -43,4 +43,4 @@
|
||||
- [ ] Tests have been added to verify that the new code works (under `tests/` folder).
|
||||
|
||||
If user exposed functionality or configuration variables are added/changed:
|
||||
- [ ] Documentation added/updated in [esphome-docs](https://github.com/esphome/esphome-docs).
|
||||
- [ ] Documentation added/updated in [esphome.io](https://github.com/esphome/esphome.io).
|
||||
|
||||
1
.github/dependabot.yml
vendored
1
.github/dependabot.yml
vendored
@@ -5,6 +5,7 @@ updates:
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: daily
|
||||
open-pull-requests-limit: 10
|
||||
ignore:
|
||||
# Hypotehsis is only used for testing and is updated quite often
|
||||
- dependency-name: hypothesis
|
||||
|
||||
3
.github/scripts/auto-label-pr/constants.js
vendored
3
.github/scripts/auto-label-pr/constants.js
vendored
@@ -35,6 +35,9 @@ module.exports = {
|
||||
],
|
||||
|
||||
DOCS_PR_PATTERNS: [
|
||||
/https:\/\/github\.com\/esphome\/esphome\.io\/pull\/\d+/,
|
||||
/esphome\/esphome\.io#\d+/,
|
||||
// Keep matching the old esphome-docs name during the transition period
|
||||
/https:\/\/github\.com\/esphome\/esphome-docs\/pull\/\d+/,
|
||||
/esphome\/esphome-docs#\d+/
|
||||
]
|
||||
|
||||
8
.github/scripts/auto-label-pr/detectors.js
vendored
8
.github/scripts/auto-label-pr/detectors.js
vendored
@@ -107,6 +107,8 @@ async function detectNewPlatforms(github, context, prFiles, apiData) {
|
||||
/^esphome\/components\/([^\/]+)\/([^\/]+)\/__init__\.py$/,
|
||||
];
|
||||
|
||||
const removedFiles = new Set(prFiles.filter(file => file.status === 'removed').map(file => file.filename));
|
||||
|
||||
for (const file of addedFiles) {
|
||||
for (const re of platformPathPatterns) {
|
||||
const match = file.match(re);
|
||||
@@ -114,6 +116,12 @@ async function detectNewPlatforms(github, context, prFiles, apiData) {
|
||||
const platform = match[2];
|
||||
if (!apiData.platformComponents.includes(platform)) break;
|
||||
|
||||
// Skip if this is a restructure between flat and subdirectory forms (either direction):
|
||||
// <component>/<platform>.py <-> <component>/<platform>/__init__.py
|
||||
const flatEquivalent = `esphome/components/${match[1]}/${platform}.py`;
|
||||
const subdirEquivalent = `esphome/components/${match[1]}/${platform}/__init__.py`;
|
||||
if (removedFiles.has(flatEquivalent) || removedFiles.has(subdirEquivalent)) break;
|
||||
|
||||
labels.add('new-platform');
|
||||
const content = await fetchPrFileContent(github, context, file);
|
||||
if (content === null) {
|
||||
|
||||
7
.github/scripts/auto-label-pr/package.json
vendored
Normal file
7
.github/scripts/auto-label-pr/package.json
vendored
Normal file
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"name": "auto-label-pr",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"test": "node --test tests/*.test.js"
|
||||
}
|
||||
}
|
||||
147
.github/scripts/auto-label-pr/tests/detectors.test.js
vendored
Normal file
147
.github/scripts/auto-label-pr/tests/detectors.test.js
vendored
Normal file
@@ -0,0 +1,147 @@
|
||||
const { describe, it } = require('node:test');
|
||||
const assert = require('node:assert/strict');
|
||||
const { detectNewPlatforms, detectNewComponents } = require('../detectors');
|
||||
|
||||
// Minimal GitHub API mock — only repos.getContent is called by detectNewPlatforms/detectNewComponents
|
||||
// to check for CONFIG_SCHEMA in newly added files.
|
||||
function makeGithub(content = '') {
|
||||
return {
|
||||
rest: {
|
||||
repos: {
|
||||
getContent: async () => ({
|
||||
data: { content: Buffer.from(content).toString('base64') }
|
||||
})
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
const CONTEXT = {
|
||||
repo: { owner: 'esphome', repo: 'esphome' },
|
||||
payload: { pull_request: { head: { sha: 'abc123' }, base: { ref: 'dev' } } }
|
||||
};
|
||||
|
||||
const API_DATA = {
|
||||
targetPlatforms: ['esp32', 'esp8266', 'rp2040'],
|
||||
platformComponents: ['cover', 'sensor', 'binary_sensor', 'switch', 'light', 'fan', 'climate', 'valve']
|
||||
};
|
||||
|
||||
const WITH_SCHEMA = 'CONFIG_SCHEMA = cv.Schema({})';
|
||||
const WITHOUT_SCHEMA = 'CODEOWNERS = ["@esphome/core"]';
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// detectNewPlatforms
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
describe('detectNewPlatforms', () => {
|
||||
describe('restructure detection (no false positives)', () => {
|
||||
it('flat .py -> subdir __init__.py is not a new platform', async () => {
|
||||
const prFiles = [
|
||||
{ filename: 'esphome/components/endstop/cover.py', status: 'removed' },
|
||||
{ filename: 'esphome/components/endstop/cover/__init__.py', status: 'added' },
|
||||
];
|
||||
const result = await detectNewPlatforms(makeGithub(WITH_SCHEMA), CONTEXT, prFiles, API_DATA);
|
||||
assert.equal(result.labels.size, 0);
|
||||
assert.equal(result.hasYamlLoadable, false);
|
||||
});
|
||||
|
||||
it('subdir __init__.py -> flat .py is not a new platform', async () => {
|
||||
const prFiles = [
|
||||
{ filename: 'esphome/components/endstop/cover/__init__.py', status: 'removed' },
|
||||
{ filename: 'esphome/components/endstop/cover.py', status: 'added' },
|
||||
];
|
||||
const result = await detectNewPlatforms(makeGithub(WITH_SCHEMA), CONTEXT, prFiles, API_DATA);
|
||||
assert.equal(result.labels.size, 0);
|
||||
assert.equal(result.hasYamlLoadable, false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('genuine new platforms', () => {
|
||||
it('new subdir platform with CONFIG_SCHEMA sets new-platform and hasYamlLoadable', async () => {
|
||||
const prFiles = [
|
||||
{ filename: 'esphome/components/my_sensor/cover/__init__.py', status: 'added' },
|
||||
];
|
||||
const result = await detectNewPlatforms(makeGithub(WITH_SCHEMA), CONTEXT, prFiles, API_DATA);
|
||||
assert.ok(result.labels.has('new-platform'));
|
||||
assert.equal(result.hasYamlLoadable, true);
|
||||
});
|
||||
|
||||
it('new flat platform with CONFIG_SCHEMA sets new-platform and hasYamlLoadable', async () => {
|
||||
const prFiles = [
|
||||
{ filename: 'esphome/components/my_sensor/cover.py', status: 'added' },
|
||||
];
|
||||
const result = await detectNewPlatforms(makeGithub(WITH_SCHEMA), CONTEXT, prFiles, API_DATA);
|
||||
assert.ok(result.labels.has('new-platform'));
|
||||
assert.equal(result.hasYamlLoadable, true);
|
||||
});
|
||||
|
||||
it('new platform without CONFIG_SCHEMA sets new-platform but not hasYamlLoadable', async () => {
|
||||
const prFiles = [
|
||||
{ filename: 'esphome/components/my_sensor/cover.py', status: 'added' },
|
||||
];
|
||||
const result = await detectNewPlatforms(makeGithub(WITHOUT_SCHEMA), CONTEXT, prFiles, API_DATA);
|
||||
assert.ok(result.labels.has('new-platform'));
|
||||
assert.equal(result.hasYamlLoadable, false);
|
||||
});
|
||||
|
||||
it('non-platform file addition produces no labels', async () => {
|
||||
const prFiles = [
|
||||
{ filename: 'esphome/components/my_sensor/sensor.py', status: 'added' },
|
||||
];
|
||||
// Override platformComponents so 'sensor' is not a recognized platform -> no label expected.
|
||||
const nonPlatformApiData = { ...API_DATA, platformComponents: ['cover'] };
|
||||
const result = await detectNewPlatforms(makeGithub(WITH_SCHEMA), CONTEXT, prFiles, nonPlatformApiData);
|
||||
assert.equal(result.labels.size, 0);
|
||||
assert.equal(result.hasYamlLoadable, false);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// detectNewComponents
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
describe('detectNewComponents', () => {
|
||||
it('new top-level __init__.py sets new-component', async () => {
|
||||
const prFiles = [
|
||||
{ filename: 'esphome/components/actuator/__init__.py', status: 'added', },
|
||||
];
|
||||
const result = await detectNewComponents(makeGithub(WITHOUT_SCHEMA), CONTEXT, prFiles);
|
||||
assert.ok(result.labels.has('new-component'));
|
||||
assert.equal(result.hasYamlLoadable, false);
|
||||
});
|
||||
|
||||
it('new top-level __init__.py with CONFIG_SCHEMA sets hasYamlLoadable', async () => {
|
||||
const prFiles = [
|
||||
{ filename: 'esphome/components/my_component/__init__.py', status: 'added' },
|
||||
];
|
||||
const result = await detectNewComponents(makeGithub(WITH_SCHEMA), CONTEXT, prFiles);
|
||||
assert.ok(result.labels.has('new-component'));
|
||||
assert.equal(result.hasYamlLoadable, true);
|
||||
});
|
||||
|
||||
it('new top-level __init__.py with IS_TARGET_PLATFORM sets new-target-platform', async () => {
|
||||
const prFiles = [
|
||||
{ filename: 'esphome/components/my_platform/__init__.py', status: 'added' },
|
||||
];
|
||||
const result = await detectNewComponents(makeGithub('IS_TARGET_PLATFORM = True'), CONTEXT, prFiles);
|
||||
assert.ok(result.labels.has('new-component'));
|
||||
assert.ok(result.labels.has('new-target-platform'));
|
||||
});
|
||||
|
||||
it('modified __init__.py does not set new-component', async () => {
|
||||
const prFiles = [
|
||||
{ filename: 'esphome/components/existing/__init__.py', status: 'modified' },
|
||||
];
|
||||
const result = await detectNewComponents(makeGithub(WITH_SCHEMA), CONTEXT, prFiles);
|
||||
assert.equal(result.labels.size, 0);
|
||||
});
|
||||
|
||||
it('nested __init__.py does not set new-component', async () => {
|
||||
const prFiles = [
|
||||
{ filename: 'esphome/components/endstop/cover/__init__.py', status: 'added' },
|
||||
];
|
||||
const result = await detectNewComponents(makeGithub(WITH_SCHEMA), CONTEXT, prFiles);
|
||||
assert.equal(result.labels.size, 0);
|
||||
});
|
||||
});
|
||||
2
.github/workflows/auto-label-pr.yml
vendored
2
.github/workflows/auto-label-pr.yml
vendored
@@ -24,7 +24,7 @@ jobs:
|
||||
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
|
||||
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||
|
||||
- name: Generate a token
|
||||
id: generate-token
|
||||
|
||||
2
.github/workflows/ci-api-proto.yml
vendored
2
.github/workflows/ci-api-proto.yml
vendored
@@ -21,7 +21,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
|
||||
with:
|
||||
|
||||
2
.github/workflows/ci-clang-tidy-hash.yml
vendored
2
.github/workflows/ci-clang-tidy-hash.yml
vendored
@@ -21,7 +21,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
|
||||
|
||||
2
.github/workflows/ci-docker.yml
vendored
2
.github/workflows/ci-docker.yml
vendored
@@ -42,7 +42,7 @@ jobs:
|
||||
- "docker"
|
||||
# - "lint"
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
|
||||
with:
|
||||
|
||||
27
.github/workflows/ci-github-scripts.yml
vendored
Normal file
27
.github/workflows/ci-github-scripts.yml
vendored
Normal file
@@ -0,0 +1,27 @@
|
||||
name: CI - GitHub Scripts
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [dev, beta, release]
|
||||
paths:
|
||||
- ".github/scripts/**"
|
||||
- ".github/workflows/ci-github-scripts.yml"
|
||||
pull_request:
|
||||
paths:
|
||||
- ".github/scripts/**"
|
||||
- ".github/workflows/ci-github-scripts.yml"
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
test-auto-label-pr:
|
||||
name: Test auto-label-pr scripts
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Check out code from GitHub
|
||||
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||
|
||||
- name: Run tests
|
||||
working-directory: .github/scripts/auto-label-pr
|
||||
run: npm test
|
||||
@@ -49,7 +49,7 @@ jobs:
|
||||
|
||||
- name: Check out code from base repository
|
||||
if: steps.pr.outputs.skip != 'true'
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||
with:
|
||||
# Always check out from the base repository (esphome/esphome), never from forks
|
||||
# Use the PR's target branch to ensure we run trusted code from the main repo
|
||||
|
||||
42
.github/workflows/ci.yml
vendored
42
.github/workflows/ci.yml
vendored
@@ -28,7 +28,7 @@ jobs:
|
||||
cache-key: ${{ steps.cache-key.outputs.key }}
|
||||
steps:
|
||||
- name: Check out code from GitHub
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||
- name: Generate cache-key
|
||||
id: cache-key
|
||||
run: echo key="${{ hashFiles('requirements.txt', 'requirements_dev.txt', 'requirements_test.txt', '.pre-commit-config.yaml') }}" >> $GITHUB_OUTPUT
|
||||
@@ -74,7 +74,7 @@ jobs:
|
||||
if: needs.determine-jobs.outputs.python-linters == 'true'
|
||||
steps:
|
||||
- name: Check out code from GitHub
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||
- name: Restore Python
|
||||
uses: ./.github/actions/restore-python
|
||||
with:
|
||||
@@ -97,7 +97,7 @@ jobs:
|
||||
if: needs.determine-jobs.outputs.core-ci == 'true'
|
||||
steps:
|
||||
- name: Check out code from GitHub
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||
- name: Restore Python
|
||||
uses: ./.github/actions/restore-python
|
||||
with:
|
||||
@@ -123,7 +123,7 @@ jobs:
|
||||
if: needs.determine-jobs.outputs.import-time == 'true'
|
||||
steps:
|
||||
- name: Check out code from GitHub
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||
- name: Restore Python
|
||||
uses: ./.github/actions/restore-python
|
||||
with:
|
||||
@@ -151,11 +151,11 @@ jobs:
|
||||
if: needs.determine-jobs.outputs.device-builder == 'true'
|
||||
steps:
|
||||
- name: Check out esphome (this PR)
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||
with:
|
||||
path: esphome
|
||||
- name: Check out esphome/device-builder
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||
with:
|
||||
repository: esphome/device-builder
|
||||
ref: main
|
||||
@@ -221,7 +221,7 @@ jobs:
|
||||
if: needs.determine-jobs.outputs.core-ci == 'true'
|
||||
steps:
|
||||
- name: Check out code from GitHub
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||
- name: Restore Python
|
||||
id: restore-python
|
||||
uses: ./.github/actions/restore-python
|
||||
@@ -281,7 +281,7 @@ jobs:
|
||||
benchmarks: ${{ steps.determine.outputs.benchmarks }}
|
||||
steps:
|
||||
- name: Check out code from GitHub
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||
with:
|
||||
# Fetch enough history to find the merge base
|
||||
fetch-depth: 2
|
||||
@@ -353,7 +353,7 @@ jobs:
|
||||
bucket: ${{ fromJson(needs.determine-jobs.outputs.integration-test-buckets) }}
|
||||
steps:
|
||||
- name: Check out code from GitHub
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||
- name: Set up Python 3.13
|
||||
id: python
|
||||
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
|
||||
@@ -405,7 +405,7 @@ jobs:
|
||||
if: github.event_name == 'pull_request' && (needs.determine-jobs.outputs.cpp-unit-tests-run-all == 'true' || needs.determine-jobs.outputs.cpp-unit-tests-components != '[]')
|
||||
steps:
|
||||
- name: Check out code from GitHub
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||
|
||||
- name: Restore Python
|
||||
uses: ./.github/actions/restore-python
|
||||
@@ -434,7 +434,7 @@ jobs:
|
||||
(github.event_name == 'pull_request' && needs.determine-jobs.outputs.benchmarks == 'true')
|
||||
steps:
|
||||
- name: Check out code from GitHub
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||
|
||||
- name: Restore Python
|
||||
uses: ./.github/actions/restore-python
|
||||
@@ -452,7 +452,7 @@ jobs:
|
||||
echo "binary=$BINARY" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Run CodSpeed benchmarks
|
||||
uses: CodSpeedHQ/action@3194d9a39c4d46684cb44bf7207fc56626aad8fd # v4.15.1
|
||||
uses: CodSpeedHQ/action@9d332c4d90b43981c3e55ae8e38e68709996240f # v4.17.0
|
||||
with:
|
||||
run: |
|
||||
. venv/bin/activate
|
||||
@@ -490,7 +490,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Check out code from GitHub
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||
with:
|
||||
# Need history for HEAD~1 to work for checking changed files
|
||||
fetch-depth: 2
|
||||
@@ -575,7 +575,7 @@ jobs:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
steps:
|
||||
- name: Check out code from GitHub
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||
with:
|
||||
# Need history for HEAD~1 to work for checking changed files
|
||||
fetch-depth: 2
|
||||
@@ -670,7 +670,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Check out code from GitHub
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||
with:
|
||||
# Need history for HEAD~1 to work for checking changed files
|
||||
fetch-depth: 2
|
||||
@@ -764,7 +764,7 @@ jobs:
|
||||
version: 1.0
|
||||
|
||||
- name: Check out code from GitHub
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||
- name: Restore Python
|
||||
uses: ./.github/actions/restore-python
|
||||
with:
|
||||
@@ -889,7 +889,7 @@ jobs:
|
||||
TEST_COMPONENTS: ${{ needs.determine-jobs.outputs.native-idf-components }}
|
||||
steps:
|
||||
- name: Check out code from GitHub
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||
|
||||
- name: Restore Python
|
||||
uses: ./.github/actions/restore-python
|
||||
@@ -971,7 +971,7 @@ jobs:
|
||||
if: github.event_name == 'pull_request' && !startsWith(github.base_ref, 'beta') && !startsWith(github.base_ref, 'release') && needs.determine-jobs.outputs.core-ci == 'true'
|
||||
steps:
|
||||
- name: Check out code from GitHub
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||
- name: Restore Python
|
||||
uses: ./.github/actions/restore-python
|
||||
with:
|
||||
@@ -997,7 +997,7 @@ jobs:
|
||||
skip: ${{ steps.check-script.outputs.skip || steps.check-tests.outputs.skip }}
|
||||
steps:
|
||||
- name: Check out target branch
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||
with:
|
||||
ref: ${{ github.base_ref }}
|
||||
|
||||
@@ -1179,7 +1179,7 @@ jobs:
|
||||
flash_usage: ${{ steps.extract.outputs.flash_usage }}
|
||||
steps:
|
||||
- name: Check out PR branch
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||
- name: Restore Python
|
||||
uses: ./.github/actions/restore-python
|
||||
with:
|
||||
@@ -1248,7 +1248,7 @@ jobs:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
steps:
|
||||
- name: Check out code
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||
- name: Restore Python
|
||||
uses: ./.github/actions/restore-python
|
||||
with:
|
||||
|
||||
@@ -26,7 +26,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout base branch
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||
with:
|
||||
ref: ${{ github.event.pull_request.base.sha }}
|
||||
sparse-checkout: |
|
||||
|
||||
@@ -29,7 +29,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout base branch
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||
with:
|
||||
ref: ${{ github.event.pull_request.base.sha }}
|
||||
|
||||
|
||||
6
.github/workflows/codeql.yml
vendored
6
.github/workflows/codeql.yml
vendored
@@ -52,11 +52,11 @@ jobs:
|
||||
# your codebase is analyzed, see https://docs.github.com/en/code-security/code-scanning/creating-an-advanced-setup-for-code-scanning/codeql-code-scanning-for-compiled-languages
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||
|
||||
# Initializes the CodeQL tools for scanning.
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@7211b7c8077ea37d8641b6271f6a365a22a5fbfa # v4.36.0
|
||||
uses: github/codeql-action/init@87557b9c84dde89fdd9b10e88954ac2f4248e463 # v4.36.1
|
||||
with:
|
||||
languages: ${{ matrix.language }}
|
||||
build-mode: ${{ matrix.build-mode }}
|
||||
@@ -84,6 +84,6 @@ jobs:
|
||||
exit 1
|
||||
|
||||
- name: Perform CodeQL Analysis
|
||||
uses: github/codeql-action/analyze@7211b7c8077ea37d8641b6271f6a365a22a5fbfa # v4.36.0
|
||||
uses: github/codeql-action/analyze@87557b9c84dde89fdd9b10e88954ac2f4248e463 # v4.36.1
|
||||
with:
|
||||
category: "/language:${{matrix.language}}"
|
||||
|
||||
2
.github/workflows/pr-title-check.yml
vendored
2
.github/workflows/pr-title-check.yml
vendored
@@ -16,7 +16,7 @@ jobs:
|
||||
name: Validate PR title
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||
|
||||
- uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
|
||||
with:
|
||||
|
||||
8
.github/workflows/release.yml
vendored
8
.github/workflows/release.yml
vendored
@@ -20,7 +20,7 @@ jobs:
|
||||
branch_build: ${{ steps.tag.outputs.branch_build }}
|
||||
deploy_env: ${{ steps.tag.outputs.deploy_env }}
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||
- name: Get tag
|
||||
id: tag
|
||||
# yamllint disable rule:line-length
|
||||
@@ -60,7 +60,7 @@ jobs:
|
||||
contents: read # actions/checkout to build the sdist/wheel
|
||||
id-token: write # OIDC token for PyPI Trusted Publishing (pypa/gh-action-pypi-publish)
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
|
||||
with:
|
||||
@@ -92,7 +92,7 @@ jobs:
|
||||
os: "ubuntu-24.04-arm"
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
|
||||
with:
|
||||
@@ -168,7 +168,7 @@ jobs:
|
||||
- ghcr
|
||||
- dockerhub
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||
|
||||
- name: Download digests
|
||||
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
|
||||
|
||||
4
.github/workflows/sync-device-classes.yml
vendored
4
.github/workflows/sync-device-classes.yml
vendored
@@ -28,10 +28,10 @@ jobs:
|
||||
permission-pull-requests: write # pulls.create / pulls.update to open or refresh the sync PR
|
||||
|
||||
- name: Checkout
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||
|
||||
- name: Checkout Home Assistant
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||
with:
|
||||
repository: home-assistant/core
|
||||
path: lib/home-assistant
|
||||
|
||||
@@ -11,7 +11,7 @@ ci:
|
||||
repos:
|
||||
- repo: https://github.com/astral-sh/ruff-pre-commit
|
||||
# Ruff version.
|
||||
rev: v0.15.14
|
||||
rev: v0.15.15
|
||||
hooks:
|
||||
# Run the linter.
|
||||
- id: ruff
|
||||
|
||||
@@ -462,7 +462,7 @@ This document provides essential context for AI models interacting with this pro
|
||||
6. **Pull Request:** Submit a PR against the `dev` branch. The Pull Request title should have a prefix of the component being worked on (e.g., `[display] Fix bug`, `[abc123] Add new component`). Update documentation, examples, and add `CODEOWNERS` entries as needed. Pull requests should always be made using the `.github/PULL_REQUEST_TEMPLATE.md` template - fill out all sections completely without removing any parts of the template.
|
||||
|
||||
* **Documentation Contributions:**
|
||||
* Documentation is hosted in the separate `esphome/esphome-docs` repository.
|
||||
* Documentation is hosted in the separate `esphome/esphome.io` repository.
|
||||
* The contribution workflow is the same as for the codebase.
|
||||
* When editing a component's documentation page, also update the corresponding component index page to ensure both pages remain in sync.
|
||||
|
||||
@@ -681,7 +681,7 @@ This document provides essential context for AI models interacting with this pro
|
||||
- [ ] Explored non-breaking alternatives
|
||||
- [ ] Added deprecation warnings if possible (use `ESPDEPRECATED` macro for C++)
|
||||
- [ ] Documented migration path in PR description with before/after examples
|
||||
- [ ] Updated all internal usage and esphome-docs
|
||||
- [ ] Updated all internal usage and esphome.io
|
||||
- [ ] Tested backward compatibility during deprecation period
|
||||
|
||||
* **Deprecation Pattern (C++):**
|
||||
|
||||
@@ -417,6 +417,7 @@ esphome/components/restart/* @esphome/core
|
||||
esphome/components/rf_bridge/* @jesserockz
|
||||
esphome/components/rgbct/* @jesserockz
|
||||
esphome/components/ring_buffer/* @kahrendt
|
||||
esphome/components/router/speaker/* @kahrendt
|
||||
esphome/components/rp2040/* @jesserockz
|
||||
esphome/components/rp2040_ble/* @bdraco
|
||||
esphome/components/rp2040_pio_led_strip/* @Papa-DMan
|
||||
|
||||
@@ -608,7 +608,7 @@ def run_miniterm(config: ConfigType, port: str, args) -> int:
|
||||
|
||||
try:
|
||||
module = importlib.import_module("esphome.components." + CORE.target_platform)
|
||||
process_stacktrace = getattr(module, "process_stacktrace")
|
||||
process_stacktrace = module.process_stacktrace
|
||||
except (AttributeError, ImportError):
|
||||
_LOGGER.info(
|
||||
'Stacktrace analysis is unavailable: no compatible analyzer found for target platform "%s".',
|
||||
@@ -639,7 +639,7 @@ def run_miniterm(config: ConfigType, port: str, args) -> int:
|
||||
chunk = ser.read(ser.in_waiting or 1)
|
||||
if not chunk:
|
||||
continue
|
||||
time_ = datetime.now()
|
||||
time_ = datetime.now().astimezone()
|
||||
milliseconds = time_.microsecond // 1000
|
||||
time_str = f"[{time_.hour:02}:{time_.minute:02}:{time_.second:02}.{milliseconds:03}]"
|
||||
|
||||
@@ -695,6 +695,11 @@ def _wrap_to_code(name, comp, yaml_util):
|
||||
def write_cpp(config: ConfigType) -> int:
|
||||
from esphome import writer
|
||||
|
||||
# Refresh the storage sidecar and clean an incompatible previous build
|
||||
# before regenerating any sources. This may full-wipe the build dir, so it
|
||||
# has to run before write_cpp_file writes src/.
|
||||
writer.update_storage_json()
|
||||
|
||||
if not get_bool_env(ENV_NOGITIGNORE):
|
||||
writer.write_gitignore()
|
||||
|
||||
@@ -760,6 +765,7 @@ def compile_program(args: ArgsProtocol, config: ConfigType) -> int:
|
||||
toolchain.create_factory_bin()
|
||||
toolchain.create_ota_bin()
|
||||
toolchain.create_elf_copy()
|
||||
toolchain.get_idedata()
|
||||
else:
|
||||
from esphome.platformio import toolchain
|
||||
|
||||
@@ -794,7 +800,7 @@ def _check_and_emit_build_info() -> None:
|
||||
|
||||
# Read build_info from JSON
|
||||
try:
|
||||
with open(build_info_json_path, encoding="utf-8") as f:
|
||||
with build_info_json_path.open(encoding="utf-8") as f:
|
||||
build_info = json.load(f)
|
||||
except (OSError, json.JSONDecodeError) as e:
|
||||
_LOGGER.debug("Failed to read build_info: %s", e)
|
||||
@@ -1056,7 +1062,7 @@ def _wait_for_serial_port(
|
||||
def _port_found() -> bool:
|
||||
if port is not None:
|
||||
if os.name == "posix":
|
||||
return os.path.exists(port)
|
||||
return Path(port).exists()
|
||||
return any(p.path == port for p in get_serial_ports())
|
||||
ports = get_serial_ports()
|
||||
if known_ports is not None:
|
||||
@@ -1101,7 +1107,7 @@ def upload_program(
|
||||
host = devices[0]
|
||||
try:
|
||||
module = importlib.import_module("esphome.components." + CORE.target_platform)
|
||||
if getattr(module, "upload_program")(config, args, host):
|
||||
if module.upload_program(config, args, host):
|
||||
return 0, host
|
||||
except AttributeError:
|
||||
pass
|
||||
@@ -1350,10 +1356,23 @@ def _validate_bootloader_binary(binary: Path) -> None:
|
||||
)
|
||||
|
||||
|
||||
def _should_subscribe_states(args: ArgsProtocol) -> bool:
|
||||
"""Determine whether entity state changes should be shown in log output.
|
||||
|
||||
The ``--states``/``--no-states`` command line flags take precedence. When
|
||||
neither is given, the ``ESPHOME_LOG_STATES`` environment variable controls
|
||||
the behavior, defaulting to showing states.
|
||||
"""
|
||||
states = getattr(args, "states", None)
|
||||
if states is not None:
|
||||
return states
|
||||
return get_bool_env("ESPHOME_LOG_STATES", True)
|
||||
|
||||
|
||||
def show_logs(config: ConfigType, args: ArgsProtocol, devices: list[str]) -> int | None:
|
||||
try:
|
||||
module = importlib.import_module("esphome.components." + CORE.target_platform)
|
||||
if getattr(module, "show_logs")(config, args, devices):
|
||||
if module.show_logs(config, args, devices):
|
||||
return 0
|
||||
except AttributeError:
|
||||
pass
|
||||
@@ -1379,7 +1398,7 @@ def show_logs(config: ConfigType, args: ArgsProtocol, devices: list[str]) -> int
|
||||
return run_logs(
|
||||
config,
|
||||
network_devices,
|
||||
subscribe_states=not getattr(args, "no_states", False),
|
||||
subscribe_states=_should_subscribe_states(args),
|
||||
)
|
||||
|
||||
if port_type in (PortType.NETWORK, PortType.MQTT) and has_mqtt_logging():
|
||||
@@ -1412,17 +1431,47 @@ def command_config(args: ArgsProtocol, config: ConfigType) -> int | None:
|
||||
if not CORE.verbose:
|
||||
config = strip_default_ids(config)
|
||||
output = yaml_util.dump(config, args.show_secrets)
|
||||
# add the console decoration so the front-end can hide the secrets
|
||||
if not args.show_secrets:
|
||||
output = re.sub(
|
||||
r"(password|key|psk|ssid)\: (.+)", r"\1: \\033[8m\2\\033[28m", output
|
||||
)
|
||||
output = _redact_with_legacy_fallback(output)
|
||||
if not CORE.quiet:
|
||||
safe_print(output)
|
||||
_LOGGER.info("Configuration is valid!")
|
||||
return 0
|
||||
|
||||
|
||||
# Legacy substring redaction fallback for unmigrated schemas; removed in
|
||||
# 2026.12.0 once canonical sensitive fields are tagged. The lookahead skips
|
||||
# values that already render themselves: ``\033[8m`` (SensitiveStr wrap),
|
||||
# ``!secret`` (preserves the user-friendly tag), ``!lambda`` (multi-line
|
||||
# block; first line is structural). The fragment must either start the
|
||||
# field name or follow ``_`` so the warning names a real field; this avoids
|
||||
# false positives like ``monkey:`` matching the ``key`` fragment.
|
||||
_LEGACY_REDACTION_RE = re.compile(
|
||||
r"(?P<key>\b(?:\w+_)?(?:password|key|psk|ssid))\: "
|
||||
r"(?!\\033\[8m|!secret\b|!lambda\b)(?P<val>.+)"
|
||||
)
|
||||
_LEGACY_REDACTION_REMOVAL = "2026.12.0"
|
||||
|
||||
|
||||
def _redact_with_legacy_fallback(output: str) -> str:
|
||||
unmarked: set[str] = set()
|
||||
|
||||
def _replace(m: re.Match[str]) -> str:
|
||||
unmarked.add(m.group("key"))
|
||||
return f"{m.group('key')}: \\033[8m{m.group('val')}\\033[28m"
|
||||
|
||||
output = _LEGACY_REDACTION_RE.sub(_replace, output)
|
||||
for key in sorted(unmarked):
|
||||
_LOGGER.warning(
|
||||
"Field '%s' is being redacted by a legacy substring heuristic. "
|
||||
"Mark this field's schema validator with cv.sensitive(...) for "
|
||||
"deterministic redaction; the heuristic will be removed in %s.",
|
||||
key,
|
||||
_LEGACY_REDACTION_REMOVAL,
|
||||
)
|
||||
return output
|
||||
|
||||
|
||||
def command_config_hash(args: ArgsProtocol, config: ConfigType) -> int | None:
|
||||
# generating code might modify config, so it must be done in order to generate
|
||||
# a hash that will match what was generated when compiling and then running
|
||||
@@ -1587,7 +1636,7 @@ def command_clean(args: ArgsProtocol, config: ConfigType) -> int | None:
|
||||
from esphome import writer
|
||||
|
||||
try:
|
||||
writer.clean_build()
|
||||
writer.clean_build(full=True)
|
||||
except OSError as err:
|
||||
_LOGGER.error("Error deleting build files: %s", err)
|
||||
return 1
|
||||
@@ -1800,7 +1849,7 @@ def command_analyze_memory(args: ArgsProtocol, config: ConfigType) -> int:
|
||||
ram_report = ram_analyzer.generate_report()
|
||||
print()
|
||||
print(ram_report)
|
||||
except Exception as e: # pylint: disable=broad-except
|
||||
except Exception as e: # noqa: BLE001 # pylint: disable=broad-except
|
||||
_LOGGER.warning("RAM strings analysis failed: %s", e)
|
||||
|
||||
return 0
|
||||
@@ -1988,6 +2037,29 @@ SIMPLE_CONFIG_ACTIONS = [
|
||||
]
|
||||
|
||||
|
||||
def _add_states_args(parser: argparse.ArgumentParser) -> None:
|
||||
"""Add mutually exclusive ``--states``/``--no-states`` flags to a parser.
|
||||
|
||||
When neither flag is given, the ``ESPHOME_LOG_STATES`` environment variable
|
||||
controls whether entity state changes are shown (defaulting to showing them).
|
||||
"""
|
||||
states_group = parser.add_mutually_exclusive_group()
|
||||
states_group.add_argument(
|
||||
"--states",
|
||||
dest="states",
|
||||
action="store_true",
|
||||
default=None,
|
||||
help="Show entity state changes in log output (overrides ESPHOME_LOG_STATES).",
|
||||
)
|
||||
states_group.add_argument(
|
||||
"--no-states",
|
||||
dest="states",
|
||||
action="store_false",
|
||||
default=None,
|
||||
help="Do not show entity state changes in log output.",
|
||||
)
|
||||
|
||||
|
||||
def parse_args(argv):
|
||||
options_parser = argparse.ArgumentParser(add_help=False)
|
||||
options_parser.add_argument(
|
||||
@@ -2164,11 +2236,7 @@ def parse_args(argv):
|
||||
help="Reset the device before starting serial logs.",
|
||||
default=os.getenv("ESPHOME_SERIAL_LOGGING_RESET"),
|
||||
)
|
||||
parser_logs.add_argument(
|
||||
"--no-states",
|
||||
action="store_true",
|
||||
help="Do not show entity state changes in log output.",
|
||||
)
|
||||
_add_states_args(parser_logs)
|
||||
|
||||
parser_discover = subparsers.add_parser(
|
||||
"discover",
|
||||
@@ -2200,11 +2268,7 @@ def parse_args(argv):
|
||||
"--no-logs", help="Disable starting logs.", action="store_true"
|
||||
)
|
||||
|
||||
parser_run.add_argument(
|
||||
"--no-states",
|
||||
action="store_true",
|
||||
help="Do not show entity state changes in log output.",
|
||||
)
|
||||
_add_states_args(parser_run)
|
||||
|
||||
parser_run.add_argument(
|
||||
"--reset",
|
||||
|
||||
@@ -6,6 +6,7 @@ from collections import defaultdict
|
||||
from collections.abc import Callable
|
||||
import heapq
|
||||
from operator import itemgetter
|
||||
from pathlib import Path
|
||||
import sys
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
@@ -509,7 +510,7 @@ class MemoryAnalyzerCLI(MemoryAnalyzer):
|
||||
lines.append(
|
||||
f"{_COMPONENT_CORE} Symbols > {self.SYMBOL_SIZE_THRESHOLD} B ({len(large_core_symbols)} symbols):"
|
||||
)
|
||||
for i, (symbol, demangled, size) in enumerate(large_core_symbols):
|
||||
for i, (_symbol, demangled, size) in enumerate(large_core_symbols):
|
||||
# Core symbols only track (symbol, demangled, size) without section info,
|
||||
# so we don't show section labels here
|
||||
lines.append(
|
||||
@@ -601,7 +602,7 @@ class MemoryAnalyzerCLI(MemoryAnalyzer):
|
||||
lines.append(
|
||||
f"{comp_name} Symbols > {self.SYMBOL_SIZE_THRESHOLD} B & storage ({len(large_symbols)} symbols):"
|
||||
)
|
||||
for i, (symbol, demangled, size, section) in enumerate(large_symbols):
|
||||
for i, (_symbol, demangled, size, section) in enumerate(large_symbols):
|
||||
lines.append(
|
||||
f"{i + 1}. {self._format_symbol_with_section(demangled, size, section)}"
|
||||
)
|
||||
@@ -640,7 +641,7 @@ class MemoryAnalyzerCLI(MemoryAnalyzer):
|
||||
lines.append(
|
||||
f" Symbols > {self.RAM_SYMBOL_SIZE_THRESHOLD} B ({len(large_ram_syms)}):"
|
||||
)
|
||||
for symbol, demangled, size, section in large_ram_syms[:10]:
|
||||
for _symbol, demangled, size, section in large_ram_syms[:10]:
|
||||
# Format section label consistently by stripping leading dot
|
||||
section_label = section.lstrip(".") if section else ""
|
||||
display_name = _format_pstorage_name(demangled)
|
||||
@@ -699,7 +700,7 @@ class MemoryAnalyzerCLI(MemoryAnalyzer):
|
||||
content = "\n".join(lines)
|
||||
|
||||
if output_file:
|
||||
with open(output_file, "w", encoding="utf-8") as f:
|
||||
with Path(output_file).open("w", encoding="utf-8") as f:
|
||||
f.write(content)
|
||||
else:
|
||||
print(content)
|
||||
@@ -737,7 +738,6 @@ def main():
|
||||
|
||||
# Load build directory
|
||||
import json
|
||||
from pathlib import Path
|
||||
|
||||
from esphome.platformio.toolchain import IDEData
|
||||
|
||||
@@ -785,7 +785,7 @@ def main():
|
||||
if not idedata_path.exists():
|
||||
continue
|
||||
try:
|
||||
with open(idedata_path, encoding="utf-8") as f:
|
||||
with idedata_path.open(encoding="utf-8") as f:
|
||||
raw_data = json.load(f)
|
||||
idedata = IDEData(raw_data)
|
||||
print(f"Loaded idedata from: {idedata_path}", file=sys.stderr)
|
||||
|
||||
@@ -154,7 +154,7 @@ def batch_demangle(
|
||||
failed_count = 0
|
||||
|
||||
for original, stripped, prefix, demangled in zip(
|
||||
symbols, symbols_stripped, symbols_prefixes, demangled_lines
|
||||
symbols, symbols_stripped, symbols_prefixes, demangled_lines, strict=True
|
||||
):
|
||||
# Add back any prefix that was removed
|
||||
demangled = _restore_symbol_prefix(prefix, stripped, demangled)
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
import os
|
||||
from pathlib import Path
|
||||
import subprocess
|
||||
from typing import TYPE_CHECKING
|
||||
@@ -37,7 +36,7 @@ def _find_in_platformio_packages(tool_name: str) -> str | None:
|
||||
Full path to the tool or None if not found
|
||||
"""
|
||||
# Get PlatformIO packages directory
|
||||
platformio_home = Path(os.path.expanduser("~/.platformio/packages"))
|
||||
platformio_home = Path("~/.platformio/packages").expanduser()
|
||||
if not platformio_home.exists():
|
||||
return None
|
||||
|
||||
|
||||
@@ -45,7 +45,7 @@ class AsyncThreadRunner(threading.Thread, Generic[_T]):
|
||||
async def _runner(self) -> None:
|
||||
try:
|
||||
self.result = await self._coro_factory()
|
||||
except Exception as exc: # pylint: disable=broad-except
|
||||
except Exception as exc: # noqa: BLE001 # pylint: disable=broad-except
|
||||
# Capture all exceptions so ``event`` is always set — otherwise a
|
||||
# crash would hang the waiter forever.
|
||||
self.exception = exc
|
||||
|
||||
@@ -7,7 +7,6 @@ from esphome.components.esp32 import get_esp32_variant, idf_version
|
||||
import esphome.config_validation as cv
|
||||
from esphome.core import CORE
|
||||
from esphome.helpers import mkdir_p, write_file_if_changed
|
||||
from esphome.writer import update_storage_json
|
||||
|
||||
|
||||
def get_available_components() -> list[str] | None:
|
||||
@@ -24,7 +23,7 @@ def get_available_components() -> list[str] | None:
|
||||
return None
|
||||
|
||||
try:
|
||||
with open(project_desc, encoding="utf-8") as f:
|
||||
with project_desc.open(encoding="utf-8") as f:
|
||||
data = json.load(f)
|
||||
|
||||
component_info = data.get("build_component_info", {})
|
||||
@@ -213,11 +212,6 @@ target_link_options(${{COMPONENT_LIB}} PUBLIC
|
||||
|
||||
def write_project(minimal: bool = False) -> None:
|
||||
"""Write ESP-IDF project files."""
|
||||
# Refresh <data_dir>/storage/<name>.yaml.json so the dashboard's
|
||||
# /info and /downloads endpoints can locate the build (they 404
|
||||
# otherwise). This mirrors the PlatformIO build-gen path's call
|
||||
# in build_gen/platformio.py:write_ini().
|
||||
update_storage_json()
|
||||
mkdir_p(CORE.build_path)
|
||||
mkdir_p(CORE.relative_src_path())
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
from esphome.const import __version__
|
||||
from esphome.core import CORE
|
||||
from esphome.helpers import mkdir_p, read_file, write_file_if_changed
|
||||
from esphome.writer import find_begin_end, update_storage_json
|
||||
from esphome.writer import find_begin_end
|
||||
|
||||
INI_AUTO_GENERATE_BEGIN = "; ========== AUTO GENERATED CODE BEGIN ==========="
|
||||
INI_AUTO_GENERATE_END = "; =========== AUTO GENERATED CODE END ============"
|
||||
@@ -58,7 +58,6 @@ def get_ini_content():
|
||||
|
||||
|
||||
def write_ini(content):
|
||||
update_storage_json()
|
||||
path = CORE.relative_build_path("platformio.ini")
|
||||
|
||||
if path.is_file():
|
||||
|
||||
@@ -412,7 +412,7 @@ class ConfigBundleCreator:
|
||||
@staticmethod
|
||||
def _add_to_tar(tar: tarfile.TarFile, bf: BundleFile) -> None:
|
||||
"""Add a BundleFile to the tar archive with deterministic metadata."""
|
||||
with open(bf.source, "rb") as f:
|
||||
with bf.source.open("rb") as f:
|
||||
_add_bytes_to_tar(tar, bf.path, f.read())
|
||||
|
||||
|
||||
|
||||
@@ -43,7 +43,7 @@ def save_compiled_config(config: ConfigType) -> None:
|
||||
try:
|
||||
rendered = yaml_util.dump(config, show_secrets=True)
|
||||
write_file(compiled_config_path(CORE.config_filename), rendered, private=True)
|
||||
except Exception as err: # pylint: disable=broad-except
|
||||
except Exception as err: # noqa: BLE001 # pylint: disable=broad-except
|
||||
_LOGGER.debug("Skipping compiled config cache write: %s", err)
|
||||
|
||||
|
||||
@@ -62,7 +62,7 @@ def load_compiled_config(conf_path: Path) -> ConfigType | None:
|
||||
|
||||
try:
|
||||
config = yaml_util.load_yaml(cache_path, clear_secrets=False)
|
||||
except Exception: # pylint: disable=broad-except
|
||||
except Exception: # noqa: BLE001 # pylint: disable=broad-except
|
||||
return None
|
||||
|
||||
storage = StorageJSON.load(ext_storage_path(conf_path.name))
|
||||
|
||||
@@ -234,7 +234,7 @@ ACTIONS_SCHEMA = automation.validate_automation(
|
||||
|
||||
ENCRYPTION_SCHEMA = cv.Schema(
|
||||
{
|
||||
cv.Optional(CONF_KEY): validate_encryption_key,
|
||||
cv.Optional(CONF_KEY): cv.sensitive(validate_encryption_key),
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@@ -1169,7 +1169,7 @@ void APIConnection::on_camera_image_request(const CameraImageRequest &msg) {
|
||||
void APIConnection::on_get_time_response(const GetTimeResponse &value) {
|
||||
if (homeassistant::global_homeassistant_time != nullptr) {
|
||||
homeassistant::global_homeassistant_time->set_epoch_time(value.epoch_seconds);
|
||||
#ifdef USE_TIME_TIMEZONE
|
||||
#if defined(USE_HOMEASSISTANT_TIMEZONE) && defined(USE_TIME_TIMEZONE)
|
||||
if (!value.timezone.empty()) {
|
||||
// Check if the sender provided pre-parsed timezone data.
|
||||
// If std_offset is non-zero or DST rules are present, the parsed data was populated.
|
||||
@@ -1306,6 +1306,9 @@ void APIConnection::on_voice_assistant_announce_request(const VoiceAssistantAnno
|
||||
bool APIConnection::send_voice_assistant_get_configuration_response_(const VoiceAssistantConfigurationRequest &msg) {
|
||||
VoiceAssistantConfigurationResponse resp;
|
||||
if (!this->check_voice_assistant_api_connection_()) {
|
||||
// send_message encodes synchronously, so this stack local outlives the encode
|
||||
const std::vector<std::string> empty_wake_words;
|
||||
resp.active_wake_words = &empty_wake_words;
|
||||
return this->send_message(resp);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
#include "api_server.h"
|
||||
#ifdef USE_API
|
||||
#include <cerrno>
|
||||
#include <cinttypes>
|
||||
#include "api_connection.h"
|
||||
#include "esphome/components/network/util.h"
|
||||
#include "esphome/core/application.h"
|
||||
@@ -677,7 +678,7 @@ uint32_t APIServer::register_active_action_call(uint32_t client_call_id, APIConn
|
||||
// Schedule automatic cleanup after timeout (client will have given up by then)
|
||||
// Uses numeric ID overload to avoid heap allocation from str_sprintf
|
||||
this->set_timeout(action_call_id, USE_API_ACTION_CALL_TIMEOUT_MS, [this, action_call_id]() {
|
||||
ESP_LOGD(TAG, "Action call %u timed out", action_call_id);
|
||||
ESP_LOGD(TAG, "Action call %" PRIu32 " timed out", action_call_id);
|
||||
this->unregister_active_action_call(action_call_id);
|
||||
});
|
||||
|
||||
@@ -721,7 +722,7 @@ void APIServer::send_action_response(uint32_t action_call_id, bool success, Stri
|
||||
return;
|
||||
}
|
||||
}
|
||||
ESP_LOGW(TAG, "Cannot send response: no active call found for action_call_id %u", action_call_id);
|
||||
ESP_LOGW(TAG, "Cannot send response: no active call found for action_call_id %" PRIu32, action_call_id);
|
||||
}
|
||||
#ifdef USE_API_USER_DEFINED_ACTION_RESPONSES_JSON
|
||||
void APIServer::send_action_response(uint32_t action_call_id, bool success, StringRef error_message,
|
||||
@@ -733,7 +734,7 @@ void APIServer::send_action_response(uint32_t action_call_id, bool success, Stri
|
||||
return;
|
||||
}
|
||||
}
|
||||
ESP_LOGW(TAG, "Cannot send response: no active call found for action_call_id %u", action_call_id);
|
||||
ESP_LOGW(TAG, "Cannot send response: no active call found for action_call_id %" PRIu32, action_call_id);
|
||||
}
|
||||
#endif // USE_API_USER_DEFINED_ACTION_RESPONSES_JSON
|
||||
#endif // USE_API_USER_DEFINED_ACTION_RESPONSES
|
||||
|
||||
@@ -101,13 +101,14 @@ async def async_run_logs(
|
||||
client_info=f"ESPHome Logs {__version__}",
|
||||
noise_psk=noise_psk,
|
||||
addresses=addresses, # Pass all addresses for automatic retry
|
||||
provide_time=False,
|
||||
)
|
||||
|
||||
# Try platform-specific stacktrace handler first, fall back to generic
|
||||
platform_process_stacktrace = None
|
||||
try:
|
||||
module = importlib.import_module("esphome.components." + CORE.target_platform)
|
||||
platform_process_stacktrace = getattr(module, "process_stacktrace")
|
||||
platform_process_stacktrace = module.process_stacktrace
|
||||
except (AttributeError, ImportError):
|
||||
_LOGGER.info(
|
||||
'Stacktrace analysis is unavailable: no compatible analyzer found for target platform "%s".',
|
||||
@@ -118,7 +119,7 @@ async def async_run_logs(
|
||||
|
||||
def on_log(msg: SubscribeLogsResponse) -> None:
|
||||
"""Handle a new log message."""
|
||||
time_ = datetime.now()
|
||||
time_ = datetime.now().astimezone()
|
||||
message: bytes = msg.message
|
||||
text = message.decode("utf8", "backslashreplace")
|
||||
nanoseconds = time_.microsecond // 1000
|
||||
|
||||
@@ -100,7 +100,7 @@ def position(min=-MAX_POSITION, max=MAX_POSITION):
|
||||
if isinstance(value, str) and value.endswith("%"):
|
||||
value = percent_to_position(value)
|
||||
|
||||
if isinstance(value, str) and (value.endswith("°") or value.endswith("deg")):
|
||||
if isinstance(value, str) and value.endswith(("°", "deg")):
|
||||
return angle_to_position(
|
||||
value,
|
||||
min=round(min * POSITION_TO_ANGLE),
|
||||
|
||||
@@ -9,9 +9,12 @@ namespace esphome::audio {
|
||||
|
||||
static const char *const TAG = "audio.decoder";
|
||||
|
||||
static const uint32_t DECODING_TIMEOUT_MS = 50; // The decode function will yield after this duration
|
||||
static const uint32_t READ_WRITE_TIMEOUT_MS = 20; // Timeout for transferring audio data
|
||||
|
||||
// Max consecutive decode iterations that consume input but produce no output; e.g., skipping a large metadata block,
|
||||
// before yielding and returning.
|
||||
static const uint8_t MAX_NO_OUTPUT_ITERATIONS = 32;
|
||||
|
||||
static const uint32_t MAX_POTENTIALLY_FAILED_COUNT = 10;
|
||||
|
||||
AudioDecoder::AudioDecoder(size_t input_buffer_size, size_t output_buffer_size)
|
||||
@@ -20,11 +23,13 @@ AudioDecoder::AudioDecoder(size_t input_buffer_size, size_t output_buffer_size)
|
||||
}
|
||||
|
||||
esp_err_t AudioDecoder::add_source(std::weak_ptr<ring_buffer::RingBuffer> &input_ring_buffer) {
|
||||
auto source = AudioSourceTransferBuffer::create(this->input_buffer_size_);
|
||||
// Zero-copy source reading directly from the ring buffer's internal storage. Raw file data is byte
|
||||
// aligned, so no frame alignment is required.
|
||||
auto source = RingBufferAudioSource::create(input_ring_buffer.lock(), this->input_buffer_size_);
|
||||
if (source == nullptr) {
|
||||
return ESP_ERR_NO_MEM;
|
||||
// create() only returns nullptr for invalid arguments (expired ring buffer or zero buffer size)
|
||||
return ESP_ERR_INVALID_ARG;
|
||||
}
|
||||
source->set_source(input_ring_buffer);
|
||||
this->input_buffer_ = std::move(source);
|
||||
return ESP_OK;
|
||||
}
|
||||
@@ -141,13 +146,7 @@ AudioDecoderState AudioDecoder::decode(bool stop_gracefully) {
|
||||
}
|
||||
|
||||
FileDecoderState state = FileDecoderState::MORE_TO_PROCESS;
|
||||
|
||||
uint32_t decoding_start = millis();
|
||||
|
||||
bool first_loop_iteration = true;
|
||||
|
||||
size_t bytes_processed = 0;
|
||||
size_t bytes_available_before_processing = 0;
|
||||
uint8_t no_output_iterations = 0;
|
||||
|
||||
while (state == FileDecoderState::MORE_TO_PROCESS) {
|
||||
// Transfer decoded out
|
||||
@@ -161,45 +160,39 @@ AudioDecoderState AudioDecoder::decode(bool stop_gracefully) {
|
||||
this->playback_ms_ +=
|
||||
this->audio_stream_info_.value().frames_to_milliseconds_with_remainder(&this->accumulated_frames_written_);
|
||||
}
|
||||
|
||||
if ((bytes_written > 0) && (this->output_transfer_buffer_->available() == 0)) {
|
||||
// All decoded audio has been flushed to the sink; return so the caller can react to stop/pause before
|
||||
// decoding the next batch
|
||||
return AudioDecoderState::DECODING;
|
||||
}
|
||||
} else {
|
||||
// If paused, block to avoid wasting CPU resources
|
||||
delay(READ_WRITE_TIMEOUT_MS);
|
||||
}
|
||||
|
||||
// Verify there is enough space to store more decoded audio and that the function hasn't been running too long
|
||||
if ((this->output_transfer_buffer_->free() < this->free_buffer_required_) ||
|
||||
(millis() - decoding_start > DECODING_TIMEOUT_MS)) {
|
||||
if (this->output_transfer_buffer_->available() > 0) {
|
||||
// Output transfer buffer indicates backpressure, return so caller can handle other events;
|
||||
// e.g., stop/pause, before trying again
|
||||
return AudioDecoderState::DECODING;
|
||||
}
|
||||
|
||||
// Decode more audio
|
||||
|
||||
// Never shift the input buffer; every decoder buffers internally and consumes only what it processed.
|
||||
size_t bytes_read = this->input_buffer_->fill(pdMS_TO_TICKS(READ_WRITE_TIMEOUT_MS), false);
|
||||
|
||||
if (!first_loop_iteration && (this->input_buffer_->available() < bytes_processed)) {
|
||||
// Less data is available than what was processed in last iteration, so don't attempt to decode.
|
||||
// This attempts to avoid the decoder from consistently trying to decode an incomplete frame. The transfer buffer
|
||||
// will shift the remaining data to the start and copy more from the source the next time the decode function is
|
||||
// called
|
||||
break;
|
||||
// Reaching here means no decoded output is pending (any would have returned above). Bounds long no-output
|
||||
// stretches; e.g., skipping a large metadata block, so a source that keeps the ring buffer full can't spin this
|
||||
// loop without yielding and trip the watchdog. The delay yields allowing other tasks to feed the watchdog and
|
||||
// the return keeps stop/pause responsive.
|
||||
if (++no_output_iterations >= MAX_NO_OUTPUT_ITERATIONS) {
|
||||
delay(1);
|
||||
return AudioDecoderState::DECODING;
|
||||
}
|
||||
|
||||
bytes_available_before_processing = this->input_buffer_->available();
|
||||
// Expose the next chunk of file data. Every decoder buffers internally and consumes only what it
|
||||
// processed, so the source does not need to accumulate or stitch chunks across fill() calls.
|
||||
this->input_buffer_->fill(pdMS_TO_TICKS(READ_WRITE_TIMEOUT_MS), false);
|
||||
|
||||
if ((this->potentially_failed_count_ > 0) && (bytes_read == 0)) {
|
||||
// Failed to decode in last attempt and there is no new data
|
||||
const size_t available_before_decode = this->input_buffer_->available();
|
||||
|
||||
if ((this->input_buffer_->free() == 0) && first_loop_iteration) {
|
||||
// The input buffer is full (or read-only, e.g. const flash source). Since it previously failed on the exact
|
||||
// same data, we can never recover. For const sources this is correct: the entire file is already available, so
|
||||
// a decode failure is genuine, not a transient out-of-data condition.
|
||||
state = FileDecoderState::FAILED;
|
||||
} else {
|
||||
// Attempt to get more data next time
|
||||
state = FileDecoderState::IDLE;
|
||||
}
|
||||
} else if (this->input_buffer_->available() == 0) {
|
||||
if (available_before_decode == 0) {
|
||||
// No data to decode, attempt to get more data next time
|
||||
state = FileDecoderState::IDLE;
|
||||
} else {
|
||||
@@ -231,9 +224,6 @@ AudioDecoderState AudioDecoder::decode(bool stop_gracefully) {
|
||||
}
|
||||
}
|
||||
|
||||
first_loop_iteration = false;
|
||||
bytes_processed = bytes_available_before_processing - this->input_buffer_->available();
|
||||
|
||||
if (state == FileDecoderState::POTENTIALLY_FAILED) {
|
||||
++this->potentially_failed_count_;
|
||||
} else if (state == FileDecoderState::END_OF_FILE) {
|
||||
@@ -241,7 +231,16 @@ AudioDecoderState AudioDecoder::decode(bool stop_gracefully) {
|
||||
} else if (state == FileDecoderState::FAILED) {
|
||||
return AudioDecoderState::FAILED;
|
||||
} else if (state == FileDecoderState::MORE_TO_PROCESS) {
|
||||
this->potentially_failed_count_ = 0;
|
||||
// Reset the failsafe only when the iteration made forward progress: input was consumed or output was
|
||||
// produced (output_transfer_buffer_ is drained empty above, so any available bytes are new). A
|
||||
// MORE_TO_PROCESS that neither consumes input nor produces output means the decoder is stalled; count it
|
||||
// toward the failsafe so a stuck stream eventually surfaces as FAILED instead of looping forever.
|
||||
if ((this->input_buffer_->available() < available_before_decode) ||
|
||||
(this->output_transfer_buffer_->available() > 0)) {
|
||||
this->potentially_failed_count_ = 0;
|
||||
} else {
|
||||
++this->potentially_failed_count_;
|
||||
}
|
||||
}
|
||||
}
|
||||
return AudioDecoderState::DECODING;
|
||||
|
||||
@@ -61,15 +61,16 @@ class AudioDecoder {
|
||||
*/
|
||||
public:
|
||||
/// @brief Allocates the output transfer buffer and stores the input buffer size for later use by add_source()
|
||||
/// @param input_buffer_size Size of the input transfer buffer in bytes.
|
||||
/// @param input_buffer_size Soft cap on the bytes a ring buffer source exposes per fill, in bytes.
|
||||
/// @param output_buffer_size Size of the output transfer buffer in bytes.
|
||||
AudioDecoder(size_t input_buffer_size, size_t output_buffer_size);
|
||||
|
||||
~AudioDecoder() = default;
|
||||
|
||||
/// @brief Adds a source ring buffer for raw file data. Takes ownership of the ring buffer in a shared_ptr.
|
||||
/// @param input_ring_buffer weak_ptr of a shared_ptr of the sink ring buffer to transfer ownership
|
||||
/// @return ESP_OK if successsful, ESP_ERR_NO_MEM if the transfer buffer wasn't allocated
|
||||
/// @brief Adds a source ring buffer for raw file data. Shares ownership of the ring buffer via a shared_ptr.
|
||||
/// The decoder reads directly from the ring buffer's internal storage with a zero-copy RingBufferAudioSource.
|
||||
/// @param input_ring_buffer weak_ptr of the source ring buffer to read from
|
||||
/// @return ESP_OK if successful, ESP_ERR_INVALID_ARG if the ring buffer is expired or the buffer size is zero
|
||||
esp_err_t add_source(std::weak_ptr<ring_buffer::RingBuffer> &input_ring_buffer);
|
||||
|
||||
/// @brief Adds a sink ring buffer for decoded audio. Takes ownership of the ring buffer in a shared_ptr.
|
||||
|
||||
@@ -12,16 +12,17 @@ static const uint32_t READ_WRITE_TIMEOUT_MS = 20;
|
||||
|
||||
AudioResampler::AudioResampler(size_t input_buffer_size, size_t output_buffer_size)
|
||||
: input_buffer_size_(input_buffer_size), output_buffer_size_(output_buffer_size) {
|
||||
this->input_transfer_buffer_ = AudioSourceTransferBuffer::create(input_buffer_size);
|
||||
this->output_transfer_buffer_ = AudioSinkTransferBuffer::create(output_buffer_size);
|
||||
}
|
||||
|
||||
esp_err_t AudioResampler::add_source(std::weak_ptr<ring_buffer::RingBuffer> &input_ring_buffer) {
|
||||
if (this->input_transfer_buffer_ != nullptr) {
|
||||
this->input_transfer_buffer_->set_source(input_ring_buffer);
|
||||
return ESP_OK;
|
||||
// The zero-copy RingBufferAudioSource is created lazily on the first resample() call, once both the ring
|
||||
// buffer (stored here) and the input stream info (set by start()) are available, in either order.
|
||||
this->source_ring_buffer_ = input_ring_buffer.lock();
|
||||
if (this->source_ring_buffer_ == nullptr) {
|
||||
return ESP_ERR_INVALID_STATE;
|
||||
}
|
||||
return ESP_ERR_NO_MEM;
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
esp_err_t AudioResampler::add_sink(std::weak_ptr<ring_buffer::RingBuffer> &output_ring_buffer) {
|
||||
@@ -47,7 +48,7 @@ esp_err_t AudioResampler::start(AudioStreamInfo &input_stream_info, AudioStreamI
|
||||
this->input_stream_info_ = input_stream_info;
|
||||
this->output_stream_info_ = output_stream_info;
|
||||
|
||||
if ((this->input_transfer_buffer_ == nullptr) || (this->output_transfer_buffer_ == nullptr)) {
|
||||
if (this->output_transfer_buffer_ == nullptr) {
|
||||
return ESP_ERR_NO_MEM;
|
||||
}
|
||||
|
||||
@@ -56,6 +57,13 @@ esp_err_t AudioResampler::start(AudioStreamInfo &input_stream_info, AudioStreamI
|
||||
return ESP_ERR_NOT_SUPPORTED;
|
||||
}
|
||||
|
||||
// Reject frame sizes that can't be used as the zero-copy source's alignment up front, where the caller checks
|
||||
// the return code. The lazy create() in resample() keeps its own guard since it runs before the uint8_t cast.
|
||||
const size_t bytes_per_frame = this->input_stream_info_.frames_to_bytes(1);
|
||||
if ((bytes_per_frame == 0) || (bytes_per_frame > RingBufferAudioSource::MAX_ALIGNMENT_BYTES)) {
|
||||
return ESP_ERR_NOT_SUPPORTED;
|
||||
}
|
||||
|
||||
if ((input_stream_info.get_sample_rate() != output_stream_info.get_sample_rate()) ||
|
||||
(input_stream_info.get_bits_per_sample() != output_stream_info.get_bits_per_sample())) {
|
||||
this->resampler_ = make_unique<esp_audio_libs::resampler::Resampler>(
|
||||
@@ -87,8 +95,27 @@ esp_err_t AudioResampler::start(AudioStreamInfo &input_stream_info, AudioStreamI
|
||||
}
|
||||
|
||||
AudioResamplerState AudioResampler::resample(bool stop_gracefully, int32_t *ms_differential) {
|
||||
if (this->audio_source_ == nullptr) {
|
||||
// Lazily create the zero-copy source on first use. Frame-aligned reads ensure multi-channel frames are
|
||||
// never split across the ring buffer's wrap boundary.
|
||||
const size_t bytes_per_frame = this->input_stream_info_.frames_to_bytes(1);
|
||||
if ((bytes_per_frame == 0) || (bytes_per_frame > RingBufferAudioSource::MAX_ALIGNMENT_BYTES)) {
|
||||
// Stream info is unset or the frame is too large to use as an alignment; the uint8_t cast below would
|
||||
// truncate it and could yield a source that tears frames.
|
||||
return AudioResamplerState::FAILED;
|
||||
}
|
||||
// Pass the shared_ptr by copy so a failed create() leaves source_ring_buffer_ intact; release our
|
||||
// reference only after the source has taken ownership.
|
||||
this->audio_source_ = RingBufferAudioSource::create(this->source_ring_buffer_, this->input_buffer_size_,
|
||||
static_cast<uint8_t>(bytes_per_frame));
|
||||
if (this->audio_source_ == nullptr) {
|
||||
return AudioResamplerState::FAILED;
|
||||
}
|
||||
this->source_ring_buffer_.reset();
|
||||
}
|
||||
|
||||
if (stop_gracefully) {
|
||||
if (!this->input_transfer_buffer_->has_buffered_data() && (this->output_transfer_buffer_->available() == 0)) {
|
||||
if (!this->audio_source_->has_buffered_data() && (this->output_transfer_buffer_->available() == 0)) {
|
||||
return AudioResamplerState::FINISHED;
|
||||
}
|
||||
}
|
||||
@@ -102,9 +129,11 @@ AudioResamplerState AudioResampler::resample(bool stop_gracefully, int32_t *ms_d
|
||||
delay(READ_WRITE_TIMEOUT_MS);
|
||||
}
|
||||
|
||||
this->input_transfer_buffer_->transfer_data_from_source(pdMS_TO_TICKS(READ_WRITE_TIMEOUT_MS));
|
||||
// Expose a chunk of the ring buffer's internal storage. pre_shift is ignored by RingBufferAudioSource
|
||||
// (there is no intermediate transfer buffer to compact).
|
||||
this->audio_source_->fill(pdMS_TO_TICKS(READ_WRITE_TIMEOUT_MS), false);
|
||||
|
||||
if (this->input_transfer_buffer_->available() == 0) {
|
||||
if (this->audio_source_->available() == 0) {
|
||||
// No samples available to process
|
||||
return AudioResamplerState::RESAMPLING;
|
||||
}
|
||||
@@ -112,17 +141,17 @@ AudioResamplerState AudioResampler::resample(bool stop_gracefully, int32_t *ms_d
|
||||
const size_t bytes_free = this->output_transfer_buffer_->free();
|
||||
const uint32_t frames_free = this->output_stream_info_.bytes_to_frames(bytes_free);
|
||||
|
||||
const size_t bytes_available = this->input_transfer_buffer_->available();
|
||||
const size_t bytes_available = this->audio_source_->available();
|
||||
const uint32_t frames_available = this->input_stream_info_.bytes_to_frames(bytes_available);
|
||||
|
||||
if ((this->input_stream_info_.get_sample_rate() != this->output_stream_info_.get_sample_rate()) ||
|
||||
(this->input_stream_info_.get_bits_per_sample() != this->output_stream_info_.get_bits_per_sample())) {
|
||||
// Adjust gain by -3 dB to avoid clipping due to the resampling process
|
||||
esp_audio_libs::resampler::ResamplerResults results =
|
||||
this->resampler_->resample(this->input_transfer_buffer_->get_buffer_start(),
|
||||
this->output_transfer_buffer_->get_buffer_end(), frames_available, frames_free, -3);
|
||||
this->resampler_->resample(this->audio_source_->data(), this->output_transfer_buffer_->get_buffer_end(),
|
||||
frames_available, frames_free, -3);
|
||||
|
||||
this->input_transfer_buffer_->decrease_buffer_length(this->input_stream_info_.frames_to_bytes(results.frames_used));
|
||||
this->audio_source_->consume(this->input_stream_info_.frames_to_bytes(results.frames_used));
|
||||
this->output_transfer_buffer_->increase_buffer_length(
|
||||
this->output_stream_info_.frames_to_bytes(results.frames_generated));
|
||||
|
||||
@@ -146,10 +175,10 @@ AudioResamplerState AudioResampler::resample(bool stop_gracefully, int32_t *ms_d
|
||||
const size_t bytes_to_transfer = std::min(this->output_stream_info_.frames_to_bytes(frames_free),
|
||||
this->input_stream_info_.frames_to_bytes(frames_available));
|
||||
|
||||
std::memcpy((void *) this->output_transfer_buffer_->get_buffer_end(),
|
||||
(void *) this->input_transfer_buffer_->get_buffer_start(), bytes_to_transfer);
|
||||
std::memcpy((void *) this->output_transfer_buffer_->get_buffer_end(), (const void *) this->audio_source_->data(),
|
||||
bytes_to_transfer);
|
||||
|
||||
this->input_transfer_buffer_->decrease_buffer_length(bytes_to_transfer);
|
||||
this->audio_source_->consume(bytes_to_transfer);
|
||||
this->output_transfer_buffer_->increase_buffer_length(bytes_to_transfer);
|
||||
}
|
||||
|
||||
|
||||
@@ -22,7 +22,7 @@ namespace esphome::audio {
|
||||
enum class AudioResamplerState : uint8_t {
|
||||
RESAMPLING, // More data is available to resample
|
||||
FINISHED, // All file data has been resampled and transferred
|
||||
FAILED, // Unused state included for consistency among Audio classes
|
||||
FAILED, // Failed to allocate the audio source
|
||||
};
|
||||
|
||||
class AudioResampler {
|
||||
@@ -32,14 +32,16 @@ class AudioResampler {
|
||||
* component). Also supports converting bits per sample.
|
||||
*/
|
||||
public:
|
||||
/// @brief Allocates the input and output transfer buffers
|
||||
/// @param input_buffer_size Size of the input transfer buffer in bytes.
|
||||
/// @brief Allocates the output transfer buffer. The input source is created later in resample().
|
||||
/// @param input_buffer_size Max bytes exposed per fill() call on the zero-copy input source.
|
||||
/// @param output_buffer_size Size of the output transfer buffer in bytes.
|
||||
AudioResampler(size_t input_buffer_size, size_t output_buffer_size);
|
||||
|
||||
/// @brief Adds a source ring buffer for audio data. Takes ownership of the ring buffer in a shared_ptr.
|
||||
/// @param input_ring_buffer weak_ptr of a shared_ptr of the sink ring buffer to transfer ownership
|
||||
/// @return ESP_OK if successsful, ESP_ERR_NO_MEM if the transfer buffer wasn't allocated
|
||||
/// @brief Sets the ring buffer the audio is read from and takes shared ownership of it. The zero-copy
|
||||
/// RingBufferAudioSource that reads directly from its internal storage is created lazily on the first
|
||||
/// resample() call, so add_source() and start() may be called in any order.
|
||||
/// @param input_ring_buffer weak_ptr of a shared_ptr of the source ring buffer to transfer ownership
|
||||
/// @return ESP_OK if successful, ESP_ERR_INVALID_STATE if the ring buffer is no longer alive
|
||||
esp_err_t add_source(std::weak_ptr<ring_buffer::RingBuffer> &input_ring_buffer);
|
||||
|
||||
/// @brief Adds a sink ring buffer for resampled audio. Takes ownership of the ring buffer in a shared_ptr.
|
||||
@@ -78,7 +80,8 @@ class AudioResampler {
|
||||
void set_pause_output_state(bool pause_state) { this->pause_output_ = pause_state; }
|
||||
|
||||
protected:
|
||||
std::unique_ptr<AudioSourceTransferBuffer> input_transfer_buffer_;
|
||||
std::shared_ptr<ring_buffer::RingBuffer> source_ring_buffer_;
|
||||
std::unique_ptr<RingBufferAudioSource> audio_source_;
|
||||
std::unique_ptr<AudioSinkTransferBuffer> output_transfer_buffer_;
|
||||
|
||||
size_t input_buffer_size_;
|
||||
|
||||
@@ -252,6 +252,22 @@ void RingBufferAudioSource::consume(size_t bytes) {
|
||||
}
|
||||
}
|
||||
|
||||
void RingBufferAudioSource::clear_buffered_data() {
|
||||
// Release the held item before reset() so the source no longer references memory the reset will reclaim.
|
||||
if (this->acquired_item_ != nullptr) {
|
||||
this->ring_buffer_->receive_release(this->acquired_item_);
|
||||
this->acquired_item_ = nullptr;
|
||||
}
|
||||
this->current_data_ = nullptr;
|
||||
this->current_available_ = 0;
|
||||
this->queued_data_ = nullptr;
|
||||
this->queued_length_ = 0;
|
||||
this->item_trailing_ptr_ = nullptr;
|
||||
this->item_trailing_length_ = 0;
|
||||
this->splice_length_ = 0;
|
||||
this->ring_buffer_->reset();
|
||||
}
|
||||
|
||||
bool RingBufferAudioSource::has_buffered_data() const {
|
||||
// splice_length_ is deliberately not considered here. It holds an incomplete frame whose completion
|
||||
// bytes must still arrive through the ring buffer, which ring_buffer_->available() already reports.
|
||||
|
||||
@@ -250,6 +250,10 @@ class RingBufferAudioSource : public AudioReadableBuffer {
|
||||
/// exposure stays in place and fill() returns 0 until it is fully consumed.
|
||||
size_t fill(TickType_t ticks_to_wait, bool pre_shift) override;
|
||||
|
||||
/// @brief Discards all buffered audio: releases any held ring buffer item, clears the source's in-flight
|
||||
/// state, and resets the underlying ring buffer. Must be invoked from the ring buffer's consumer thread.
|
||||
void clear_buffered_data();
|
||||
|
||||
/// @brief Returns a mutable pointer to the currently exposed audio data.
|
||||
/// The pointer may reference the ring buffer's internal storage or, when exposing a stitched frame
|
||||
/// across a wrap boundary, an internal splice buffer. In either case mutations are safe but data
|
||||
|
||||
@@ -72,7 +72,7 @@ def _file_schema(value: ConfigType | str) -> ConfigType:
|
||||
|
||||
def _validate_file_shorthand(value: str) -> ConfigType:
|
||||
value = cv.string_strict(value)
|
||||
if value.startswith("http://") or value.startswith("https://"):
|
||||
if value.startswith(("http://", "https://")):
|
||||
return _file_schema(
|
||||
{
|
||||
CONF_TYPE: TYPE_WEB,
|
||||
@@ -98,7 +98,7 @@ def read_audio_file_and_type(file_config: ConfigType) -> tuple[bytes, MockObj]:
|
||||
else:
|
||||
raise cv.Invalid("Unsupported file source")
|
||||
|
||||
with open(path, "rb") as f:
|
||||
with path.open("rb") as f:
|
||||
data = f.read()
|
||||
|
||||
try:
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
from typing import Any
|
||||
|
||||
import esphome.codegen as cg
|
||||
from esphome.components import audio, esp32, media_source, psram
|
||||
from esphome.components import audio, media_source, psram
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import CONF_ID, CONF_TASK_STACK_IN_PSRAM
|
||||
from esphome.types import ConfigType
|
||||
@@ -21,19 +19,13 @@ def _request_micro_decoder(config: ConfigType) -> ConfigType:
|
||||
return config
|
||||
|
||||
|
||||
def _validate_task_stack_in_psram(value: Any) -> bool:
|
||||
if value := cv.boolean(value):
|
||||
return cv.requires_component(psram.DOMAIN)(value)
|
||||
return value
|
||||
|
||||
|
||||
CONFIG_SCHEMA = cv.All(
|
||||
media_source.media_source_schema(
|
||||
AudioFileMediaSource,
|
||||
)
|
||||
.extend(
|
||||
{
|
||||
cv.Optional(CONF_TASK_STACK_IN_PSRAM): _validate_task_stack_in_psram,
|
||||
cv.Optional(CONF_TASK_STACK_IN_PSRAM): psram.validate_task_stack_in_psram,
|
||||
}
|
||||
)
|
||||
.extend(cv.COMPONENT_SCHEMA),
|
||||
@@ -49,6 +41,4 @@ async def to_code(config: ConfigType) -> None:
|
||||
|
||||
if config.get(CONF_TASK_STACK_IN_PSRAM):
|
||||
cg.add(var.set_task_stack_in_psram(True))
|
||||
esp32.add_idf_sdkconfig_option(
|
||||
"CONFIG_SPIRAM_ALLOW_STACK_EXTERNAL_MEMORY", True
|
||||
)
|
||||
psram.request_external_task_stack()
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
from typing import Any
|
||||
|
||||
import esphome.codegen as cg
|
||||
from esphome.components import audio, esp32, media_source, psram
|
||||
from esphome.components import audio, media_source, psram
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import CONF_BUFFER_SIZE, CONF_ID, CONF_TASK_STACK_IN_PSRAM
|
||||
from esphome.types import ConfigType
|
||||
@@ -20,14 +18,6 @@ def _request_micro_decoder(config: ConfigType) -> ConfigType:
|
||||
return config
|
||||
|
||||
|
||||
def _validate_task_stack_in_psram(value: Any) -> bool:
|
||||
# Only require the psram component when actually enabling PSRAM stacks; validating
|
||||
# the boolean first means `false` doesn't trigger the requires_component check.
|
||||
if value := cv.boolean(value):
|
||||
return cv.requires_component(psram.DOMAIN)(value)
|
||||
return value
|
||||
|
||||
|
||||
CONFIG_SCHEMA = cv.All(
|
||||
media_source.media_source_schema(
|
||||
AudioHTTPMediaSource,
|
||||
@@ -37,7 +27,7 @@ CONFIG_SCHEMA = cv.All(
|
||||
cv.Optional(CONF_BUFFER_SIZE, default=50000): cv.int_range(
|
||||
min=5000, max=1000000
|
||||
),
|
||||
cv.Optional(CONF_TASK_STACK_IN_PSRAM): _validate_task_stack_in_psram,
|
||||
cv.Optional(CONF_TASK_STACK_IN_PSRAM): psram.validate_task_stack_in_psram,
|
||||
}
|
||||
)
|
||||
.extend(cv.COMPONENT_SCHEMA),
|
||||
@@ -53,7 +43,5 @@ async def to_code(config: ConfigType) -> None:
|
||||
|
||||
if config.get(CONF_TASK_STACK_IN_PSRAM):
|
||||
cg.add(var.set_task_stack_in_psram(True))
|
||||
esp32.add_idf_sdkconfig_option(
|
||||
"CONFIG_SPIRAM_ALLOW_STACK_EXTERNAL_MEMORY", True
|
||||
)
|
||||
psram.request_external_task_stack()
|
||||
cg.add(var.set_buffer_size(config[CONF_BUFFER_SIZE]))
|
||||
|
||||
@@ -135,12 +135,26 @@ void BluetoothConnection::loop() {
|
||||
// - For V3_WITH_CACHE: Services are never sent, disable after INIT state
|
||||
// - For V3_WITHOUT_CACHE: Disable only after service discovery is complete
|
||||
// (send_service_ == DONE_SENDING_SERVICES, which is only set after services are sent)
|
||||
if (this->state() != espbt::ClientState::INIT && (this->connection_type_ == espbt::ConnectionType::V3_WITH_CACHE ||
|
||||
this->send_service_ == DONE_SENDING_SERVICES)) {
|
||||
// Never disable while DISCONNECTING — BLEClientBase::loop() needs to keep running so the
|
||||
// 10s safety timeout can force IDLE if CLOSE_EVT is never delivered.
|
||||
if (this->state() != espbt::ClientState::INIT && this->state() != espbt::ClientState::DISCONNECTING &&
|
||||
(this->connection_type_ == espbt::ConnectionType::V3_WITH_CACHE ||
|
||||
this->send_service_ == DONE_SENDING_SERVICES)) {
|
||||
this->disable_loop();
|
||||
}
|
||||
}
|
||||
|
||||
void BluetoothConnection::on_disconnect_complete(esp_err_t reason) {
|
||||
// Called from both the CLOSE_EVT handler and the DISCONNECTING safety timeout in the
|
||||
// base class. Free the proxy slot, notify the API client, and reset send_service_.
|
||||
// address_ may already be 0 if reset_connection_ ran earlier on this teardown.
|
||||
if (this->address_ == 0) {
|
||||
return;
|
||||
}
|
||||
ESP_LOGD(TAG, "[%d] [%s] Close, reason=0x%02x, freeing slot", this->connection_index_, this->address_str_, reason);
|
||||
this->reset_connection_(reason);
|
||||
}
|
||||
|
||||
void BluetoothConnection::reset_connection_(esp_err_t reason) {
|
||||
// Send disconnection notification
|
||||
this->proxy_->send_device_connection(this->address_, false, 0, reason);
|
||||
@@ -372,14 +386,6 @@ bool BluetoothConnection::gattc_event_handler(esp_gattc_cb_event_t event, esp_ga
|
||||
this->proxy_->send_device_connection(this->address_, false, 0, param->disconnect.reason);
|
||||
break;
|
||||
}
|
||||
case ESP_GATTC_CLOSE_EVT: {
|
||||
ESP_LOGD(TAG, "[%d] [%s] Close, reason=0x%02x, freeing slot", this->connection_index_, this->address_str_,
|
||||
param->close.reason);
|
||||
// Now the GATT connection is fully closed and controller resources are freed
|
||||
// Safe to mark the connection slot as available
|
||||
this->reset_connection_(param->close.reason);
|
||||
break;
|
||||
}
|
||||
case ESP_GATTC_OPEN_EVT: {
|
||||
if (param->open.status != ESP_GATT_OK && param->open.status != ESP_GATT_ALREADY_OPEN) {
|
||||
this->reset_connection_(param->open.status);
|
||||
|
||||
@@ -33,6 +33,8 @@ class BluetoothConnection final : public esp32_ble_client::BLEClientBase {
|
||||
protected:
|
||||
friend class BluetoothProxy;
|
||||
|
||||
void on_disconnect_complete(esp_err_t reason) override;
|
||||
|
||||
bool supports_efficient_uuids_() const;
|
||||
void send_service_for_discovery_();
|
||||
void reset_connection_(esp_err_t reason);
|
||||
|
||||
@@ -169,7 +169,7 @@ async def to_code_base(config):
|
||||
path = _compute_local_file_path(_compute_url(config))
|
||||
|
||||
try:
|
||||
with open(path, encoding="utf-8") as f:
|
||||
with path.open(encoding="utf-8") as f:
|
||||
bsec2_iaq_config = f.read()
|
||||
except Exception as e:
|
||||
raise core.EsphomeError(
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
import esphome.codegen as cg
|
||||
from esphome.components.esp32 import add_idf_component
|
||||
from esphome.components.esp32 import (
|
||||
add_idf_component,
|
||||
require_libc_picolibc_newlib_compat,
|
||||
)
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import CONF_BUFFER_SIZE, CONF_ID, CONF_TYPE
|
||||
from esphome.types import ConfigType
|
||||
@@ -51,6 +54,8 @@ async def to_code(config: ConfigType) -> None:
|
||||
cg.add(buffer.set_buffer_size(config[CONF_BUFFER_SIZE]))
|
||||
if config[CONF_TYPE] == ESP32_CAMERA_ENCODER:
|
||||
add_idf_component(name="espressif/esp32-camera", ref="2.1.5")
|
||||
# esp32-camera 2.1.5 needs the Newlib shim on IDF 6.0+; remove when fixed upstream
|
||||
require_libc_picolibc_newlib_compat()
|
||||
cg.add_define("USE_ESP32_CAMERA_JPEG_ENCODER")
|
||||
var = cg.new_Pvariable(
|
||||
config[CONF_ID],
|
||||
|
||||
@@ -22,6 +22,7 @@ CONF_PARITY = "parity"
|
||||
CONF_RECEIVER_FREQUENCY = "receiver_frequency"
|
||||
CONF_REQUEST_HEADERS = "request_headers"
|
||||
CONF_ROWS = "rows"
|
||||
CONF_SHA256 = "sha256"
|
||||
CONF_STOP_BITS = "stop_bits"
|
||||
CONF_USE_PSRAM = "use_psram"
|
||||
CONF_VOLUME_INCREMENT = "volume_increment"
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
// DNM: do not merge. Trivial touch to mark daikin changed; this is the working
|
||||
// esp8266-ard component that should generate memory impact (see PR #16788).
|
||||
#include "daikin.h"
|
||||
#include "esphome/components/remote_base/remote_base.h"
|
||||
|
||||
|
||||
@@ -3,11 +3,18 @@ from dataclasses import dataclass
|
||||
from esphome import automation, core
|
||||
from esphome.automation import maybe_simple_id
|
||||
import esphome.codegen as cg
|
||||
from esphome.components.const import KEY_METADATA
|
||||
from esphome.components.const import (
|
||||
BYTE_ORDER_BIG,
|
||||
CONF_BYTE_ORDER,
|
||||
CONF_DRAW_ROUNDING,
|
||||
KEY_METADATA,
|
||||
)
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import (
|
||||
CONF_AUTO_CLEAR_ENABLED,
|
||||
CONF_DIMENSIONS,
|
||||
CONF_FROM,
|
||||
CONF_HEIGHT,
|
||||
CONF_ID,
|
||||
CONF_LAMBDA,
|
||||
CONF_PAGE_ID,
|
||||
@@ -16,10 +23,11 @@ from esphome.const import (
|
||||
CONF_TO,
|
||||
CONF_TRIGGER_ID,
|
||||
CONF_UPDATE_INTERVAL,
|
||||
CONF_WIDTH,
|
||||
SCHEDULER_DONT_RUN,
|
||||
)
|
||||
from esphome.core import CORE, CoroPriority, coroutine_with_priority
|
||||
from esphome.cpp_generator import MockObj
|
||||
from esphome.core import CORE, ID, CoroPriority, coroutine_with_priority
|
||||
from esphome.final_validate import full_config
|
||||
|
||||
DOMAIN = "display"
|
||||
IS_PLATFORM_COMPONENT = True
|
||||
@@ -159,29 +167,97 @@ async def setup_display_core_(var, config):
|
||||
class DisplayMetaData:
|
||||
width: int = 0
|
||||
height: int = 0
|
||||
has_writer: bool = False
|
||||
has_hardware_rotation: bool = False
|
||||
byte_order: str = BYTE_ORDER_BIG
|
||||
has_writer: bool = False
|
||||
rotation: int = 0
|
||||
draw_rounding: int = 0
|
||||
|
||||
|
||||
def _get_metadata_list() -> list[tuple]:
|
||||
"""Get the raw metadata list. Each entry is (id, DisplayMetaData)."""
|
||||
return CORE.data.setdefault(DOMAIN, {}).setdefault(KEY_METADATA, [])
|
||||
|
||||
|
||||
def get_all_display_metadata() -> dict[str, DisplayMetaData]:
|
||||
"""Get all display metadata."""
|
||||
return CORE.data.setdefault(DOMAIN, {}).setdefault(KEY_METADATA, {})
|
||||
"""Get all display metadata as a dict keyed by resolved ID strings.
|
||||
|
||||
Must not be called before IDs have been finalised.
|
||||
"""
|
||||
entries = _get_metadata_list()
|
||||
assert all(id_.id is not None for id_, _ in entries), (
|
||||
"get_all_display_metadata called before display IDs have been resolved"
|
||||
)
|
||||
return {id_.id: meta for id_, meta in entries}
|
||||
|
||||
|
||||
def get_display_metadata(display_id: str) -> DisplayMetaData | None:
|
||||
"""Get display metadata by ID for use by other components."""
|
||||
return get_all_display_metadata().get(display_id, DisplayMetaData())
|
||||
def get_display_metadata(display_id: ID) -> DisplayMetaData:
|
||||
"""Get display metadata by ID object
|
||||
|
||||
Must not be called before IDs have been finalised.
|
||||
"""
|
||||
for id_, meta in _get_metadata_list():
|
||||
if id_ is display_id:
|
||||
return meta
|
||||
assert id_.id is not None, (
|
||||
"get_display_metadata called before display IDs have been resolved"
|
||||
)
|
||||
if id_.id == display_id.id:
|
||||
return meta
|
||||
# No metadata found, display driver may not yet support it.
|
||||
# Read the raw config to populate the returned data
|
||||
global_config = full_config.get()
|
||||
path = global_config.get_path_for_id(display_id)[:-1]
|
||||
disp_config = global_config.get_config_for_path(path)
|
||||
dimensions = disp_config.get(CONF_DIMENSIONS, (0, 0))
|
||||
if isinstance(dimensions, dict):
|
||||
dimensions = (dimensions.get(CONF_WIDTH, 0), dimensions.get(CONF_HEIGHT, 0))
|
||||
elif not isinstance(dimensions, tuple) or len(dimensions) != 2:
|
||||
dimensions = (0, 0)
|
||||
|
||||
meta = DisplayMetaData(
|
||||
width=dimensions[0],
|
||||
height=dimensions[1],
|
||||
has_hardware_rotation=False,
|
||||
byte_order=disp_config.get(CONF_BYTE_ORDER, cv.UNDEFINED),
|
||||
has_writer=disp_config.get(CONF_AUTO_CLEAR_ENABLED) is True
|
||||
or disp_config.get(CONF_PAGES) is not None
|
||||
or disp_config.get(CONF_LAMBDA) is not None
|
||||
or disp_config.get(CONF_SHOW_TEST_CARD) is True,
|
||||
rotation=disp_config.get(CONF_ROTATION, 0),
|
||||
draw_rounding=disp_config.get(CONF_DRAW_ROUNDING, 0),
|
||||
)
|
||||
_get_metadata_list().append((display_id, meta))
|
||||
return meta
|
||||
|
||||
|
||||
def add_metadata(
|
||||
id: str | MockObj,
|
||||
id: ID,
|
||||
width: int,
|
||||
height: int,
|
||||
has_writer: bool,
|
||||
has_hardware_rotation: bool = False,
|
||||
byte_order: str = BYTE_ORDER_BIG,
|
||||
has_writer: bool = False,
|
||||
rotation: int = 0,
|
||||
draw_rounding: int = 0,
|
||||
):
|
||||
get_all_display_metadata()[str(id)] = DisplayMetaData(
|
||||
width, height, has_writer, has_hardware_rotation
|
||||
entries = _get_metadata_list()
|
||||
assert not any(existing_id is id for existing_id, _ in entries), (
|
||||
f"Duplicate display metadata for ID {id}"
|
||||
)
|
||||
entries.append(
|
||||
(
|
||||
id,
|
||||
DisplayMetaData(
|
||||
width=width,
|
||||
height=height,
|
||||
has_hardware_rotation=has_hardware_rotation,
|
||||
byte_order=byte_order,
|
||||
has_writer=has_writer,
|
||||
rotation=rotation,
|
||||
draw_rounding=draw_rounding,
|
||||
),
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -87,7 +87,7 @@ async def to_code(config):
|
||||
cg.add_build_flag("-DDSMR_WATER_MBUS_ID=" + str(config[CONF_WATER_MBUS_ID]))
|
||||
cg.add_build_flag("-DDSMR_THERMAL_MBUS_ID=" + str(config[CONF_THERMAL_MBUS_ID]))
|
||||
|
||||
cg.add_library("esphome/dsmr_parser", "1.4.0")
|
||||
cg.add_library("esphome/dsmr_parser", "1.8.0")
|
||||
|
||||
|
||||
def final_validate(config: ConfigType) -> ConfigType:
|
||||
|
||||
@@ -153,8 +153,9 @@ void Dsmr::receive_encrypted_telegram_() {
|
||||
bool Dsmr::parse_telegram_(const dsmr_parser::DsmrUnencryptedTelegram &telegram) {
|
||||
this->stop_requesting_data_();
|
||||
|
||||
ESP_LOGV(TAG, "Trying to parse telegram (%zu bytes)", telegram.content().size());
|
||||
ESP_LOGVV(TAG, "Telegram content:\n %.*s", static_cast<int>(telegram.content().size()), telegram.content().data());
|
||||
ESP_LOGV(TAG, "Trying to parse telegram (%zu bytes)", telegram.full_content().size());
|
||||
ESP_LOGVV(TAG, "Telegram content:\n %.*s", static_cast<int>(telegram.full_content().size()),
|
||||
telegram.full_content().data());
|
||||
|
||||
MyData data;
|
||||
if (const bool res = dsmr_parser::DsmrParser::parse(data, telegram); !res) {
|
||||
@@ -167,7 +168,7 @@ bool Dsmr::parse_telegram_(const dsmr_parser::DsmrUnencryptedTelegram &telegram)
|
||||
|
||||
// Publish the telegram, after publishing the sensors so it can also trigger action based on latest values
|
||||
if (this->s_telegram_ != nullptr) {
|
||||
this->s_telegram_->publish_state(telegram.content().data(), telegram.content().size());
|
||||
this->s_telegram_->publish_state(telegram.full_content().data(), telegram.full_content().size());
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -16,9 +16,14 @@
|
||||
#include <span>
|
||||
#include <vector>
|
||||
|
||||
// On ESP8266 Arduino, BearSSL is the native crypto. The mbedtls headers can
|
||||
// still be in scope when a sibling component (e.g. wireguard) pulls in
|
||||
// esp_mbedtls_esp8266, but that build leaves MBEDTLS_GCM_C disabled so the
|
||||
// gcm.h symbols are unresolved at link time. Force BearSSL on ESP8266 to
|
||||
// avoid that linker error.
|
||||
#if __has_include(<psa/crypto.h>)
|
||||
#include <dsmr_parser/decryption/aes128gcm_tfpsa.h>
|
||||
#elif __has_include(<mbedtls/gcm.h>)
|
||||
#elif !defined(USE_ESP8266) && __has_include(<mbedtls/gcm.h>)
|
||||
#if __has_include(<mbedtls/esp_config.h>)
|
||||
#include <mbedtls/esp_config.h>
|
||||
#endif
|
||||
@@ -33,7 +38,7 @@ namespace esphome::dsmr {
|
||||
|
||||
#if __has_include(<psa/crypto.h>)
|
||||
using Aes128GcmDecryptorImpl = dsmr_parser::Aes128GcmTfPsa;
|
||||
#elif __has_include(<mbedtls/gcm.h>)
|
||||
#elif !defined(USE_ESP8266) && __has_include(<mbedtls/gcm.h>)
|
||||
using Aes128GcmDecryptorImpl = dsmr_parser::Aes128GcmMbedTls;
|
||||
#else
|
||||
using Aes128GcmDecryptorImpl = dsmr_parser::Aes128GcmBearSsl;
|
||||
@@ -69,7 +74,8 @@ class Dsmr : public Component, public uart::UARTDevice {
|
||||
receive_timeout_(receive_timeout),
|
||||
request_pin_(request_pin),
|
||||
buffer_(max_telegram_length),
|
||||
packet_accumulator_(buffer_, crc_check) {
|
||||
packet_accumulator_(buffer_, crc_check),
|
||||
dlms_decryptor_(gcm_decryptor_, crc_check) {
|
||||
this->set_decryption_key_(decryption_key);
|
||||
}
|
||||
|
||||
@@ -92,7 +98,11 @@ class Dsmr : public Component, public uart::UARTDevice {
|
||||
|
||||
// Remove before 2026.8.0
|
||||
ESPDEPRECATED("Use 'decryption_key' configuration parameter. This method will be removed in 2026.8.0", "2026.2.0")
|
||||
void set_decryption_key(const std::string &decryption_key) { this->set_decryption_key_(decryption_key.c_str()); }
|
||||
void set_decryption_key(const std::string &decryption_key) {
|
||||
// Some YAML configs pass a string longer than 32 symbols. We only need the first 32 symbols,
|
||||
// otherwise `Aes128GcmDecryptionKey::from_hex` will fail.
|
||||
this->set_decryption_key_(std::string(decryption_key, 0, 32).c_str());
|
||||
}
|
||||
|
||||
// Sensor setters
|
||||
#define DSMR_SET_SENSOR(s) \
|
||||
@@ -138,7 +148,7 @@ class Dsmr : public Component, public uart::UARTDevice {
|
||||
std::vector<uint8_t> buffer_;
|
||||
dsmr_parser::PacketAccumulator packet_accumulator_;
|
||||
Aes128GcmDecryptorImpl gcm_decryptor_;
|
||||
dsmr_parser::DlmsPacketDecryptor dlms_decryptor_{gcm_decryptor_};
|
||||
dsmr_parser::DlmsPacketDecryptor dlms_decryptor_;
|
||||
std::array<uint8_t, 256> uart_chunk_reading_buf_;
|
||||
};
|
||||
} // namespace esphome::dsmr
|
||||
|
||||
@@ -248,10 +248,6 @@ CONFIG_SCHEMA = cv.Schema(
|
||||
device_class=DEVICE_CLASS_POWER,
|
||||
state_class=STATE_CLASS_MEASUREMENT,
|
||||
),
|
||||
cv.Optional("electricity_switch_position"): sensor.sensor_schema(
|
||||
accuracy_decimals=3,
|
||||
state_class=STATE_CLASS_MEASUREMENT,
|
||||
),
|
||||
cv.Optional("electricity_failures"): sensor.sensor_schema(
|
||||
accuracy_decimals=0,
|
||||
state_class=STATE_CLASS_MEASUREMENT,
|
||||
@@ -808,6 +804,10 @@ CONFIG_SCHEMA = cv.Schema(
|
||||
device_class=DEVICE_CLASS_DURATION,
|
||||
state_class=STATE_CLASS_MEASUREMENT,
|
||||
),
|
||||
cv.Optional("electricity_switch_position"): cv.invalid(
|
||||
"'electricity_switch_position' has moved to the 'text_sensor' platform."
|
||||
"Move it under 'text_sensor' to fix."
|
||||
),
|
||||
}
|
||||
).extend(cv.COMPONENT_SCHEMA)
|
||||
|
||||
|
||||
@@ -14,6 +14,7 @@ CONFIG_SCHEMA = cv.Schema(
|
||||
cv.Optional("p1_version"): text_sensor.text_sensor_schema(),
|
||||
cv.Optional("p1_version_be"): text_sensor.text_sensor_schema(),
|
||||
cv.Optional("timestamp"): text_sensor.text_sensor_schema(),
|
||||
cv.Optional("electricity_switch_position"): text_sensor.text_sensor_schema(),
|
||||
cv.Optional("electricity_tariff"): text_sensor.text_sensor_schema(),
|
||||
cv.Optional("electricity_tariff_il"): text_sensor.text_sensor_schema(),
|
||||
cv.Optional("electricity_failure_log"): text_sensor.text_sensor_schema(),
|
||||
|
||||
@@ -52,6 +52,8 @@ class E131Component : public esphome::Component {
|
||||
if (!this->udp_.parsePacket())
|
||||
return -1;
|
||||
return this->udp_.read(buf, len);
|
||||
#else
|
||||
return -1;
|
||||
#endif
|
||||
}
|
||||
bool packet_(const uint8_t *data, size_t len, int &universe, E131Packet &packet);
|
||||
|
||||
@@ -46,7 +46,7 @@ from esphome.const import (
|
||||
Toolchain,
|
||||
__version__,
|
||||
)
|
||||
from esphome.core import CORE, HexInt, Library
|
||||
from esphome.core import CORE, EsphomeError, HexInt, Library
|
||||
from esphome.core.config import BOARD_MAX_LENGTH
|
||||
from esphome.coroutine import CoroPriority, coroutine_with_priority
|
||||
from esphome.espidf.component import generate_idf_component
|
||||
@@ -56,7 +56,7 @@ from esphome.types import ConfigType
|
||||
from esphome.writer import clean_build, clean_cmake_cache
|
||||
|
||||
from .boards import BOARDS, STANDARD_BOARDS
|
||||
from .const import ( # noqa
|
||||
from .const import (
|
||||
KEY_ARDUINO_LIBRARIES,
|
||||
KEY_BOARD,
|
||||
KEY_COMPONENTS,
|
||||
@@ -78,15 +78,18 @@ from .const import ( # noqa
|
||||
VARIANT_ESP32C6,
|
||||
VARIANT_ESP32C61,
|
||||
VARIANT_ESP32H2,
|
||||
VARIANT_ESP32H4,
|
||||
VARIANT_ESP32H21,
|
||||
VARIANT_ESP32P4,
|
||||
VARIANT_ESP32S2,
|
||||
VARIANT_ESP32S3,
|
||||
VARIANT_ESP32S31,
|
||||
VARIANT_FRIENDLY,
|
||||
VARIANTS,
|
||||
)
|
||||
|
||||
# force import gpio to register pin schema
|
||||
from .gpio import esp32_pin_to_code # noqa
|
||||
from .gpio import esp32_pin_to_code # noqa: F401
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
AUTO_LOAD = ["preferences"]
|
||||
@@ -403,9 +406,12 @@ CPU_FREQUENCIES = {
|
||||
VARIANT_ESP32C6: get_cpu_frequencies(80, 120, 160),
|
||||
VARIANT_ESP32C61: get_cpu_frequencies(80, 120, 160),
|
||||
VARIANT_ESP32H2: get_cpu_frequencies(16, 32, 48, 64, 96),
|
||||
VARIANT_ESP32H4: get_cpu_frequencies(48, 64, 96),
|
||||
VARIANT_ESP32H21: get_cpu_frequencies(48, 64, 96),
|
||||
VARIANT_ESP32P4: get_cpu_frequencies(40, 360, 400),
|
||||
VARIANT_ESP32S2: get_cpu_frequencies(80, 160, 240),
|
||||
VARIANT_ESP32S3: get_cpu_frequencies(80, 160, 240),
|
||||
VARIANT_ESP32S31: get_cpu_frequencies(240, 320),
|
||||
}
|
||||
|
||||
# Make sure not missed here if a new variant added.
|
||||
@@ -464,21 +470,20 @@ def set_core_data(config):
|
||||
framework_ver = cv.Version.parse(config[CONF_FRAMEWORK][CONF_VERSION])
|
||||
CORE.data[KEY_CORE][KEY_FRAMEWORK_VERSION] = framework_ver
|
||||
|
||||
# Store the underlying IDF version for framework-agnostic checks
|
||||
# Store the underlying IDF version for framework-agnostic checks.
|
||||
if conf[CONF_TYPE] == FRAMEWORK_ESP_IDF:
|
||||
CORE.data[KEY_ESP32][KEY_IDF_VERSION] = framework_ver
|
||||
elif (idf_ver := ARDUINO_IDF_VERSION_LOOKUP.get(framework_ver)) is not None:
|
||||
if CORE.using_toolchain_esp_idf:
|
||||
# Official ESP-IDF frameworks don't use extra
|
||||
idf_ver = cv.Version(idf_ver.major, idf_ver.minor, idf_ver.patch)
|
||||
CORE.data[KEY_ESP32][KEY_IDF_VERSION] = idf_ver
|
||||
else:
|
||||
idf_ver = framework_ver
|
||||
elif (idf_ver := ARDUINO_IDF_VERSION_LOOKUP.get(framework_ver)) is None:
|
||||
raise cv.Invalid(
|
||||
f"Arduino version {framework_ver} has no known ESP-IDF version mapping. "
|
||||
"Please update ARDUINO_IDF_VERSION_LOOKUP.",
|
||||
path=[CONF_FRAMEWORK, CONF_VERSION],
|
||||
)
|
||||
# The esp-idf toolchain doesn't use pioarduino's packaging revision; PIO does.
|
||||
if CORE.using_toolchain_esp_idf:
|
||||
idf_ver = _strip_pioarduino_revision(idf_ver)
|
||||
|
||||
CORE.data[KEY_ESP32][KEY_IDF_VERSION] = idf_ver
|
||||
CORE.data[KEY_ESP32][KEY_BOARD] = config[CONF_BOARD]
|
||||
CORE.data[KEY_ESP32][KEY_FLASH_SIZE] = config[CONF_FLASH_SIZE]
|
||||
CORE.data[KEY_ESP32][KEY_VARIANT] = variant
|
||||
@@ -715,6 +720,9 @@ ARDUINO_FRAMEWORK_VERSION_LOOKUP = {
|
||||
"dev": cv.Version(3, 3, 8),
|
||||
}
|
||||
ARDUINO_PLATFORM_VERSION_LOOKUP = {
|
||||
cv.Version(
|
||||
4, 0, 0, "alpha1"
|
||||
): "https://github.com/pioarduino/platform-espressif32.git#prep_IDF6",
|
||||
cv.Version(3, 3, 8): cv.Version(55, 3, 38, "1"),
|
||||
cv.Version(3, 3, 7): cv.Version(55, 3, 37),
|
||||
cv.Version(3, 3, 6): cv.Version(55, 3, 36),
|
||||
@@ -735,6 +743,7 @@ ARDUINO_PLATFORM_VERSION_LOOKUP = {
|
||||
# These versions correspond to pioarduino/esp-idf releases
|
||||
# See: https://github.com/pioarduino/esp-idf/releases
|
||||
ARDUINO_IDF_VERSION_LOOKUP = {
|
||||
cv.Version(4, 0, 0, "alpha1"): cv.Version(6, 0, 1),
|
||||
cv.Version(3, 3, 8): cv.Version(5, 5, 4),
|
||||
cv.Version(3, 3, 7): cv.Version(5, 5, 3, "1"),
|
||||
cv.Version(3, 3, 6): cv.Version(5, 5, 2),
|
||||
@@ -829,6 +838,16 @@ def _resolve_framework_version(value: ConfigType) -> cv.Version:
|
||||
return version
|
||||
|
||||
|
||||
def _strip_pioarduino_revision(ver: cv.Version) -> cv.Version:
|
||||
"""Drop a numeric 'extra' (pioarduino packaging revision, e.g. "5.5.3-1").
|
||||
|
||||
Alphanumeric prerelease extras (e.g. "6.0.0-rc1") are kept.
|
||||
"""
|
||||
if ver.extra.isdigit():
|
||||
return cv.Version(ver.major, ver.minor, ver.patch)
|
||||
return ver
|
||||
|
||||
|
||||
def _check_pio_versions(config: ConfigType) -> ConfigType:
|
||||
config = config.copy()
|
||||
value = config[CONF_FRAMEWORK]
|
||||
@@ -897,8 +916,10 @@ def _check_esp_idf_versions(config: ConfigType) -> ConfigType:
|
||||
"If there are connectivity or build issues please remove the manual source."
|
||||
)
|
||||
|
||||
# Official ESP-IDF frameworks don't use the 'extra' semver component.
|
||||
value[CONF_VERSION] = str(cv.Version(version.major, version.minor, version.patch))
|
||||
# esp-idf framework only: drop pioarduino's packaging revision (config + download).
|
||||
# Arduino keeps its extra (it's the arduino-esp32 release tag / lookup key).
|
||||
if value[CONF_TYPE] == FRAMEWORK_ESP_IDF:
|
||||
value[CONF_VERSION] = str(_strip_pioarduino_revision(version))
|
||||
|
||||
return config
|
||||
|
||||
@@ -907,11 +928,16 @@ def _validate_toolchain(value) -> Toolchain:
|
||||
return Toolchain(cv.one_of(*(t.value for t in Toolchain), lower=True)(value))
|
||||
|
||||
|
||||
def _check_versions(config):
|
||||
def _resolve_toolchain(value: ConfigType) -> ConfigType:
|
||||
# Resolve toolchain: CLI (already on CORE.toolchain) > YAML > default.
|
||||
# Runs before _detect_variant so downstream validators can rely on
|
||||
# CORE.toolchain instead of re-resolving it from the config dict.
|
||||
if CORE.toolchain is None:
|
||||
CORE.toolchain = config.get(CONF_TOOLCHAIN, Toolchain.PLATFORMIO)
|
||||
CORE.toolchain = value.get(CONF_TOOLCHAIN, Toolchain.PLATFORMIO)
|
||||
return value
|
||||
|
||||
|
||||
def _check_versions(config: ConfigType) -> ConfigType:
|
||||
if CORE.using_toolchain_esp_idf:
|
||||
return _check_esp_idf_versions(config)
|
||||
return _check_pio_versions(config)
|
||||
@@ -933,7 +959,21 @@ def _detect_variant(value):
|
||||
variant = value.get(CONF_VARIANT)
|
||||
if variant and board is None:
|
||||
# If variant is set, we can derive the board from it
|
||||
# variant has already been validated against the known set
|
||||
# variant has already been validated against the known set.
|
||||
# PlatformIO needs a real board name to find its board file; the
|
||||
# ESP-IDF toolchain only uses CONF_BOARD as the informational
|
||||
# ESPHOME_BOARD string, so synthesize one from the friendly variant
|
||||
# name rather than carrying a PIO board name through the IDF build.
|
||||
if CORE.using_toolchain_esp_idf:
|
||||
value = value.copy()
|
||||
value[CONF_BOARD] = VARIANT_FRIENDLY[variant].lower()
|
||||
return value
|
||||
if variant not in STANDARD_BOARDS:
|
||||
raise cv.Invalid(
|
||||
f"No default board is known for {variant}. "
|
||||
f"Please specify the `board:` option explicitly.",
|
||||
path=[CONF_VARIANT],
|
||||
)
|
||||
value = value.copy()
|
||||
value[CONF_BOARD] = STANDARD_BOARDS[variant]
|
||||
if variant == VARIANT_ESP32P4:
|
||||
@@ -1220,6 +1260,7 @@ 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"
|
||||
KEY_LIBC_PICOLIBC_NEWLIB_COMPAT_REQUIRED = "libc_picolibc_newlib_compat_required"
|
||||
|
||||
|
||||
def require_vfs_select() -> None:
|
||||
@@ -1328,6 +1369,15 @@ def require_adc_oneshot_iram() -> None:
|
||||
CORE.data[KEY_ESP32][KEY_ADC_ONESHOT_IRAM_REQUIRED] = True
|
||||
|
||||
|
||||
def require_libc_picolibc_newlib_compat() -> None:
|
||||
"""Keep CONFIG_LIBC_PICOLIBC_NEWLIB_COMPATIBILITY enabled on IDF 6.0+.
|
||||
|
||||
Call this from components that link against precompiled Newlib binaries
|
||||
referencing types/symbols the shim provides (e.g. esp32-camera).
|
||||
"""
|
||||
CORE.data[KEY_ESP32][KEY_LIBC_PICOLIBC_NEWLIB_COMPAT_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 *)
|
||||
@@ -1606,6 +1656,7 @@ CONFIG_SCHEMA = cv.All(
|
||||
),
|
||||
}
|
||||
),
|
||||
_resolve_toolchain,
|
||||
_detect_variant,
|
||||
_set_default_framework,
|
||||
_check_versions,
|
||||
@@ -1732,6 +1783,26 @@ async def _write_arduino_libraries_sdkconfig() -> None:
|
||||
add_idf_sdkconfig_option(f"CONFIG_ARDUINO_SELECTIVE_{lib}", lib in enabled_libs)
|
||||
|
||||
|
||||
@coroutine_with_priority(CoroPriority.FINAL)
|
||||
async def _set_libc_picolibc_newlib_compat() -> None:
|
||||
"""Apply the PicolibC Newlib compatibility shim option on IDF 6.0+.
|
||||
|
||||
IDF 6.0 switched from Newlib to PicolibC; the shim is disabled by default.
|
||||
Runs at FINAL priority so every require_libc_picolibc_newlib_compat() call
|
||||
(default priority) is seen before the option is written. A user-supplied
|
||||
sdkconfig_options value takes precedence.
|
||||
"""
|
||||
if idf_version() < cv.Version(6, 0, 0):
|
||||
return
|
||||
option = "CONFIG_LIBC_PICOLIBC_NEWLIB_COMPATIBILITY"
|
||||
if option in CORE.data[KEY_ESP32][KEY_SDKCONFIG_OPTIONS]:
|
||||
return
|
||||
add_idf_sdkconfig_option(
|
||||
option,
|
||||
CORE.data[KEY_ESP32].get(KEY_LIBC_PICOLIBC_NEWLIB_COMPAT_REQUIRED, False),
|
||||
)
|
||||
|
||||
|
||||
@coroutine_with_priority(CoroPriority.FINAL)
|
||||
async def _add_yaml_idf_components(components: list[ConfigType]):
|
||||
"""Add IDF components from YAML config with final priority to override code-added components."""
|
||||
@@ -1816,12 +1887,12 @@ async def to_code(config):
|
||||
Path(__file__).parent / "iram_fix.py.script",
|
||||
)
|
||||
else:
|
||||
cg.add_build_flag("-Wno-error=format")
|
||||
cg.add_build_flag("-Wno-error=maybe-uninitialized")
|
||||
cg.add_build_flag("-Wno-error=overloaded-virtual")
|
||||
cg.add_build_flag("-Wno-error=reorder")
|
||||
cg.add_build_flag("-Wno-error=volatile")
|
||||
cg.add_build_flag("-Wno-error=cpp")
|
||||
# Demote IDF's blanket -Werror to warnings so third-party libs
|
||||
# and user lambdas don't need a -Wno-error=<class> per warning.
|
||||
# The sdkconfig knob disables IDF's rewrite to -Werror=all (which
|
||||
# can't be globally undone); -Wno-error then handles the demotion.
|
||||
add_idf_sdkconfig_option("CONFIG_COMPILER_DISABLE_DEFAULT_ERRORS", False)
|
||||
cg.add_build_flag("-Wno-error")
|
||||
# -Wno- (not -Wno-error=): suppress entirely, too noisy on C++ aggregates
|
||||
cg.add_build_flag("-Wno-missing-field-initializers")
|
||||
|
||||
@@ -2265,17 +2336,8 @@ async def to_code(config):
|
||||
add_idf_sdkconfig_option("CONFIG_MBEDTLS_SHA384_C", False)
|
||||
add_idf_sdkconfig_option("CONFIG_MBEDTLS_SHA512_C", False)
|
||||
|
||||
# Disable PicolibC Newlib compatibility shim on IDF 6.0+
|
||||
# IDF 6.0 switched from Newlib to PicolibC. The shim provides thread-local
|
||||
# stdin/stdout/stderr and getreent() for code compiled against Newlib.
|
||||
# ESPHome doesn't link against Newlib-built libraries that use stdio.
|
||||
# If a component needs it (e.g. precompiled Newlib binaries), re-enable via:
|
||||
# esp32:
|
||||
# framework:
|
||||
# sdkconfig_options:
|
||||
# CONFIG_LIBC_PICOLIBC_NEWLIB_COMPATIBILITY: "y"
|
||||
if idf_version() >= cv.Version(6, 0, 0):
|
||||
add_idf_sdkconfig_option("CONFIG_LIBC_PICOLIBC_NEWLIB_COMPATIBILITY", False)
|
||||
# FINAL priority: runs after every require_libc_picolibc_newlib_compat() call
|
||||
CORE.add_job(_set_libc_picolibc_newlib_compat)
|
||||
|
||||
# Disable regi2c control functions in IRAM
|
||||
# Only needed if using analog peripherals (ADC, DAC, etc.) from ISRs while cache is disabled
|
||||
@@ -2583,6 +2645,26 @@ def _write_idf_component_yml():
|
||||
"override_path": str(stub_path),
|
||||
}
|
||||
|
||||
# On the PlatformIO toolchain, framework-arduinoespressif32 already
|
||||
# ships arduino-esp32. Stub the managed component so anything that
|
||||
# `REQUIRES arduino-esp32` (e.g. third-party FastLED) resolves to a
|
||||
# CMake target that re-exports the framework's INTERFACE properties
|
||||
# (INCLUDE_DIRS, public compile options like -DESP32, transitive
|
||||
# REQUIRES) instead of triggering a duplicate download/rebuild.
|
||||
if CORE.using_toolchain_platformio:
|
||||
arduino_stub = stubs_dir / "arduino-esp32"
|
||||
arduino_stub.mkdir(exist_ok=True)
|
||||
write_file_if_changed(
|
||||
arduino_stub / "CMakeLists.txt",
|
||||
"idf_component_register()\n"
|
||||
"target_link_libraries(${COMPONENT_LIB} "
|
||||
f"INTERFACE idf::{ARDUINO_FRAMEWORK_NAME})\n",
|
||||
)
|
||||
dependencies[ARDUINO_ESP32_COMPONENT_NAME] = {
|
||||
"version": "*",
|
||||
"override_path": str(arduino_stub),
|
||||
}
|
||||
|
||||
# Remove stubs for components that are now required by enabled libraries
|
||||
for component_name in required_idf_components:
|
||||
stub_path = stubs_dir / _idf_component_stub_name(component_name)
|
||||
@@ -2658,16 +2740,32 @@ def copy_files():
|
||||
|
||||
|
||||
def _decode_pc(config, addr):
|
||||
from esphome.platformio import toolchain
|
||||
# _decode_pc runs from the api log processor's asyncio callback, which
|
||||
# only catches EsphomeError. Any other exception escaping here tears down
|
||||
# the protocol and triggers an infinite reconnect/replay loop. Convert
|
||||
# toolchain-resolution errors (e.g. missing build dir / cmake cache) into
|
||||
# EsphomeError so the caller can disable decoding cleanly.
|
||||
if CORE.using_toolchain_esp_idf:
|
||||
from esphome.espidf import toolchain as idf_toolchain
|
||||
|
||||
idedata = toolchain.get_idedata(config)
|
||||
if not idedata.addr2line_path or not idedata.firmware_elf_path:
|
||||
try:
|
||||
addr2line_path = idf_toolchain.get_addr2line_path()
|
||||
firmware_elf_path = idf_toolchain.get_elf_path()
|
||||
except RuntimeError as err:
|
||||
raise EsphomeError(f"ESP-IDF toolchain not available: {err}") from err
|
||||
else:
|
||||
from esphome.platformio import toolchain
|
||||
|
||||
idedata = toolchain.get_idedata(config)
|
||||
addr2line_path = idedata.addr2line_path
|
||||
firmware_elf_path = idedata.firmware_elf_path
|
||||
if not addr2line_path or not firmware_elf_path:
|
||||
_LOGGER.debug("decode_pc no addr2line")
|
||||
return
|
||||
command = [idedata.addr2line_path, "-pfiaC", "-e", idedata.firmware_elf_path, addr]
|
||||
command = [str(addr2line_path), "-pfiaC", "-e", str(firmware_elf_path), addr]
|
||||
try:
|
||||
translation = subprocess.check_output(command, close_fds=False).decode().strip()
|
||||
except Exception: # pylint: disable=broad-except
|
||||
except Exception: # noqa: BLE001 # pylint: disable=broad-except
|
||||
_LOGGER.debug("Caught exception for command %s", command, exc_info=1)
|
||||
return
|
||||
|
||||
|
||||
@@ -9,7 +9,6 @@ from .const import (
|
||||
VARIANT_ESP32P4,
|
||||
VARIANT_ESP32S2,
|
||||
VARIANT_ESP32S3,
|
||||
VARIANTS,
|
||||
)
|
||||
|
||||
STANDARD_BOARDS = {
|
||||
@@ -25,9 +24,6 @@ STANDARD_BOARDS = {
|
||||
VARIANT_ESP32S3: "esp32-s3-devkitc-1",
|
||||
}
|
||||
|
||||
# Make sure not missed here if a new variant added.
|
||||
assert all(v in STANDARD_BOARDS for v in VARIANTS)
|
||||
|
||||
ESP32_BASE_PINS = {
|
||||
"TX": 1,
|
||||
"RX": 3,
|
||||
|
||||
@@ -24,9 +24,12 @@ VARIANT_ESP32C5 = "ESP32C5"
|
||||
VARIANT_ESP32C6 = "ESP32C6"
|
||||
VARIANT_ESP32C61 = "ESP32C61"
|
||||
VARIANT_ESP32H2 = "ESP32H2"
|
||||
VARIANT_ESP32H4 = "ESP32H4"
|
||||
VARIANT_ESP32H21 = "ESP32H21"
|
||||
VARIANT_ESP32P4 = "ESP32P4"
|
||||
VARIANT_ESP32S2 = "ESP32S2"
|
||||
VARIANT_ESP32S3 = "ESP32S3"
|
||||
VARIANT_ESP32S31 = "ESP32S31"
|
||||
VARIANTS = [
|
||||
VARIANT_ESP32,
|
||||
VARIANT_ESP32C2,
|
||||
@@ -35,9 +38,12 @@ VARIANTS = [
|
||||
VARIANT_ESP32C6,
|
||||
VARIANT_ESP32C61,
|
||||
VARIANT_ESP32H2,
|
||||
VARIANT_ESP32H4,
|
||||
VARIANT_ESP32H21,
|
||||
VARIANT_ESP32P4,
|
||||
VARIANT_ESP32S2,
|
||||
VARIANT_ESP32S3,
|
||||
VARIANT_ESP32S31,
|
||||
]
|
||||
|
||||
VARIANT_FRIENDLY = {
|
||||
@@ -48,9 +54,12 @@ VARIANT_FRIENDLY = {
|
||||
VARIANT_ESP32C6: "ESP32-C6",
|
||||
VARIANT_ESP32C61: "ESP32-C61",
|
||||
VARIANT_ESP32H2: "ESP32-H2",
|
||||
VARIANT_ESP32H4: "ESP32-H4",
|
||||
VARIANT_ESP32H21: "ESP32-H21",
|
||||
VARIANT_ESP32P4: "ESP32-P4",
|
||||
VARIANT_ESP32S2: "ESP32-S2",
|
||||
VARIANT_ESP32S3: "ESP32-S3",
|
||||
VARIANT_ESP32S31: "ESP32-S31",
|
||||
}
|
||||
|
||||
esp32_ns = cg.esphome_ns.namespace("esp32")
|
||||
|
||||
@@ -31,9 +31,12 @@ from .const import (
|
||||
VARIANT_ESP32C6,
|
||||
VARIANT_ESP32C61,
|
||||
VARIANT_ESP32H2,
|
||||
VARIANT_ESP32H4,
|
||||
VARIANT_ESP32H21,
|
||||
VARIANT_ESP32P4,
|
||||
VARIANT_ESP32S2,
|
||||
VARIANT_ESP32S3,
|
||||
VARIANT_ESP32S31,
|
||||
esp32_ns,
|
||||
)
|
||||
from .gpio_esp32 import esp32_validate_gpio_pin, esp32_validate_supports
|
||||
@@ -43,9 +46,12 @@ from .gpio_esp32_c5 import esp32_c5_validate_gpio_pin, esp32_c5_validate_support
|
||||
from .gpio_esp32_c6 import esp32_c6_validate_gpio_pin, esp32_c6_validate_supports
|
||||
from .gpio_esp32_c61 import esp32_c61_validate_gpio_pin, esp32_c61_validate_supports
|
||||
from .gpio_esp32_h2 import esp32_h2_validate_gpio_pin, esp32_h2_validate_supports
|
||||
from .gpio_esp32_h4 import esp32_h4_validate_gpio_pin, esp32_h4_validate_supports
|
||||
from .gpio_esp32_h21 import esp32_h21_validate_gpio_pin, esp32_h21_validate_supports
|
||||
from .gpio_esp32_p4 import esp32_p4_validate_gpio_pin, esp32_p4_validate_supports
|
||||
from .gpio_esp32_s2 import esp32_s2_validate_gpio_pin, esp32_s2_validate_supports
|
||||
from .gpio_esp32_s3 import esp32_s3_validate_gpio_pin, esp32_s3_validate_supports
|
||||
from .gpio_esp32_s31 import esp32_s31_validate_gpio_pin, esp32_s31_validate_supports
|
||||
|
||||
ESP32InternalGPIOPin = esp32_ns.class_("ESP32InternalGPIOPin", cg.InternalGPIOPin)
|
||||
|
||||
@@ -120,6 +126,14 @@ _esp32_validations = {
|
||||
pin_validation=esp32_h2_validate_gpio_pin,
|
||||
usage_validation=esp32_h2_validate_supports,
|
||||
),
|
||||
VARIANT_ESP32H4: ESP32ValidationFunctions(
|
||||
pin_validation=esp32_h4_validate_gpio_pin,
|
||||
usage_validation=esp32_h4_validate_supports,
|
||||
),
|
||||
VARIANT_ESP32H21: ESP32ValidationFunctions(
|
||||
pin_validation=esp32_h21_validate_gpio_pin,
|
||||
usage_validation=esp32_h21_validate_supports,
|
||||
),
|
||||
VARIANT_ESP32P4: ESP32ValidationFunctions(
|
||||
pin_validation=esp32_p4_validate_gpio_pin,
|
||||
usage_validation=esp32_p4_validate_supports,
|
||||
@@ -132,6 +146,10 @@ _esp32_validations = {
|
||||
pin_validation=esp32_s3_validate_gpio_pin,
|
||||
usage_validation=esp32_s3_validate_supports,
|
||||
),
|
||||
VARIANT_ESP32S31: ESP32ValidationFunctions(
|
||||
pin_validation=esp32_s31_validate_gpio_pin,
|
||||
usage_validation=esp32_s31_validate_supports,
|
||||
),
|
||||
}
|
||||
|
||||
|
||||
|
||||
34
esphome/components/esp32/gpio_esp32_h21.py
Normal file
34
esphome/components/esp32/gpio_esp32_h21.py
Normal file
@@ -0,0 +1,34 @@
|
||||
import logging
|
||||
from typing import Any
|
||||
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import CONF_INPUT, CONF_MODE, CONF_NUMBER
|
||||
from esphome.pins import check_strapping_pin
|
||||
|
||||
# Partial set from the ESP-IDF / esptool boot-mode docs:
|
||||
# https://docs.espressif.com/projects/esptool/en/latest/esp32h21/advanced-topics/boot-mode-selection.html
|
||||
# The full list awaits the ESP32-H21 datasheet's "Strapping Pins" section.
|
||||
_ESP32H21_STRAPPING_PINS: set[int] = {13, 14}
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def esp32_h21_validate_gpio_pin(value: int) -> int:
|
||||
if value < 0 or value > 25:
|
||||
raise cv.Invalid(f"Invalid pin number: {value} (must be 0-25)")
|
||||
return value
|
||||
|
||||
|
||||
def esp32_h21_validate_supports(value: dict[str, Any]) -> dict[str, Any]:
|
||||
num = value[CONF_NUMBER]
|
||||
mode = value[CONF_MODE]
|
||||
is_input = mode[CONF_INPUT]
|
||||
|
||||
if num < 0 or num > 25:
|
||||
raise cv.Invalid(f"Invalid pin number: {num} (must be 0-25)")
|
||||
if is_input:
|
||||
# All ESP32 pins support input mode
|
||||
pass
|
||||
|
||||
check_strapping_pin(value, _ESP32H21_STRAPPING_PINS, _LOGGER)
|
||||
return value
|
||||
34
esphome/components/esp32/gpio_esp32_h4.py
Normal file
34
esphome/components/esp32/gpio_esp32_h4.py
Normal file
@@ -0,0 +1,34 @@
|
||||
import logging
|
||||
from typing import Any
|
||||
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import CONF_INPUT, CONF_MODE, CONF_NUMBER
|
||||
from esphome.pins import check_strapping_pin
|
||||
|
||||
# Partial set from the ESP-IDF / esptool boot-mode docs:
|
||||
# https://docs.espressif.com/projects/esptool/en/latest/esp32h4/advanced-topics/boot-mode-selection.html
|
||||
# The full list awaits the ESP32-H4 datasheet's "Strapping Pins" section.
|
||||
_ESP32H4_STRAPPING_PINS: set[int] = {13, 14}
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def esp32_h4_validate_gpio_pin(value: int) -> int:
|
||||
if value < 0 or value > 39:
|
||||
raise cv.Invalid(f"Invalid pin number: {value} (must be 0-39)")
|
||||
return value
|
||||
|
||||
|
||||
def esp32_h4_validate_supports(value: dict[str, Any]) -> dict[str, Any]:
|
||||
num = value[CONF_NUMBER]
|
||||
mode = value[CONF_MODE]
|
||||
is_input = mode[CONF_INPUT]
|
||||
|
||||
if num < 0 or num > 39:
|
||||
raise cv.Invalid(f"Invalid pin number: {num} (must be 0-39)")
|
||||
if is_input:
|
||||
# All ESP32 pins support input mode
|
||||
pass
|
||||
|
||||
check_strapping_pin(value, _ESP32H4_STRAPPING_PINS, _LOGGER)
|
||||
return value
|
||||
38
esphome/components/esp32/gpio_esp32_s31.py
Normal file
38
esphome/components/esp32/gpio_esp32_s31.py
Normal file
@@ -0,0 +1,38 @@
|
||||
import logging
|
||||
from typing import Any
|
||||
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import CONF_INPUT, CONF_MODE, CONF_NUMBER
|
||||
from esphome.pins import check_strapping_pin
|
||||
|
||||
# Per the ESP32-S31 datasheet (page 96):
|
||||
# https://documentation.espressif.com/esp32-s31_datasheet_en.pdf
|
||||
_ESP32S31_SPI_FLASH_PINS: set[int] = {27, 28, 29, 31, 32, 33}
|
||||
_ESP32S31_STRAPPING_PINS: set[int] = {60, 61}
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def esp32_s31_validate_gpio_pin(value: int) -> int:
|
||||
if value < 0 or value > 61:
|
||||
raise cv.Invalid(f"Invalid pin number: {value} (must be 0-61)")
|
||||
if value in _ESP32S31_SPI_FLASH_PINS:
|
||||
raise cv.Invalid(
|
||||
f"GPIO{value} is reserved for the SPI flash interface on ESP32-S31 and cannot be used."
|
||||
)
|
||||
return value
|
||||
|
||||
|
||||
def esp32_s31_validate_supports(value: dict[str, Any]) -> dict[str, Any]:
|
||||
num = value[CONF_NUMBER]
|
||||
mode = value[CONF_MODE]
|
||||
is_input = mode[CONF_INPUT]
|
||||
|
||||
if num < 0 or num > 61:
|
||||
raise cv.Invalid(f"Invalid pin number: {num} (must be 0-61)")
|
||||
if is_input:
|
||||
# All ESP32 pins support input mode
|
||||
pass
|
||||
|
||||
check_strapping_pin(value, _ESP32S31_STRAPPING_PINS, _LOGGER)
|
||||
return value
|
||||
@@ -72,6 +72,7 @@ void BLEClientBase::loop() {
|
||||
// never delivered CLOSE_EVT/DISCONNECT_EVT, services would leak without this call.
|
||||
this->release_services();
|
||||
this->set_idle_();
|
||||
this->on_disconnect_complete(ESP_GATT_CONN_TIMEOUT);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -418,6 +419,7 @@ bool BLEClientBase::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_
|
||||
this->log_gattc_lifecycle_event_("CLOSE");
|
||||
this->release_services();
|
||||
this->set_idle_();
|
||||
this->on_disconnect_complete(param->close.reason);
|
||||
break;
|
||||
}
|
||||
case ESP_GATTC_SEARCH_RES_EVT: {
|
||||
|
||||
@@ -140,6 +140,12 @@ class BLEClientBase : public espbt::ESPBTClient, public Component {
|
||||
void log_gattc_warning_(const char *operation, esp_err_t err);
|
||||
void log_connection_params_(const char *param_type);
|
||||
void handle_connection_result_(esp_err_t ret);
|
||||
/// Hook called once a connection has been fully torn down (after release_services() and
|
||||
/// set_idle_()), from both the CLOSE_EVT handler and the DISCONNECTING safety timeout.
|
||||
/// Subclasses with extra per-connection accounting (e.g. bluetooth_proxy slot state)
|
||||
/// override this to release that state. `reason` is the controller reason code, or
|
||||
/// ESP_GATT_CONN_TIMEOUT for the safety-timeout path.
|
||||
virtual void on_disconnect_complete(esp_err_t reason) {}
|
||||
/// Transition to IDLE and reset conn_id — call when the connection is fully dead.
|
||||
void set_idle_() {
|
||||
this->set_state(espbt::ClientState::IDLE);
|
||||
@@ -149,6 +155,10 @@ class BLEClientBase : public espbt::ESPBTClient, public Component {
|
||||
void set_disconnecting_() {
|
||||
this->disconnecting_started_ = millis();
|
||||
this->set_state(espbt::ClientState::DISCONNECTING);
|
||||
// BluetoothConnection::loop() disables the component loop after service discovery
|
||||
// completes, so the DISCONNECTING timeout check in loop() would never run if CLOSE_EVT
|
||||
// gets lost. Re-enable the loop so the 10s safety timeout can force IDLE.
|
||||
this->enable_loop();
|
||||
}
|
||||
// Compact error logging helpers to reduce flash usage
|
||||
void log_error_(const char *message);
|
||||
|
||||
@@ -62,6 +62,26 @@ MANUFACTURER_NAME_CHARACTERISTIC_UUID = 0x2A29
|
||||
MODEL_CHARACTERISTIC_UUID = 0x2A24
|
||||
FIRMWARE_VERSION_CHARACTERISTIC_UUID = 0x2A26
|
||||
|
||||
# Suffix of the Bluetooth Base UUID used to expand 16/32 bit UUIDs to 128 bit.
|
||||
_BASE_UUID_SUFFIX = "-0000-1000-8000-00805F9B34FB"
|
||||
|
||||
|
||||
def uuid_is(uuid: int | str, uuid16: int) -> bool:
|
||||
"""Return True if a validated UUID refers to the given 16-bit short UUID.
|
||||
|
||||
A service/characteristic UUID may be an ``int`` (from ``cv.hex_uint32_t``) or an
|
||||
uppercase string in 16, 32 or 128 bit form (from ``bt_uuid``), so every
|
||||
representation of the same UUID must be considered equivalent.
|
||||
"""
|
||||
if isinstance(uuid, int):
|
||||
return uuid == uuid16
|
||||
return uuid.upper() in (
|
||||
f"{uuid16:04X}",
|
||||
f"{uuid16:08X}",
|
||||
f"{uuid16:08X}{_BASE_UUID_SUFFIX}",
|
||||
)
|
||||
|
||||
|
||||
# Core key to store the global configuration
|
||||
KEY_NOTIFY_REQUIRED = "notify_required"
|
||||
KEY_SET_VALUE = "set_value"
|
||||
@@ -195,7 +215,7 @@ def create_description_cud(char_config):
|
||||
return char_config
|
||||
# If the config displays a description, there cannot be a descriptor with the CUD UUID
|
||||
for desc in char_config[CONF_DESCRIPTORS]:
|
||||
if desc[CONF_UUID] == CUD_DESCRIPTOR_UUID:
|
||||
if uuid_is(desc[CONF_UUID], CUD_DESCRIPTOR_UUID):
|
||||
raise cv.Invalid(
|
||||
f"Characteristic {char_config[CONF_UUID]} has a description, but a CUD descriptor is already present"
|
||||
)
|
||||
@@ -218,7 +238,7 @@ def create_notify_cccd(char_config):
|
||||
return char_config
|
||||
# If the CCCD descriptor is already present, return the config
|
||||
for desc in char_config[CONF_DESCRIPTORS]:
|
||||
if desc[CONF_UUID] == CCCD_DESCRIPTOR_UUID:
|
||||
if uuid_is(desc[CONF_UUID], CCCD_DESCRIPTOR_UUID):
|
||||
# Check if the WRITE property is set
|
||||
if not desc[CONF_WRITE]:
|
||||
raise cv.Invalid(
|
||||
@@ -244,7 +264,7 @@ def create_device_information_service(config):
|
||||
# If there is already a device information service,
|
||||
# there cannot be CONF_MODEL, CONF_MANUFACTURER or CONF_FIRMWARE_VERSION properties
|
||||
for service in config[CONF_SERVICES]:
|
||||
if service[CONF_UUID] == DEVICE_INFORMATION_SERVICE_UUID:
|
||||
if uuid_is(service[CONF_UUID], DEVICE_INFORMATION_SERVICE_UUID):
|
||||
if (
|
||||
CONF_MODEL in config
|
||||
or CONF_MANUFACTURER in config
|
||||
@@ -592,7 +612,7 @@ async def to_code(config):
|
||||
)
|
||||
for char_conf in service_config[CONF_CHARACTERISTICS]:
|
||||
await to_code_characteristic(service_var, char_conf)
|
||||
if service_config[CONF_UUID] == DEVICE_INFORMATION_SERVICE_UUID:
|
||||
if uuid_is(service_config[CONF_UUID], DEVICE_INFORMATION_SERVICE_UUID):
|
||||
cg.add(var.set_device_information_service(service_var))
|
||||
else:
|
||||
cg.add(var.enqueue_start_service(service_var))
|
||||
|
||||
@@ -3,7 +3,11 @@ import logging
|
||||
from esphome import automation, pins
|
||||
import esphome.codegen as cg
|
||||
from esphome.components import i2c
|
||||
from esphome.components.esp32 import add_idf_component, add_idf_sdkconfig_option
|
||||
from esphome.components.esp32 import (
|
||||
add_idf_component,
|
||||
add_idf_sdkconfig_option,
|
||||
require_libc_picolibc_newlib_compat,
|
||||
)
|
||||
from esphome.components.psram import DOMAIN as psram_domain
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import (
|
||||
@@ -402,6 +406,8 @@ async def to_code(config):
|
||||
add_idf_component(name="espressif/esp32-camera", ref="2.1.5")
|
||||
add_idf_sdkconfig_option("CONFIG_SCCB_HARDWARE_I2C_DRIVER_NEW", True)
|
||||
add_idf_sdkconfig_option("CONFIG_SCCB_HARDWARE_I2C_DRIVER_LEGACY", False)
|
||||
# esp32-camera 2.1.5 needs the Newlib shim on IDF 6.0+; remove when fixed upstream
|
||||
require_libc_picolibc_newlib_compat()
|
||||
|
||||
for conf in config.get(CONF_ON_STREAM_START, []):
|
||||
trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var)
|
||||
|
||||
@@ -3,6 +3,7 @@ from pathlib import Path
|
||||
|
||||
from esphome import pins
|
||||
from esphome.components import esp32
|
||||
from esphome.components.const import CONF_USE_PSRAM
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import (
|
||||
CONF_CLK_PIN,
|
||||
@@ -39,6 +40,7 @@ BASE_SCHEMA = cv.Schema(
|
||||
cv.Required(CONF_VARIANT): cv.one_of(*esp32.VARIANTS, upper=True),
|
||||
cv.Required(CONF_ACTIVE_HIGH): cv.boolean,
|
||||
cv.Required(CONF_RESET_PIN): pins.internal_gpio_output_pin_number,
|
||||
cv.Optional(CONF_USE_PSRAM, default=False): cv.boolean,
|
||||
}
|
||||
)
|
||||
|
||||
@@ -242,6 +244,12 @@ async def to_code(config):
|
||||
else:
|
||||
_configure_spi(config)
|
||||
|
||||
# Place the transport mempool in PSRAM. Required on memory-tight host
|
||||
# configurations (e.g. P4 with a large LVGL UI) where the internal-RAM
|
||||
# mempool allocation fails at boot with `sdio_mempool_create` assert.
|
||||
if config[CONF_USE_PSRAM]:
|
||||
esp32.add_idf_sdkconfig_option("CONFIG_ESP_HOSTED_MEMPOOL_PREFER_SPIRAM", True)
|
||||
|
||||
# Library versions
|
||||
idf_ver = esp32.idf_version()
|
||||
os.environ["ESP_IDF_VERSION"] = f"{idf_ver.major}.{idf_ver.minor}"
|
||||
@@ -249,7 +257,7 @@ async def to_code(config):
|
||||
esp32.add_idf_component(name="espressif/esp_wifi_remote", ref="1.5.1")
|
||||
esp32.add_idf_component(name="espressif/wifi_remote_over_eppp", ref="0.3.2")
|
||||
esp32.add_idf_component(name="espressif/eppp_link", ref="1.1.5")
|
||||
esp32.add_idf_component(name="espressif/esp_hosted", ref="2.12.7")
|
||||
esp32.add_idf_component(name="espressif/esp_hosted", ref="2.12.8")
|
||||
else:
|
||||
esp32.add_idf_component(name="espressif/esp_wifi_remote", ref="0.13.0")
|
||||
esp32.add_idf_component(name="espressif/eppp_link", ref="0.2.0")
|
||||
|
||||
@@ -3,6 +3,7 @@ from typing import Any
|
||||
|
||||
import esphome.codegen as cg
|
||||
from esphome.components import esp32, update
|
||||
from esphome.components.const import CONF_SHA256
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import CONF_ID, CONF_PATH, CONF_SOURCE, CONF_TYPE
|
||||
from esphome.core import CORE, ID, HexInt
|
||||
@@ -11,7 +12,6 @@ CODEOWNERS = ["@swoboda1337"]
|
||||
AUTO_LOAD = ["sha256", "watchdog", "json"]
|
||||
DEPENDENCIES = ["esp32_hosted"]
|
||||
|
||||
CONF_SHA256 = "sha256"
|
||||
CONF_HTTP_REQUEST_ID = "http_request_id"
|
||||
|
||||
TYPE_EMBEDDED = "embedded"
|
||||
@@ -75,7 +75,7 @@ def _validate_firmware(config: dict[str, Any]) -> None:
|
||||
return
|
||||
|
||||
path = CORE.relative_config_path(config[CONF_PATH])
|
||||
with open(path, "rb") as f:
|
||||
with path.open("rb") as f:
|
||||
firmware_data = f.read()
|
||||
calculated = hashlib.sha256(firmware_data).hexdigest()
|
||||
expected = config[CONF_SHA256].lower()
|
||||
@@ -93,7 +93,7 @@ async def to_code(config: dict[str, Any]) -> None:
|
||||
|
||||
if config[CONF_TYPE] == TYPE_EMBEDDED:
|
||||
path = config[CONF_PATH]
|
||||
with open(CORE.relative_config_path(path), "rb") as f:
|
||||
with CORE.relative_config_path(path).open("rb") as f:
|
||||
firmware_data = f.read()
|
||||
rhs = [HexInt(x) for x in firmware_data]
|
||||
arr_id = ID(f"{config[CONF_ID]}_data", is_declaration=True, type=cg.uint8)
|
||||
|
||||
@@ -472,7 +472,7 @@ def _decode_pc(config, addr):
|
||||
command = [idedata.addr2line_path, "-pfiaC", "-e", idedata.firmware_elf_path, addr]
|
||||
try:
|
||||
translation = subprocess.check_output(command, close_fds=False).decode().strip()
|
||||
except Exception: # pylint: disable=broad-except
|
||||
except Exception: # noqa: BLE001 # pylint: disable=broad-except
|
||||
_LOGGER.debug("Caught exception for command %s", command, exc_info=1)
|
||||
return
|
||||
|
||||
|
||||
@@ -133,7 +133,7 @@ CONFIG_SCHEMA = cv.All(
|
||||
host=8082,
|
||||
): cv.port,
|
||||
cv.Optional(CONF_ALLOW_PARTITION_ACCESS, default=False): cv.boolean,
|
||||
cv.Optional(CONF_PASSWORD): cv.string,
|
||||
cv.Optional(CONF_PASSWORD): cv.sensitive(),
|
||||
cv.Optional(CONF_NUM_ATTEMPTS): cv.invalid(
|
||||
f"'{CONF_SAFE_MODE}' (and its related configuration variables) has moved from 'ota' to its own component. See https://esphome.io/components/safe_mode"
|
||||
),
|
||||
|
||||
@@ -17,7 +17,7 @@ from esphome.core import HexInt
|
||||
from esphome.types import ConfigType
|
||||
|
||||
CODEOWNERS = ["@jesserockz"]
|
||||
|
||||
AUTO_LOAD = ["network"]
|
||||
|
||||
byte_vector = cg.std_vector.template(cg.uint8)
|
||||
peer_address_t = cg.std_ns.class_("array").template(cg.uint8, 6)
|
||||
|
||||
@@ -149,12 +149,6 @@ bool ESPNowComponent::is_wifi_enabled() {
|
||||
}
|
||||
|
||||
void ESPNowComponent::setup() {
|
||||
#ifndef USE_WIFI
|
||||
// Initialize LwIP stack for wake_loop_threadsafe() socket support
|
||||
// When WiFi component is present, it handles esp_netif_init()
|
||||
ESP_ERROR_CHECK(esp_netif_init());
|
||||
#endif
|
||||
|
||||
if (this->enable_on_boot_) {
|
||||
this->enable_();
|
||||
} else {
|
||||
@@ -174,8 +168,6 @@ void ESPNowComponent::enable() {
|
||||
|
||||
void ESPNowComponent::enable_() {
|
||||
if (!this->is_wifi_enabled()) {
|
||||
esp_event_loop_create_default();
|
||||
|
||||
wifi_init_config_t cfg = WIFI_INIT_CONFIG_DEFAULT();
|
||||
|
||||
ESP_ERROR_CHECK(esp_wifi_init(&cfg));
|
||||
|
||||
@@ -2,6 +2,7 @@ from dataclasses import dataclass
|
||||
import logging
|
||||
|
||||
from esphome import automation, pins
|
||||
from esphome.automation import Condition
|
||||
import esphome.codegen as cg
|
||||
from esphome.components.network import ip_address_literal
|
||||
from esphome.config_helpers import filter_source_files_from_platform
|
||||
@@ -13,6 +14,7 @@ from esphome.const import (
|
||||
CONF_DNS1,
|
||||
CONF_DNS2,
|
||||
CONF_DOMAIN,
|
||||
CONF_ENABLE_ON_BOOT,
|
||||
CONF_GATEWAY,
|
||||
CONF_ID,
|
||||
CONF_INTERRUPT_PIN,
|
||||
@@ -163,7 +165,7 @@ _IDF6_ETHERNET_COMPONENTS: dict[str, IDFRegistryComponent] = {
|
||||
"KSZ8081": IDFRegistryComponent("espressif/ksz80xx", "1.0.0"),
|
||||
"KSZ8081RNA": IDFRegistryComponent("espressif/ksz80xx", "1.0.0"),
|
||||
"W5500": IDFRegistryComponent("espressif/w5500", "1.0.1"),
|
||||
"DM9051": IDFRegistryComponent("espressif/dm9051", "1.0.0"),
|
||||
"DM9051": IDFRegistryComponent("espressif/dm9051", "1.1.0"),
|
||||
"ENC28J60": IDFRegistryComponent("espressif/enc28j60", "1.0.1"),
|
||||
"LAN8670": IDFRegistryComponent("espressif/lan867x", "2.0.0"),
|
||||
}
|
||||
@@ -217,6 +219,10 @@ MANUAL_IP_SCHEMA = cv.Schema(
|
||||
|
||||
EthernetComponent = ethernet_ns.class_("EthernetComponent", cg.Component)
|
||||
ManualIP = ethernet_ns.struct("ManualIP")
|
||||
EthernetConnectedCondition = ethernet_ns.class_("EthernetConnectedCondition", Condition)
|
||||
EthernetEnabledCondition = ethernet_ns.class_("EthernetEnabledCondition", Condition)
|
||||
EthernetEnableAction = ethernet_ns.class_("EthernetEnableAction", automation.Action)
|
||||
EthernetDisableAction = ethernet_ns.class_("EthernetDisableAction", automation.Action)
|
||||
|
||||
|
||||
def _is_framework_spi_polling_mode_supported() -> bool:
|
||||
@@ -348,6 +354,7 @@ BASE_SCHEMA = cv.Schema(
|
||||
cv.Optional(CONF_DOMAIN, default=".local"): cv.domain_name,
|
||||
cv.Optional(CONF_USE_ADDRESS): cv.string_strict,
|
||||
cv.Optional(CONF_MAC_ADDRESS): cv.mac_address,
|
||||
cv.Optional(CONF_ENABLE_ON_BOOT, default=True): cv.boolean,
|
||||
cv.Optional(CONF_ON_CONNECT): automation.validate_automation(single=True),
|
||||
cv.Optional(CONF_ON_DISCONNECT): automation.validate_automation(single=True),
|
||||
}
|
||||
@@ -494,6 +501,9 @@ async def to_code(config):
|
||||
|
||||
cg.add(var.set_type(ETHERNET_TYPES[config[CONF_TYPE]]))
|
||||
cg.add(var.set_use_address(config[CONF_USE_ADDRESS]))
|
||||
# enable_on_boot defaults to true in C++ - only set if false
|
||||
if not config[CONF_ENABLE_ON_BOOT]:
|
||||
cg.add(var.set_enable_on_boot(False))
|
||||
CORE.data.setdefault(KEY_ETHERNET, {})[ETHERNET_TYPE_KEY] = config[CONF_TYPE]
|
||||
|
||||
if CONF_MANUAL_IP in config:
|
||||
@@ -715,3 +725,21 @@ def _filter_source_files() -> list[str]:
|
||||
|
||||
|
||||
FILTER_SOURCE_FILES = _filter_source_files
|
||||
|
||||
|
||||
async def _new_pvariable_to_code(config, id_, template_arg, args):
|
||||
return cg.new_Pvariable(id_, template_arg)
|
||||
|
||||
|
||||
for _name, _cls in (
|
||||
("ethernet.connected", EthernetConnectedCondition),
|
||||
("ethernet.enabled", EthernetEnabledCondition),
|
||||
):
|
||||
automation.register_condition(_name, _cls, cv.Schema({}))(_new_pvariable_to_code)
|
||||
for _name, _cls in (
|
||||
("ethernet.enable", EthernetEnableAction),
|
||||
("ethernet.disable", EthernetDisableAction),
|
||||
):
|
||||
automation.register_action(_name, _cls, cv.Schema({}), synchronous=True)(
|
||||
_new_pvariable_to_code
|
||||
)
|
||||
|
||||
30
esphome/components/ethernet/automation.h
Normal file
30
esphome/components/ethernet/automation.h
Normal file
@@ -0,0 +1,30 @@
|
||||
#pragma once
|
||||
|
||||
#include "esphome/core/defines.h"
|
||||
#ifdef USE_ETHERNET
|
||||
#include "ethernet_component.h"
|
||||
|
||||
namespace esphome::ethernet {
|
||||
|
||||
template<typename... Ts> class EthernetConnectedCondition : public Condition<Ts...> {
|
||||
public:
|
||||
bool check(const Ts &...x) override { return global_eth_component->is_connected(); }
|
||||
};
|
||||
|
||||
template<typename... Ts> class EthernetEnabledCondition : public Condition<Ts...> {
|
||||
public:
|
||||
bool check(const Ts &...x) override { return global_eth_component->is_enabled(); }
|
||||
};
|
||||
|
||||
template<typename... Ts> class EthernetEnableAction : public Action<Ts...> {
|
||||
public:
|
||||
void play(const Ts &...x) override { global_eth_component->enable(); }
|
||||
};
|
||||
|
||||
template<typename... Ts> class EthernetDisableAction : public Action<Ts...> {
|
||||
public:
|
||||
void play(const Ts &...x) override { global_eth_component->disable(); }
|
||||
};
|
||||
|
||||
} // namespace esphome::ethernet
|
||||
#endif
|
||||
@@ -124,6 +124,17 @@ class EthernetComponent final : public Component {
|
||||
void on_powerdown() override { powerdown(); }
|
||||
bool is_connected() { return this->state_ == EthernetComponentState::CONNECTED; }
|
||||
|
||||
// Per-interface lifecycle (parallels WiFiComponent::enable/disable/is_disabled).
|
||||
// enable_on_boot defaults to true; when false, setup() runs all the driver/netif
|
||||
// installation but skips esp_eth_start(), keeping the link cold until enable() is
|
||||
// called. This is the primary lever for memory reclamation in multi-interface
|
||||
// configurations where only one interface should carry traffic at a time.
|
||||
void set_enable_on_boot(bool enable_on_boot) { this->enable_on_boot_ = enable_on_boot; }
|
||||
void enable();
|
||||
void disable();
|
||||
bool is_disabled() { return this->disabled_; }
|
||||
bool is_enabled() { return !this->disabled_; }
|
||||
|
||||
void set_type(EthernetType type);
|
||||
#ifdef USE_ETHERNET_MANUAL_IP
|
||||
void set_manual_ip(const ManualIP &manual_ip);
|
||||
@@ -194,6 +205,16 @@ class EthernetComponent final : public Component {
|
||||
void finish_connect_();
|
||||
void dump_connect_params_();
|
||||
|
||||
#ifdef USE_ESP32
|
||||
// ESP-IDF only: defers the SPI bus init, netif creation, MAC/PHY install, driver
|
||||
// install, netif attach, and event handler registration (which together allocate
|
||||
// ~3-8KB of DMA-capable internal SRAM via SPI driver state + eth driver RX queue)
|
||||
// until ethernet actually needs to come up. Idempotent — guarded by the
|
||||
// ethernet_initialized_ flag. Called from setup() when enable_on_boot_=true, or
|
||||
// from enable() on first runtime enable. Mirrors wifi_lazy_init_() in WiFi.
|
||||
void ethernet_lazy_init_();
|
||||
#endif
|
||||
|
||||
#ifdef USE_ETHERNET_IP_STATE_LISTENERS
|
||||
void notify_ip_state_listeners_();
|
||||
#endif
|
||||
@@ -287,6 +308,17 @@ class EthernetComponent final : public Component {
|
||||
bool started_{false};
|
||||
bool connected_{false};
|
||||
bool got_ipv4_address_{false};
|
||||
// Codegen-time YAML option. When false, setup() defers esp_eth_start().
|
||||
bool enable_on_boot_{true};
|
||||
// Mirror of "is the link intentionally stopped" — set when setup() honors
|
||||
// enable_on_boot=false, cleared by enable(), set again by disable().
|
||||
bool disabled_{false};
|
||||
#ifdef USE_ESP32
|
||||
// Tracks whether ethernet_lazy_init_() has completed successfully. Allows enable()
|
||||
// to be called at runtime after enable_on_boot:false without re-allocating, and
|
||||
// ensures setup() skips the heavy init when enable_on_boot_ is false.
|
||||
bool ethernet_initialized_{false};
|
||||
#endif
|
||||
#if LWIP_IPV6
|
||||
uint8_t ipv6_count_{0};
|
||||
bool ipv6_setup_done_{false};
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
#include "esphome/core/application.h"
|
||||
#include "esphome/core/helpers.h"
|
||||
#include "esphome/core/log.h"
|
||||
#include "w5500_custom_spi.h"
|
||||
|
||||
#include <lwip/dns.h>
|
||||
#include <cinttypes>
|
||||
@@ -137,6 +138,24 @@ void EthernetComponent::setup() {
|
||||
delay(300); // NOLINT
|
||||
}
|
||||
|
||||
if (this->enable_on_boot_) {
|
||||
this->ethernet_lazy_init_();
|
||||
if (!this->ethernet_initialized_) {
|
||||
// lazy_init bailed early via ESPHL_ERROR_CHECK or mark_failed; nothing more to do.
|
||||
return;
|
||||
}
|
||||
esp_err_t err = esp_eth_start(this->eth_handle_);
|
||||
ESPHL_ERROR_CHECK(err, "ETH start error");
|
||||
} else {
|
||||
ESP_LOGCONFIG(TAG, "Skipping init (enable_on_boot: false)");
|
||||
this->disabled_ = true;
|
||||
}
|
||||
}
|
||||
|
||||
void EthernetComponent::ethernet_lazy_init_() {
|
||||
if (this->ethernet_initialized_)
|
||||
return;
|
||||
|
||||
esp_err_t err;
|
||||
|
||||
#ifdef USE_ETHERNET_SPI
|
||||
@@ -163,11 +182,7 @@ void EthernetComponent::setup() {
|
||||
err = spi_bus_initialize(host, &buscfg, SPI_DMA_CH_AUTO);
|
||||
ESPHL_ERROR_CHECK(err, "SPI bus initialize error");
|
||||
#endif
|
||||
|
||||
err = esp_netif_init();
|
||||
ESPHL_ERROR_CHECK(err, "ETH netif init error");
|
||||
err = esp_event_loop_create_default();
|
||||
ESPHL_ERROR_CHECK(err, "ETH event loop error");
|
||||
// Network interface setup handled by network component
|
||||
|
||||
esp_netif_config_t cfg = ESP_NETIF_DEFAULT_ETH();
|
||||
this->eth_netif_ = esp_netif_new(&cfg);
|
||||
@@ -207,6 +222,10 @@ void EthernetComponent::setup() {
|
||||
#ifdef USE_ETHERNET_SPI_POLLING_SUPPORT
|
||||
w5500_config.poll_period_ms = this->polling_interval_;
|
||||
#endif
|
||||
// Install the custom SPI driver that offloads the bulk RX/TX frame transfers off the busy-wait
|
||||
// path. w5500_config (and the devcfg it references) outlives esp_eth_mac_new_w5500() below, which
|
||||
// runs the driver's init().
|
||||
install_w5500_async_spi(w5500_config);
|
||||
#elif defined(USE_ETHERNET_DM9051)
|
||||
dm9051_config.int_gpio_num = this->interrupt_pin_;
|
||||
#ifdef USE_ETHERNET_SPI_POLLING_SUPPORT
|
||||
@@ -370,9 +389,41 @@ void EthernetComponent::setup() {
|
||||
ESPHL_ERROR_CHECK(err, "GOT IPv6 event handler register error");
|
||||
#endif /* USE_NETWORK_IPV6 */
|
||||
|
||||
/* start Ethernet driver state machine */
|
||||
err = esp_eth_start(this->eth_handle_);
|
||||
ESPHL_ERROR_CHECK(err, "ETH start error");
|
||||
this->ethernet_initialized_ = true;
|
||||
}
|
||||
|
||||
void EthernetComponent::enable() {
|
||||
if (!this->disabled_)
|
||||
return;
|
||||
|
||||
ESP_LOGD(TAG, "Enabling");
|
||||
this->ethernet_lazy_init_();
|
||||
if (!this->ethernet_initialized_) {
|
||||
ESP_LOGE(TAG, "Cannot enable - init failed");
|
||||
return;
|
||||
}
|
||||
esp_err_t err = esp_eth_start(this->eth_handle_);
|
||||
if (err != ESP_OK) {
|
||||
ESP_LOGE(TAG, "esp_eth_start failed: %s", esp_err_to_name(err));
|
||||
return;
|
||||
}
|
||||
this->disabled_ = false;
|
||||
// The ETH_EVENT_START handler will set started_=true; the loop state machine
|
||||
// will then drive the STOPPED -> CONNECTING -> CONNECTED transitions.
|
||||
this->enable_loop();
|
||||
}
|
||||
|
||||
void EthernetComponent::disable() {
|
||||
if (this->disabled_)
|
||||
return;
|
||||
|
||||
ESP_LOGD(TAG, "Disabling");
|
||||
esp_err_t err = esp_eth_stop(this->eth_handle_);
|
||||
if (err != ESP_OK) {
|
||||
ESP_LOGW(TAG, "esp_eth_stop failed: %s — disabling anyway", esp_err_to_name(err));
|
||||
}
|
||||
this->disabled_ = true;
|
||||
// ETH_EVENT_STOP will clear started_; loop() will transition to STOPPED.
|
||||
}
|
||||
|
||||
void EthernetComponent::dump_config() {
|
||||
@@ -486,6 +537,8 @@ void EthernetComponent::dump_config() {
|
||||
|
||||
network::IPAddresses EthernetComponent::get_ip_addresses() {
|
||||
network::IPAddresses addresses;
|
||||
if (!this->ethernet_initialized_)
|
||||
return addresses; // all-zero IPs
|
||||
esp_netif_ip_info_t ip;
|
||||
esp_err_t err = esp_netif_get_ip_info(this->eth_netif_, &ip);
|
||||
if (err != ESP_OK) {
|
||||
@@ -708,6 +761,10 @@ void EthernetComponent::start_connect_() {
|
||||
}
|
||||
|
||||
void EthernetComponent::dump_connect_params_() {
|
||||
if (!this->ethernet_initialized_) {
|
||||
ESP_LOGCONFIG(TAG, " uninitialized/disabled");
|
||||
return;
|
||||
}
|
||||
esp_netif_ip_info_t ip;
|
||||
esp_netif_get_ip_info(this->eth_netif_, &ip);
|
||||
const ip_addr_t *dns_ip1;
|
||||
@@ -775,6 +832,16 @@ void EthernetComponent::add_phy_register(PHYRegister register_value) { this->phy
|
||||
#endif
|
||||
|
||||
void EthernetComponent::get_eth_mac_address_raw(uint8_t *mac) {
|
||||
if (!this->ethernet_initialized_) {
|
||||
// External callers (mdns, ethernet_info, etc.) may ask for the MAC before/regardless
|
||||
// of whether ethernet is enabled. Use the configured MAC if set, else the system ETH MAC.
|
||||
if (this->fixed_mac_.has_value()) {
|
||||
memcpy(mac, this->fixed_mac_->data(), 6);
|
||||
} else {
|
||||
esp_read_mac(mac, ESP_MAC_ETH);
|
||||
}
|
||||
return;
|
||||
}
|
||||
esp_err_t err;
|
||||
err = esp_eth_ioctl(this->eth_handle_, ETH_CMD_G_MAC_ADDR, mac);
|
||||
ESPHL_ERROR_CHECK(err, "ETH_CMD_G_MAC error");
|
||||
@@ -794,6 +861,8 @@ const char *EthernetComponent::get_eth_mac_address_pretty_into_buffer(
|
||||
}
|
||||
|
||||
eth_duplex_t EthernetComponent::get_duplex_mode() {
|
||||
if (!this->ethernet_initialized_)
|
||||
return ETH_DUPLEX_HALF;
|
||||
esp_err_t err;
|
||||
eth_duplex_t duplex_mode;
|
||||
err = esp_eth_ioctl(this->eth_handle_, ETH_CMD_G_DUPLEX_MODE, &duplex_mode);
|
||||
@@ -802,6 +871,8 @@ eth_duplex_t EthernetComponent::get_duplex_mode() {
|
||||
}
|
||||
|
||||
eth_speed_t EthernetComponent::get_link_speed() {
|
||||
if (!this->ethernet_initialized_)
|
||||
return ETH_SPEED_10M;
|
||||
esp_err_t err;
|
||||
eth_speed_t speed;
|
||||
err = esp_eth_ioctl(this->eth_handle_, ETH_CMD_G_SPEED, &speed);
|
||||
|
||||
@@ -361,6 +361,23 @@ void EthernetComponent::set_cs_pin(uint8_t cs_pin) { this->cs_pin_ = cs_pin; }
|
||||
void EthernetComponent::set_interrupt_pin(int8_t interrupt_pin) { this->interrupt_pin_ = interrupt_pin; }
|
||||
void EthernetComponent::set_reset_pin(int8_t reset_pin) { this->reset_pin_ = reset_pin; }
|
||||
|
||||
void EthernetComponent::enable() {
|
||||
// RP2040 uses arduino-pico's LwipIntfDev which manages link state internally;
|
||||
// there is no clean enable/disable hook today. The YAML option is accepted on
|
||||
// RP2040 for schema parity but has no effect.
|
||||
if (!this->disabled_)
|
||||
return;
|
||||
ESP_LOGW(TAG, "enable_on_boot/disable not supported");
|
||||
this->disabled_ = false;
|
||||
}
|
||||
|
||||
void EthernetComponent::disable() {
|
||||
if (this->disabled_)
|
||||
return;
|
||||
ESP_LOGW(TAG, "enable_on_boot/disable not supported");
|
||||
this->disabled_ = true;
|
||||
}
|
||||
|
||||
} // namespace esphome::ethernet
|
||||
|
||||
#endif // USE_ETHERNET && USE_RP2040
|
||||
|
||||
118
esphome/components/ethernet/w5500_custom_spi.cpp
Normal file
118
esphome/components/ethernet/w5500_custom_spi.cpp
Normal file
@@ -0,0 +1,118 @@
|
||||
#include "w5500_custom_spi.h"
|
||||
|
||||
#if defined(USE_ESP32) && defined(USE_ETHERNET_W5500)
|
||||
|
||||
#include <driver/spi_master.h>
|
||||
#include <freertos/FreeRTOS.h>
|
||||
#include <freertos/semphr.h>
|
||||
#include <cstring>
|
||||
#include <new>
|
||||
|
||||
namespace esphome::ethernet {
|
||||
|
||||
namespace {
|
||||
|
||||
// Per-device context returned by init() and handed back to read/write/deinit.
|
||||
struct W5500CustomSpiContext {
|
||||
spi_device_handle_t handle;
|
||||
SemaphoreHandle_t lock;
|
||||
};
|
||||
|
||||
// Transfers up to the ESP32 SPI hardware FIFO size (64 bytes) stay on the polling path; larger
|
||||
// transfers (the frame payloads) use the blocking, DMA-backed transmit.
|
||||
constexpr uint32_t W5500_SPI_BULK_THRESHOLD = 64;
|
||||
constexpr uint32_t W5500_SPI_LOCK_TIMEOUT_MS = 50;
|
||||
|
||||
void *w5500_custom_spi_init(const void *spi_config) {
|
||||
const auto *config = static_cast<const eth_w5500_config_t *>(spi_config);
|
||||
auto *ctx = new (std::nothrow) W5500CustomSpiContext{};
|
||||
if (ctx == nullptr) {
|
||||
return nullptr;
|
||||
}
|
||||
// The W5500 SPI frame carries the 16-bit address in the command phase and the 8-bit control
|
||||
// byte in the address phase; mirror what the stock driver configures.
|
||||
spi_device_interface_config_t devcfg = *config->spi_devcfg;
|
||||
devcfg.command_bits = 16;
|
||||
devcfg.address_bits = 8;
|
||||
if (spi_bus_add_device(config->spi_host_id, &devcfg, &ctx->handle) != ESP_OK) {
|
||||
delete ctx;
|
||||
return nullptr;
|
||||
}
|
||||
ctx->lock = xSemaphoreCreateMutex();
|
||||
if (ctx->lock == nullptr) {
|
||||
spi_bus_remove_device(ctx->handle);
|
||||
delete ctx;
|
||||
return nullptr;
|
||||
}
|
||||
return ctx;
|
||||
}
|
||||
|
||||
esp_err_t w5500_custom_spi_deinit(void *spi_ctx) {
|
||||
auto *ctx = static_cast<W5500CustomSpiContext *>(spi_ctx);
|
||||
spi_bus_remove_device(ctx->handle);
|
||||
vSemaphoreDelete(ctx->lock);
|
||||
delete ctx;
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
// Runs one transaction under the device lock, choosing the polling vs blocking transmit by size.
|
||||
// Bulk payloads (> FIFO size) block so the calling task sleeps while DMA runs; small register
|
||||
// accesses stay on the cheaper polling path. Used by both read and write.
|
||||
esp_err_t w5500_custom_spi_transfer(W5500CustomSpiContext *ctx, spi_transaction_t *trans, uint32_t len) {
|
||||
if (xSemaphoreTake(ctx->lock, pdMS_TO_TICKS(W5500_SPI_LOCK_TIMEOUT_MS)) != pdTRUE) {
|
||||
return ESP_ERR_TIMEOUT;
|
||||
}
|
||||
esp_err_t ret;
|
||||
if (len > W5500_SPI_BULK_THRESHOLD) {
|
||||
ret = spi_device_transmit(ctx->handle, trans);
|
||||
} else {
|
||||
ret = spi_device_polling_transmit(ctx->handle, trans);
|
||||
}
|
||||
xSemaphoreGive(ctx->lock);
|
||||
return ret;
|
||||
}
|
||||
|
||||
esp_err_t w5500_custom_spi_write(void *spi_ctx, uint32_t cmd, uint32_t addr, const void *data, uint32_t len) {
|
||||
auto *ctx = static_cast<W5500CustomSpiContext *>(spi_ctx);
|
||||
spi_transaction_t trans = {};
|
||||
trans.cmd = static_cast<uint16_t>(cmd);
|
||||
trans.addr = addr;
|
||||
trans.length = 8 * len;
|
||||
trans.tx_buffer = data;
|
||||
return w5500_custom_spi_transfer(ctx, &trans, len);
|
||||
}
|
||||
|
||||
esp_err_t w5500_custom_spi_read(void *spi_ctx, uint32_t cmd, uint32_t addr, void *data, uint32_t len) {
|
||||
auto *ctx = static_cast<W5500CustomSpiContext *>(spi_ctx);
|
||||
spi_transaction_t trans = {};
|
||||
// Reads of <= 4 bytes use the transaction's inline RX buffer to avoid 4-byte boundary
|
||||
// overwrites of adjacent registers (same guard the stock driver uses).
|
||||
const bool use_rxdata = len <= 4;
|
||||
trans.flags = use_rxdata ? SPI_TRANS_USE_RXDATA : 0;
|
||||
trans.cmd = static_cast<uint16_t>(cmd);
|
||||
trans.addr = addr;
|
||||
trans.length = 8 * len;
|
||||
trans.rx_buffer = data;
|
||||
esp_err_t ret = w5500_custom_spi_transfer(ctx, &trans, len);
|
||||
if (use_rxdata && (ret == ESP_OK)) {
|
||||
memcpy(data, trans.rx_data, len);
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
void install_w5500_async_spi(eth_w5500_config_t &config) {
|
||||
// Point the custom driver's config at the W5500 config itself; init() reads spi_host_id and
|
||||
// spi_devcfg back out of it. The self-reference is valid because both the config and the
|
||||
// spi_devcfg it points at outlive the esp_eth_mac_new_w5500() call that runs init().
|
||||
config.custom_spi_driver.config = &config;
|
||||
config.custom_spi_driver.init = w5500_custom_spi_init;
|
||||
config.custom_spi_driver.deinit = w5500_custom_spi_deinit;
|
||||
config.custom_spi_driver.read = w5500_custom_spi_read;
|
||||
config.custom_spi_driver.write = w5500_custom_spi_write;
|
||||
}
|
||||
|
||||
} // namespace esphome::ethernet
|
||||
|
||||
#endif // USE_ESP32 && USE_ETHERNET_W5500
|
||||
35
esphome/components/ethernet/w5500_custom_spi.h
Normal file
35
esphome/components/ethernet/w5500_custom_spi.h
Normal file
@@ -0,0 +1,35 @@
|
||||
#pragma once
|
||||
|
||||
#include "esphome/core/defines.h"
|
||||
|
||||
#if defined(USE_ESP32) && defined(USE_ETHERNET_W5500)
|
||||
|
||||
#include <esp_idf_version.h>
|
||||
// IDF 6.0 moved the per-chip SPI MAC drivers to the Espressif Component Registry; eth_w5500_config_t
|
||||
// is no longer reachable through esp_eth.h and needs the explicit header.
|
||||
#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(6, 0, 0)
|
||||
#include <esp_eth_mac_w5500.h>
|
||||
#else
|
||||
#include <esp_eth.h>
|
||||
#endif
|
||||
|
||||
namespace esphome::ethernet {
|
||||
|
||||
// Installs a custom W5500 SPI driver that offloads the bulk frame transfers off the busy-wait path.
|
||||
//
|
||||
// The stock W5500 driver runs every SPI transfer through spi_device_polling_transmit(), which
|
||||
// busy-waits the CPU for the whole transfer. The frame payload (one large read per received frame,
|
||||
// one large write per transmitted frame) is by far the biggest transfer, so the RX task and the TX
|
||||
// caller each spin for hundreds of microseconds per frame. This driver sends payload transfers
|
||||
// through the blocking, interrupt-driven spi_device_transmit() instead, so the calling task sleeps
|
||||
// while DMA moves the bytes. Small register accesses stay on the polling path, where the busy-wait
|
||||
// is cheaper than an interrupt round-trip.
|
||||
//
|
||||
// Must be called before esp_eth_mac_new_w5500(). The driver reads spi_host_id and spi_devcfg back
|
||||
// out of `config` in its init() callback, so `config` (and the spi_devcfg it points at) must stay
|
||||
// alive until esp_eth_mac_new_w5500() returns.
|
||||
void install_w5500_async_spi(eth_w5500_config_t &config);
|
||||
|
||||
} // namespace esphome::ethernet
|
||||
|
||||
#endif // USE_ESP32 && USE_ETHERNET_W5500
|
||||
@@ -81,7 +81,7 @@ def _process_single_config(config: dict[str, Any]) -> None:
|
||||
elif conf[CONF_TYPE] == TYPE_LOCAL:
|
||||
components_dir = Path(CORE.relative_config_path(conf[CONF_PATH]))
|
||||
else:
|
||||
raise NotImplementedError()
|
||||
raise NotImplementedError
|
||||
|
||||
if config[CONF_COMPONENTS] == "all":
|
||||
num_components = len(list(components_dir.glob("*/__init__.py")))
|
||||
|
||||
@@ -401,7 +401,7 @@ def validate_file_shorthand(value):
|
||||
data[CONF_WEIGHT] = weight[1:]
|
||||
return font_file_schema(data)
|
||||
|
||||
if value.startswith("http://") or value.startswith("https://"):
|
||||
if value.startswith(("http://", "https://")):
|
||||
return font_file_schema(
|
||||
{
|
||||
CONF_TYPE: TYPE_WEB,
|
||||
@@ -563,13 +563,13 @@ async def to_code(config):
|
||||
point_set.update(flatten(config[CONF_GLYPHS]))
|
||||
# Create the codepoint to font file map
|
||||
base_font = FONT_CACHE[config[CONF_FILE]]
|
||||
point_font_map: dict[str, Face] = {c: base_font for c in point_set}
|
||||
point_font_map: dict[str, Face] = dict.fromkeys(point_set, base_font)
|
||||
# process extras, updating the map and extending the codepoint list
|
||||
for extra in config[CONF_EXTRAS]:
|
||||
extra_points = flatten(extra[CONF_GLYPHS])
|
||||
point_set.update(extra_points)
|
||||
extra_font = FONT_CACHE[extra[CONF_FILE]]
|
||||
point_font_map.update({c: extra_font for c in extra_points})
|
||||
point_font_map.update(dict.fromkeys(extra_points, extra_font))
|
||||
|
||||
codepoints = list(point_set)
|
||||
codepoints.sort(key=functools.cmp_to_key(glyph_comparator))
|
||||
@@ -594,7 +594,9 @@ async def to_code(config):
|
||||
x.height,
|
||||
]
|
||||
for (x, y) in zip(
|
||||
glyph_args, list(accumulate([len(x.bitmap_data) for x in glyph_args]))
|
||||
glyph_args,
|
||||
list(accumulate([len(x.bitmap_data) for x in glyph_args])),
|
||||
strict=True,
|
||||
)
|
||||
]
|
||||
|
||||
|
||||
@@ -74,8 +74,6 @@ def _final_validate(config):
|
||||
if not use_interrupt:
|
||||
return config
|
||||
|
||||
pin_num = config[CONF_PIN][CONF_NUMBER]
|
||||
|
||||
# Expander pins (e.g. PCF8574, MCP23017) don't support direct interrupt
|
||||
# attachment — only internal/native GPIO pins do.
|
||||
if pins.PIN_SCHEMA_REGISTRY.get_key(config[CONF_PIN]) != CORE.target_platform:
|
||||
@@ -87,6 +85,8 @@ def _final_validate(config):
|
||||
config[CONF_USE_INTERRUPT] = False
|
||||
return config
|
||||
|
||||
pin_num = config[CONF_PIN][CONF_NUMBER]
|
||||
|
||||
# GPIO16 on ESP8266 doesn't support interrupts through attachInterrupt().
|
||||
if CORE.is_esp8266 and pin_num == 16:
|
||||
_LOGGER.warning(
|
||||
|
||||
@@ -5,7 +5,23 @@ namespace esphome::gree {
|
||||
|
||||
static const char *const TAG = "gree.climate";
|
||||
|
||||
climate::ClimateTraits GreeClimate::traits() {
|
||||
auto t = climate_ir::ClimateIR::traits();
|
||||
// ClimateIR unconditionally includes HEAT_COOL in the base mode set; remove it when heat is not supported.
|
||||
if (!this->supports_heat_) {
|
||||
auto modes = t.get_supported_modes();
|
||||
modes.erase(climate::CLIMATE_MODE_HEAT_COOL);
|
||||
t.set_supported_modes(modes);
|
||||
}
|
||||
return t;
|
||||
}
|
||||
|
||||
void GreeClimate::set_model(Model model) {
|
||||
if (model == GREE_YAN) {
|
||||
// YAN only has a vertical vane; the horizontal swing IR bytes are not defined for this model.
|
||||
this->swing_modes_.erase(climate::CLIMATE_SWING_HORIZONTAL);
|
||||
this->swing_modes_.erase(climate::CLIMATE_SWING_BOTH);
|
||||
}
|
||||
if (model == GREE_YX1FF) {
|
||||
this->fan_modes_.insert(climate::CLIMATE_FAN_QUIET); // YX1FF 4 speed
|
||||
this->presets_.insert(climate::CLIMATE_PRESET_NONE); // YX1FF sleep mode
|
||||
|
||||
@@ -94,6 +94,7 @@ class GreeClimate : public climate_ir::ClimateIR {
|
||||
protected:
|
||||
// Transmit via IR the state of this climate controller.
|
||||
void transmit_state() override;
|
||||
climate::ClimateTraits traits() override;
|
||||
|
||||
uint8_t operation_mode_();
|
||||
uint8_t fan_speed_();
|
||||
|
||||
@@ -63,71 +63,88 @@ void GrowattSolar::on_modbus_data(const std::vector<uint8_t> &data) {
|
||||
|
||||
switch (this->protocol_version_) {
|
||||
case RTU: {
|
||||
publish_1_reg_sensor_state(this->inverter_status_, 0, 1);
|
||||
publish_1_reg_sensor_state(this->inverter_status_, RTU_INVERTER_STATUS, 1);
|
||||
|
||||
publish_2_reg_sensor_state(this->pv_active_power_sensor_, 1, 2, ONE_DEC_UNIT);
|
||||
publish_2_reg_sensor_state(this->pv_active_power_sensor_, RTU_PV_ACTIVE_POWER, RTU_PV_ACTIVE_POWER + 1,
|
||||
ONE_DEC_UNIT);
|
||||
|
||||
publish_1_reg_sensor_state(this->pvs_[0].voltage_sensor_, 3, ONE_DEC_UNIT);
|
||||
publish_1_reg_sensor_state(this->pvs_[0].current_sensor_, 4, ONE_DEC_UNIT);
|
||||
publish_2_reg_sensor_state(this->pvs_[0].active_power_sensor_, 5, 6, ONE_DEC_UNIT);
|
||||
publish_1_reg_sensor_state(this->pvs_[0].voltage_sensor_, RTU_PV1_VOLTAGE, ONE_DEC_UNIT);
|
||||
publish_1_reg_sensor_state(this->pvs_[0].current_sensor_, RTU_PV1_CURRENT, ONE_DEC_UNIT);
|
||||
publish_2_reg_sensor_state(this->pvs_[0].active_power_sensor_, RTU_PV1_ACTIVE_POWER, RTU_PV1_ACTIVE_POWER + 1,
|
||||
ONE_DEC_UNIT);
|
||||
|
||||
publish_1_reg_sensor_state(this->pvs_[1].voltage_sensor_, 7, ONE_DEC_UNIT);
|
||||
publish_1_reg_sensor_state(this->pvs_[1].current_sensor_, 8, ONE_DEC_UNIT);
|
||||
publish_2_reg_sensor_state(this->pvs_[1].active_power_sensor_, 9, 10, ONE_DEC_UNIT);
|
||||
publish_1_reg_sensor_state(this->pvs_[1].voltage_sensor_, RTU_PV2_VOLTAGE, ONE_DEC_UNIT);
|
||||
publish_1_reg_sensor_state(this->pvs_[1].current_sensor_, RTU_PV2_CURRENT, ONE_DEC_UNIT);
|
||||
publish_2_reg_sensor_state(this->pvs_[1].active_power_sensor_, RTU_PV2_ACTIVE_POWER, RTU_PV2_ACTIVE_POWER + 1,
|
||||
ONE_DEC_UNIT);
|
||||
|
||||
publish_2_reg_sensor_state(this->grid_active_power_sensor_, 11, 12, ONE_DEC_UNIT);
|
||||
publish_1_reg_sensor_state(this->grid_frequency_sensor_, 13, TWO_DEC_UNIT);
|
||||
publish_2_reg_sensor_state(this->grid_active_power_sensor_, RTU_GRID_ACTIVE_POWER, RTU_GRID_ACTIVE_POWER + 1,
|
||||
ONE_DEC_UNIT);
|
||||
publish_1_reg_sensor_state(this->grid_frequency_sensor_, RTU_GRID_FREQUENCY, TWO_DEC_UNIT);
|
||||
|
||||
publish_1_reg_sensor_state(this->phases_[0].voltage_sensor_, 14, ONE_DEC_UNIT);
|
||||
publish_1_reg_sensor_state(this->phases_[0].current_sensor_, 15, ONE_DEC_UNIT);
|
||||
publish_2_reg_sensor_state(this->phases_[0].active_power_sensor_, 16, 17, ONE_DEC_UNIT);
|
||||
publish_1_reg_sensor_state(this->phases_[0].voltage_sensor_, RTU_PHASE1_VOLTAGE, ONE_DEC_UNIT);
|
||||
publish_1_reg_sensor_state(this->phases_[0].current_sensor_, RTU_PHASE1_CURRENT, ONE_DEC_UNIT);
|
||||
publish_2_reg_sensor_state(this->phases_[0].active_power_sensor_, RTU_PHASE1_ACTIVE_POWER,
|
||||
RTU_PHASE1_ACTIVE_POWER + 1, ONE_DEC_UNIT);
|
||||
|
||||
publish_1_reg_sensor_state(this->phases_[1].voltage_sensor_, 18, ONE_DEC_UNIT);
|
||||
publish_1_reg_sensor_state(this->phases_[1].current_sensor_, 19, ONE_DEC_UNIT);
|
||||
publish_2_reg_sensor_state(this->phases_[1].active_power_sensor_, 20, 21, ONE_DEC_UNIT);
|
||||
publish_1_reg_sensor_state(this->phases_[1].voltage_sensor_, RTU_PHASE2_VOLTAGE, ONE_DEC_UNIT);
|
||||
publish_1_reg_sensor_state(this->phases_[1].current_sensor_, RTU_PHASE2_CURRENT, ONE_DEC_UNIT);
|
||||
publish_2_reg_sensor_state(this->phases_[1].active_power_sensor_, RTU_PHASE2_ACTIVE_POWER,
|
||||
RTU_PHASE2_ACTIVE_POWER + 1, ONE_DEC_UNIT);
|
||||
|
||||
publish_1_reg_sensor_state(this->phases_[2].voltage_sensor_, 22, ONE_DEC_UNIT);
|
||||
publish_1_reg_sensor_state(this->phases_[2].current_sensor_, 23, ONE_DEC_UNIT);
|
||||
publish_2_reg_sensor_state(this->phases_[2].active_power_sensor_, 24, 25, ONE_DEC_UNIT);
|
||||
publish_1_reg_sensor_state(this->phases_[2].voltage_sensor_, RTU_PHASE3_VOLTAGE, ONE_DEC_UNIT);
|
||||
publish_1_reg_sensor_state(this->phases_[2].current_sensor_, RTU_PHASE3_CURRENT, ONE_DEC_UNIT);
|
||||
publish_2_reg_sensor_state(this->phases_[2].active_power_sensor_, RTU_PHASE3_ACTIVE_POWER,
|
||||
RTU_PHASE3_ACTIVE_POWER + 1, ONE_DEC_UNIT);
|
||||
|
||||
publish_2_reg_sensor_state(this->today_production_, 26, 27, ONE_DEC_UNIT);
|
||||
publish_2_reg_sensor_state(this->total_energy_production_, 28, 29, ONE_DEC_UNIT);
|
||||
publish_2_reg_sensor_state(this->today_production_, RTU_TODAY_PRODUCTION, RTU_TODAY_PRODUCTION + 1, ONE_DEC_UNIT);
|
||||
publish_2_reg_sensor_state(this->total_energy_production_, RTU_TOTAL_ENERGY_PRODUCTION,
|
||||
RTU_TOTAL_ENERGY_PRODUCTION + 1, ONE_DEC_UNIT);
|
||||
|
||||
publish_1_reg_sensor_state(this->inverter_module_temp_, 32, ONE_DEC_UNIT);
|
||||
publish_1_reg_sensor_state(this->inverter_module_temp_, RTU_INVERTER_MODULE_TEMP, ONE_DEC_UNIT);
|
||||
break;
|
||||
}
|
||||
case RTU2: {
|
||||
publish_1_reg_sensor_state(this->inverter_status_, 0, 1);
|
||||
publish_1_reg_sensor_state(this->inverter_status_, RTU2_INVERTER_STATUS, 1);
|
||||
|
||||
publish_2_reg_sensor_state(this->pv_active_power_sensor_, 1, 2, ONE_DEC_UNIT);
|
||||
publish_2_reg_sensor_state(this->pv_active_power_sensor_, RTU2_PV_ACTIVE_POWER, RTU2_PV_ACTIVE_POWER + 1,
|
||||
ONE_DEC_UNIT);
|
||||
|
||||
publish_1_reg_sensor_state(this->pvs_[0].voltage_sensor_, 3, ONE_DEC_UNIT);
|
||||
publish_1_reg_sensor_state(this->pvs_[0].current_sensor_, 4, ONE_DEC_UNIT);
|
||||
publish_2_reg_sensor_state(this->pvs_[0].active_power_sensor_, 5, 6, ONE_DEC_UNIT);
|
||||
publish_1_reg_sensor_state(this->pvs_[0].voltage_sensor_, RTU2_PV1_VOLTAGE, ONE_DEC_UNIT);
|
||||
publish_1_reg_sensor_state(this->pvs_[0].current_sensor_, RTU2_PV1_CURRENT, ONE_DEC_UNIT);
|
||||
publish_2_reg_sensor_state(this->pvs_[0].active_power_sensor_, RTU2_PV1_ACTIVE_POWER, RTU2_PV1_ACTIVE_POWER + 1,
|
||||
ONE_DEC_UNIT);
|
||||
|
||||
publish_1_reg_sensor_state(this->pvs_[1].voltage_sensor_, 7, ONE_DEC_UNIT);
|
||||
publish_1_reg_sensor_state(this->pvs_[1].current_sensor_, 8, ONE_DEC_UNIT);
|
||||
publish_2_reg_sensor_state(this->pvs_[1].active_power_sensor_, 9, 10, ONE_DEC_UNIT);
|
||||
publish_1_reg_sensor_state(this->pvs_[1].voltage_sensor_, RTU2_PV2_VOLTAGE, ONE_DEC_UNIT);
|
||||
publish_1_reg_sensor_state(this->pvs_[1].current_sensor_, RTU2_PV2_CURRENT, ONE_DEC_UNIT);
|
||||
publish_2_reg_sensor_state(this->pvs_[1].active_power_sensor_, RTU2_PV2_ACTIVE_POWER, RTU2_PV2_ACTIVE_POWER + 1,
|
||||
ONE_DEC_UNIT);
|
||||
|
||||
publish_2_reg_sensor_state(this->grid_active_power_sensor_, 35, 36, ONE_DEC_UNIT);
|
||||
publish_1_reg_sensor_state(this->grid_frequency_sensor_, 37, TWO_DEC_UNIT);
|
||||
publish_2_reg_sensor_state(this->grid_active_power_sensor_, RTU2_GRID_ACTIVE_POWER, RTU2_GRID_ACTIVE_POWER + 1,
|
||||
ONE_DEC_UNIT);
|
||||
publish_1_reg_sensor_state(this->grid_frequency_sensor_, RTU2_GRID_FREQUENCY, TWO_DEC_UNIT);
|
||||
|
||||
publish_1_reg_sensor_state(this->phases_[0].voltage_sensor_, 38, ONE_DEC_UNIT);
|
||||
publish_1_reg_sensor_state(this->phases_[0].current_sensor_, 39, ONE_DEC_UNIT);
|
||||
publish_2_reg_sensor_state(this->phases_[0].active_power_sensor_, 40, 41, ONE_DEC_UNIT);
|
||||
publish_1_reg_sensor_state(this->phases_[0].voltage_sensor_, RTU2_PHASE1_VOLTAGE, ONE_DEC_UNIT);
|
||||
publish_1_reg_sensor_state(this->phases_[0].current_sensor_, RTU2_PHASE1_CURRENT, ONE_DEC_UNIT);
|
||||
publish_2_reg_sensor_state(this->phases_[0].active_power_sensor_, RTU2_PHASE1_ACTIVE_POWER,
|
||||
RTU2_PHASE1_ACTIVE_POWER + 1, ONE_DEC_UNIT);
|
||||
|
||||
publish_1_reg_sensor_state(this->phases_[1].voltage_sensor_, 42, ONE_DEC_UNIT);
|
||||
publish_1_reg_sensor_state(this->phases_[1].current_sensor_, 43, ONE_DEC_UNIT);
|
||||
publish_2_reg_sensor_state(this->phases_[1].active_power_sensor_, 44, 45, ONE_DEC_UNIT);
|
||||
publish_1_reg_sensor_state(this->phases_[1].voltage_sensor_, RTU2_PHASE2_VOLTAGE, ONE_DEC_UNIT);
|
||||
publish_1_reg_sensor_state(this->phases_[1].current_sensor_, RTU2_PHASE2_CURRENT, ONE_DEC_UNIT);
|
||||
publish_2_reg_sensor_state(this->phases_[1].active_power_sensor_, RTU2_PHASE2_ACTIVE_POWER,
|
||||
RTU2_PHASE2_ACTIVE_POWER + 1, ONE_DEC_UNIT);
|
||||
|
||||
publish_1_reg_sensor_state(this->phases_[2].voltage_sensor_, 46, ONE_DEC_UNIT);
|
||||
publish_1_reg_sensor_state(this->phases_[2].current_sensor_, 47, ONE_DEC_UNIT);
|
||||
publish_2_reg_sensor_state(this->phases_[2].active_power_sensor_, 48, 49, ONE_DEC_UNIT);
|
||||
publish_1_reg_sensor_state(this->phases_[2].voltage_sensor_, RTU2_PHASE3_VOLTAGE, ONE_DEC_UNIT);
|
||||
publish_1_reg_sensor_state(this->phases_[2].current_sensor_, RTU2_PHASE3_CURRENT, ONE_DEC_UNIT);
|
||||
publish_2_reg_sensor_state(this->phases_[2].active_power_sensor_, RTU2_PHASE3_ACTIVE_POWER,
|
||||
RTU2_PHASE3_ACTIVE_POWER + 1, ONE_DEC_UNIT);
|
||||
|
||||
publish_2_reg_sensor_state(this->today_production_, 53, 54, ONE_DEC_UNIT);
|
||||
publish_2_reg_sensor_state(this->total_energy_production_, 55, 56, ONE_DEC_UNIT);
|
||||
publish_2_reg_sensor_state(this->today_production_, RTU2_TODAY_PRODUCTION, RTU2_TODAY_PRODUCTION + 1,
|
||||
ONE_DEC_UNIT);
|
||||
publish_2_reg_sensor_state(this->total_energy_production_, RTU2_TOTAL_ENERGY_PRODUCTION,
|
||||
RTU2_TOTAL_ENERGY_PRODUCTION + 1, ONE_DEC_UNIT);
|
||||
|
||||
publish_1_reg_sensor_state(this->inverter_module_temp_, 93, ONE_DEC_UNIT);
|
||||
publish_1_reg_sensor_state(this->inverter_module_temp_, RTU2_INVERTER_MODULE_TEMP, ONE_DEC_UNIT);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,6 +16,55 @@ enum GrowattProtocolVersion {
|
||||
RTU2,
|
||||
};
|
||||
|
||||
// Register addresses for the RTU protocol.
|
||||
constexpr size_t RTU_INVERTER_STATUS = 0; // length = 1
|
||||
constexpr size_t RTU_PV_ACTIVE_POWER = 1; // length = 2
|
||||
constexpr size_t RTU_PV1_VOLTAGE = 3; // length = 1
|
||||
constexpr size_t RTU_PV1_CURRENT = 4; // length = 1
|
||||
constexpr size_t RTU_PV1_ACTIVE_POWER = 5; // length = 2
|
||||
constexpr size_t RTU_PV2_VOLTAGE = 7; // length = 1
|
||||
constexpr size_t RTU_PV2_CURRENT = 8; // length = 1
|
||||
constexpr size_t RTU_PV2_ACTIVE_POWER = 9; // length = 2
|
||||
constexpr size_t RTU_GRID_ACTIVE_POWER = 11; // length = 2
|
||||
constexpr size_t RTU_GRID_FREQUENCY = 13; // length = 1
|
||||
constexpr size_t RTU_PHASE1_VOLTAGE = 14; // length = 1
|
||||
constexpr size_t RTU_PHASE1_CURRENT = 15; // length = 1
|
||||
constexpr size_t RTU_PHASE1_ACTIVE_POWER = 16; // length = 2
|
||||
constexpr size_t RTU_PHASE2_VOLTAGE = 18; // length = 1
|
||||
constexpr size_t RTU_PHASE2_CURRENT = 19; // length = 1
|
||||
constexpr size_t RTU_PHASE2_ACTIVE_POWER = 20; // length = 2
|
||||
constexpr size_t RTU_PHASE3_VOLTAGE = 22; // length = 1
|
||||
constexpr size_t RTU_PHASE3_CURRENT = 23; // length = 1
|
||||
constexpr size_t RTU_PHASE3_ACTIVE_POWER = 24; // length = 2
|
||||
constexpr size_t RTU_TODAY_PRODUCTION = 26; // length = 2
|
||||
constexpr size_t RTU_TOTAL_ENERGY_PRODUCTION = 28; // length = 2
|
||||
constexpr size_t RTU_INVERTER_MODULE_TEMP = 32; // length = 1
|
||||
|
||||
// Input register addresses for the RTU2 protocol as described
|
||||
// in the "GROWATT INVERTER MODBUS PROTOCOL_II V1.39" document.
|
||||
constexpr size_t RTU2_INVERTER_STATUS = 0; // length = 1
|
||||
constexpr size_t RTU2_PV_ACTIVE_POWER = 1; // length = 2
|
||||
constexpr size_t RTU2_PV1_VOLTAGE = 3; // length = 1
|
||||
constexpr size_t RTU2_PV1_CURRENT = 4; // length = 1
|
||||
constexpr size_t RTU2_PV1_ACTIVE_POWER = 5; // length = 2
|
||||
constexpr size_t RTU2_PV2_VOLTAGE = 7; // length = 1
|
||||
constexpr size_t RTU2_PV2_CURRENT = 8; // length = 1
|
||||
constexpr size_t RTU2_PV2_ACTIVE_POWER = 9; // length = 2
|
||||
constexpr size_t RTU2_GRID_ACTIVE_POWER = 35; // length = 2
|
||||
constexpr size_t RTU2_GRID_FREQUENCY = 37; // length = 1
|
||||
constexpr size_t RTU2_PHASE1_VOLTAGE = 38; // length = 1
|
||||
constexpr size_t RTU2_PHASE1_CURRENT = 39; // length = 1
|
||||
constexpr size_t RTU2_PHASE1_ACTIVE_POWER = 40; // length = 2
|
||||
constexpr size_t RTU2_PHASE2_VOLTAGE = 42; // length = 1
|
||||
constexpr size_t RTU2_PHASE2_CURRENT = 43; // length = 1
|
||||
constexpr size_t RTU2_PHASE2_ACTIVE_POWER = 44; // length = 2
|
||||
constexpr size_t RTU2_PHASE3_VOLTAGE = 46; // length = 1
|
||||
constexpr size_t RTU2_PHASE3_CURRENT = 47; // length = 1
|
||||
constexpr size_t RTU2_PHASE3_ACTIVE_POWER = 48; // length = 2
|
||||
constexpr size_t RTU2_TODAY_PRODUCTION = 53; // length = 2
|
||||
constexpr size_t RTU2_TOTAL_ENERGY_PRODUCTION = 55; // length = 2
|
||||
constexpr size_t RTU2_INVERTER_MODULE_TEMP = 93; // length = 1
|
||||
|
||||
class GrowattSolar : public PollingComponent, public modbus::ModbusDevice {
|
||||
public:
|
||||
void loop() override;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import esphome.codegen as cg
|
||||
from esphome.components import time as time_
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import CONF_ID
|
||||
from esphome.const import CONF_ID, CONF_TIMEZONE
|
||||
|
||||
from .. import homeassistant_ns
|
||||
|
||||
@@ -21,3 +21,5 @@ async def to_code(config):
|
||||
await time_.register_time(var, config)
|
||||
await cg.register_component(var, config)
|
||||
cg.add_define("USE_HOMEASSISTANT_TIME")
|
||||
if CONF_TIMEZONE not in config:
|
||||
cg.add_define("USE_HOMEASSISTANT_TIMEZONE")
|
||||
|
||||
@@ -14,7 +14,7 @@ from esphome.core import CORE
|
||||
from .const import KEY_HOST
|
||||
|
||||
# force import gpio to register pin schema
|
||||
from .gpio import host_pin_to_code # noqa
|
||||
from .gpio import host_pin_to_code # noqa: F401
|
||||
|
||||
CODEOWNERS = ["@esphome/core", "@clydebarrow"]
|
||||
AUTO_LOAD = ["network", "preferences"]
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
from pathlib import Path
|
||||
|
||||
from esphome import automation
|
||||
import esphome.codegen as cg
|
||||
from esphome.components import esp32
|
||||
@@ -63,7 +65,7 @@ CONF_JSON = "json"
|
||||
|
||||
def validate_url(value):
|
||||
value = cv.url(value)
|
||||
if value.startswith("http://") or value.startswith("https://"):
|
||||
if value.startswith(("http://", "https://")):
|
||||
return value
|
||||
raise cv.Invalid("URL must start with 'http://' or 'https://'")
|
||||
|
||||
@@ -174,7 +176,7 @@ async def to_code(config):
|
||||
|
||||
if config.get(CONF_VERIFY_SSL):
|
||||
if ca_cert_path := config.get(CONF_CA_CERTIFICATE_PATH):
|
||||
with open(ca_cert_path, encoding="utf-8") as f:
|
||||
with Path(ca_cert_path).open(encoding="utf-8") as f:
|
||||
ca_cert_content = f.read()
|
||||
cg.add(var.set_ca_certificate(ca_cert_content))
|
||||
else:
|
||||
|
||||
@@ -57,7 +57,7 @@ OTA_HTTP_REQUEST_FLASH_ACTION_SCHEMA = cv.All(
|
||||
cv.Optional(CONF_MD5): cv.templatable(
|
||||
cv.All(cv.string, cv.Length(min=32, max=32))
|
||||
),
|
||||
cv.Optional(CONF_PASSWORD): cv.templatable(cv.string),
|
||||
cv.Optional(CONF_PASSWORD): cv.sensitive(cv.templatable(cv.string)),
|
||||
cv.Optional(CONF_USERNAME): cv.templatable(cv.string),
|
||||
cv.Required(CONF_URL): cv.templatable(cv.url),
|
||||
}
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
import logging
|
||||
import re
|
||||
import sys
|
||||
|
||||
from esphome import pins
|
||||
import esphome.codegen as cg
|
||||
@@ -29,6 +31,7 @@ from esphome.config_helpers import filter_source_files_from_platform
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import (
|
||||
CONF_ADDRESS,
|
||||
CONF_DEVICE,
|
||||
CONF_FREQUENCY,
|
||||
CONF_I2C,
|
||||
CONF_I2C_ID,
|
||||
@@ -40,6 +43,7 @@ from esphome.const import (
|
||||
CONF_TIMEOUT,
|
||||
PLATFORM_ESP32,
|
||||
PLATFORM_ESP8266,
|
||||
PLATFORM_HOST,
|
||||
PLATFORM_NRF52,
|
||||
PLATFORM_RP2040,
|
||||
PlatformFramework,
|
||||
@@ -56,6 +60,7 @@ InternalI2CBus = i2c_ns.class_("InternalI2CBus", I2CBus)
|
||||
ArduinoI2CBus = i2c_ns.class_("ArduinoI2CBus", InternalI2CBus, cg.Component)
|
||||
IDFI2CBus = i2c_ns.class_("IDFI2CBus", InternalI2CBus, cg.Component)
|
||||
ZephyrI2CBus = i2c_ns.class_("ZephyrI2CBus", I2CBus, cg.Component)
|
||||
HostI2CBus = i2c_ns.class_("HostI2CBus", I2CBus, cg.Component)
|
||||
I2CDevice = i2c_ns.class_("I2CDevice")
|
||||
|
||||
ESP32_I2C_CAPABILITIES = {
|
||||
@@ -83,6 +88,12 @@ CONF_SCL_PULLUP_ENABLED = "scl_pullup_enabled"
|
||||
MULTI_CONF = True
|
||||
|
||||
|
||||
def validate_device(value):
|
||||
if not re.match(r"^/(?:[^/]+/)*[^/]+$", value):
|
||||
raise cv.Invalid("Device must be an absolute device path (e.g., /dev/i2c-0)")
|
||||
return value
|
||||
|
||||
|
||||
def _bus_declare_type(value):
|
||||
if CORE.is_esp32:
|
||||
return cv.declare_id(IDFI2CBus)(value)
|
||||
@@ -90,6 +101,8 @@ def _bus_declare_type(value):
|
||||
return cv.declare_id(ArduinoI2CBus)(value)
|
||||
if CORE.using_zephyr:
|
||||
return cv.declare_id(ZephyrI2CBus)(value)
|
||||
if CORE.is_host:
|
||||
return cv.declare_id(HostI2CBus)(value)
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
@@ -121,15 +134,48 @@ def validate_config(config):
|
||||
return config
|
||||
|
||||
|
||||
def validate_host_config(config):
|
||||
if CORE.is_host:
|
||||
# Host I2C is currently only supported on Linux
|
||||
if not sys.platform.lower().startswith("linux"):
|
||||
raise cv.Invalid(
|
||||
"I2C is only supported on Linux for the host platform. "
|
||||
f"Current platform: {sys.platform}"
|
||||
)
|
||||
if CONF_SDA in config or CONF_SCL in config:
|
||||
raise cv.Invalid(
|
||||
"'sda' and 'scl' are not supported on host platform; use 'device' instead."
|
||||
)
|
||||
if CONF_SDA_PULLUP_ENABLED in config or CONF_SCL_PULLUP_ENABLED in config:
|
||||
raise cv.Invalid("Pull-up configuration is not supported on host platform.")
|
||||
if CONF_DEVICE not in config:
|
||||
raise cv.Invalid(
|
||||
"'device' is required for host platform (e.g., /dev/i2c-0)."
|
||||
)
|
||||
return config
|
||||
|
||||
|
||||
CONFIG_SCHEMA = cv.All(
|
||||
cv.Schema(
|
||||
{
|
||||
cv.GenerateID(): _bus_declare_type,
|
||||
cv.Optional(CONF_SDA, default="SDA"): pins.internal_gpio_pin_number,
|
||||
cv.SplitDefault(
|
||||
CONF_SDA,
|
||||
esp32="SDA",
|
||||
esp8266="SDA",
|
||||
rp2040="SDA",
|
||||
nrf52="SDA",
|
||||
): pins.internal_gpio_pin_number,
|
||||
cv.SplitDefault(CONF_SDA_PULLUP_ENABLED, esp32=True): cv.All(
|
||||
cv.only_on_esp32, cv.boolean
|
||||
),
|
||||
cv.Optional(CONF_SCL, default="SCL"): pins.internal_gpio_pin_number,
|
||||
cv.SplitDefault(
|
||||
CONF_SCL,
|
||||
esp32="SCL",
|
||||
esp8266="SCL",
|
||||
rp2040="SCL",
|
||||
nrf52="SCL",
|
||||
): pins.internal_gpio_pin_number,
|
||||
cv.SplitDefault(CONF_SCL_PULLUP_ENABLED, esp32=True): cv.All(
|
||||
cv.only_on_esp32, cv.boolean
|
||||
),
|
||||
@@ -139,6 +185,7 @@ CONFIG_SCHEMA = cv.All(
|
||||
esp8266="50kHz",
|
||||
rp2040="50kHz",
|
||||
nrf52="100kHz",
|
||||
host="50kHz",
|
||||
): cv.All(
|
||||
cv.frequency,
|
||||
cv.float_range(min=0, min_included=False),
|
||||
@@ -155,10 +202,22 @@ CONFIG_SCHEMA = cv.All(
|
||||
),
|
||||
cv.boolean,
|
||||
),
|
||||
cv.Optional(CONF_DEVICE): cv.All(
|
||||
cv.only_on(PLATFORM_HOST), validate_device
|
||||
),
|
||||
}
|
||||
).extend(cv.COMPONENT_SCHEMA),
|
||||
cv.only_on([PLATFORM_ESP32, PLATFORM_ESP8266, PLATFORM_RP2040, PLATFORM_NRF52]),
|
||||
cv.only_on(
|
||||
[
|
||||
PLATFORM_ESP32,
|
||||
PLATFORM_ESP8266,
|
||||
PLATFORM_RP2040,
|
||||
PLATFORM_NRF52,
|
||||
PLATFORM_HOST,
|
||||
]
|
||||
),
|
||||
validate_config,
|
||||
validate_host_config,
|
||||
)
|
||||
|
||||
|
||||
@@ -217,7 +276,13 @@ FINAL_VALIDATE_SCHEMA = _final_validate
|
||||
async def to_code(config):
|
||||
cg.add_global(i2c_ns.using)
|
||||
cg.add_define("USE_I2C")
|
||||
if CORE.using_zephyr:
|
||||
if CORE.is_host:
|
||||
var = cg.new_Pvariable(config[CONF_ID])
|
||||
await cg.register_component(var, config)
|
||||
cg.add(var.set_device(config[CONF_DEVICE]))
|
||||
cg.add(var.set_frequency(int(config[CONF_FREQUENCY])))
|
||||
cg.add(var.set_scan(config[CONF_SCAN]))
|
||||
elif CORE.using_zephyr:
|
||||
zephyr_add_prj_conf("I2C", True)
|
||||
i2c = "i2c0"
|
||||
if zephyr_data()[KEY_BOARD] == "xiao_ble":
|
||||
@@ -244,25 +309,40 @@ async def to_code(config):
|
||||
var = cg.new_Pvariable(
|
||||
config[CONF_ID], MockObj(f"DEVICE_DT_GET(DT_NODELABEL({i2c}))")
|
||||
)
|
||||
await cg.register_component(var, config)
|
||||
|
||||
cg.add(var.set_sda_pin(config[CONF_SDA]))
|
||||
if CONF_SDA_PULLUP_ENABLED in config:
|
||||
cg.add(var.set_sda_pullup_enabled(config[CONF_SDA_PULLUP_ENABLED]))
|
||||
cg.add(var.set_scl_pin(config[CONF_SCL]))
|
||||
if CONF_SCL_PULLUP_ENABLED in config:
|
||||
cg.add(var.set_scl_pullup_enabled(config[CONF_SCL_PULLUP_ENABLED]))
|
||||
|
||||
cg.add(var.set_frequency(int(config[CONF_FREQUENCY])))
|
||||
cg.add(var.set_scan(config[CONF_SCAN]))
|
||||
if CONF_TIMEOUT in config:
|
||||
cg.add(var.set_timeout(int(config[CONF_TIMEOUT].total_microseconds)))
|
||||
if CONF_LOW_POWER_MODE in config:
|
||||
cg.add(var.set_lp_mode(bool(config[CONF_LOW_POWER_MODE])))
|
||||
else:
|
||||
var = cg.new_Pvariable(config[CONF_ID])
|
||||
await cg.register_component(var, config)
|
||||
await cg.register_component(var, config)
|
||||
|
||||
cg.add(var.set_sda_pin(config[CONF_SDA]))
|
||||
if CONF_SDA_PULLUP_ENABLED in config:
|
||||
cg.add(var.set_sda_pullup_enabled(config[CONF_SDA_PULLUP_ENABLED]))
|
||||
cg.add(var.set_scl_pin(config[CONF_SCL]))
|
||||
if CONF_SCL_PULLUP_ENABLED in config:
|
||||
cg.add(var.set_scl_pullup_enabled(config[CONF_SCL_PULLUP_ENABLED]))
|
||||
cg.add(var.set_sda_pin(config[CONF_SDA]))
|
||||
if CONF_SDA_PULLUP_ENABLED in config:
|
||||
cg.add(var.set_sda_pullup_enabled(config[CONF_SDA_PULLUP_ENABLED]))
|
||||
cg.add(var.set_scl_pin(config[CONF_SCL]))
|
||||
if CONF_SCL_PULLUP_ENABLED in config:
|
||||
cg.add(var.set_scl_pullup_enabled(config[CONF_SCL_PULLUP_ENABLED]))
|
||||
|
||||
cg.add(var.set_frequency(int(config[CONF_FREQUENCY])))
|
||||
cg.add(var.set_scan(config[CONF_SCAN]))
|
||||
if CONF_TIMEOUT in config:
|
||||
cg.add(var.set_timeout(int(config[CONF_TIMEOUT].total_microseconds)))
|
||||
if CORE.using_arduino and not CORE.is_esp32:
|
||||
cg.add_library("Wire", None)
|
||||
if CONF_LOW_POWER_MODE in config:
|
||||
cg.add(var.set_lp_mode(bool(config[CONF_LOW_POWER_MODE])))
|
||||
cg.add(var.set_frequency(int(config[CONF_FREQUENCY])))
|
||||
cg.add(var.set_scan(config[CONF_SCAN]))
|
||||
if CONF_TIMEOUT in config:
|
||||
cg.add(var.set_timeout(int(config[CONF_TIMEOUT].total_microseconds)))
|
||||
if CORE.using_arduino and not CORE.is_esp32:
|
||||
cg.add_library("Wire", None)
|
||||
if CONF_LOW_POWER_MODE in config:
|
||||
cg.add(var.set_lp_mode(bool(config[CONF_LOW_POWER_MODE])))
|
||||
|
||||
|
||||
def i2c_device_schema(default_address):
|
||||
@@ -365,5 +445,6 @@ FILTER_SOURCE_FILES = filter_source_files_from_platform(
|
||||
PlatformFramework.ESP32_IDF,
|
||||
},
|
||||
"i2c_bus_zephyr.cpp": {PlatformFramework.NRF52_ZEPHYR},
|
||||
"i2c_bus_host.cpp": {PlatformFramework.HOST_NATIVE},
|
||||
}
|
||||
)
|
||||
|
||||
297
esphome/components/i2c/i2c_bus_host.cpp
Normal file
297
esphome/components/i2c/i2c_bus_host.cpp
Normal file
@@ -0,0 +1,297 @@
|
||||
#ifdef USE_HOST
|
||||
#if defined(__linux__)
|
||||
|
||||
#include "i2c_bus_host.h"
|
||||
#include "esphome/core/helpers.h"
|
||||
#include "esphome/core/log.h"
|
||||
|
||||
#include <fcntl.h>
|
||||
#include <linux/i2c-dev.h>
|
||||
#include <linux/i2c.h>
|
||||
#include <sys/ioctl.h>
|
||||
#include <unistd.h>
|
||||
#include <cerrno>
|
||||
#include <cstdint>
|
||||
#include <cstring>
|
||||
|
||||
namespace esphome::i2c {
|
||||
|
||||
static const char *const TAG = "i2c.host";
|
||||
|
||||
HostI2CBus::~HostI2CBus() {
|
||||
if (this->file_descriptor_ != -1) {
|
||||
close(this->file_descriptor_);
|
||||
this->file_descriptor_ = -1;
|
||||
}
|
||||
}
|
||||
|
||||
void HostI2CBus::setup() {
|
||||
ESP_LOGCONFIG(TAG, "Setting up I2C bus...");
|
||||
|
||||
// Open I2C device file
|
||||
this->file_descriptor_ = open(this->device_.c_str(), O_RDWR);
|
||||
if (this->file_descriptor_ == -1) {
|
||||
int err = errno;
|
||||
if (err == ENOENT) {
|
||||
this->update_error_("not found");
|
||||
} else if (err == EACCES) {
|
||||
this->update_error_("permission denied");
|
||||
} else {
|
||||
this->update_error_(std::string("failed to open: ") + strerror(err));
|
||||
}
|
||||
this->mark_failed();
|
||||
return;
|
||||
}
|
||||
|
||||
this->initialized_ = true;
|
||||
ESP_LOGCONFIG(TAG, " Device: %s", this->device_.c_str());
|
||||
|
||||
// Run bus scan if enabled
|
||||
if (this->scan_) {
|
||||
this->i2c_scan_();
|
||||
}
|
||||
}
|
||||
|
||||
void HostI2CBus::dump_config() {
|
||||
ESP_LOGCONFIG(TAG, "I2C Bus:");
|
||||
ESP_LOGCONFIG(TAG, " Device: %s", this->device_.c_str());
|
||||
// Bus frequency cannot be set from userspace via i2c-dev; report it as informational only
|
||||
ESP_LOGCONFIG(TAG, " Frequency: %u Hz (informational; not applied on host)", this->frequency_);
|
||||
|
||||
if (!this->first_error_.empty()) {
|
||||
ESP_LOGE(TAG, " Setup Error: %s", this->first_error_.c_str());
|
||||
}
|
||||
|
||||
if (this->scan_) {
|
||||
ESP_LOGI(TAG, " Scan Results:");
|
||||
for (const auto &s : this->scan_results_) {
|
||||
if (s.second) {
|
||||
ESP_LOGI(TAG, " 0x%02X: Found", s.first);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ErrorCode HostI2CBus::write_readv(uint8_t address, const uint8_t *write_buffer, size_t write_count,
|
||||
uint8_t *read_buffer, size_t read_count) {
|
||||
if (!this->initialized_) {
|
||||
ESP_LOGE(TAG, "I2C bus not initialized");
|
||||
return ERROR_NOT_INITIALIZED;
|
||||
}
|
||||
|
||||
ESP_LOGVV(TAG, "I2C write_readv addr=0x%02X write=%zu read=%zu", address, write_count, read_count);
|
||||
|
||||
// Handle special case: probe (no write data, no read data)
|
||||
// This is used for device detection during bus scanning
|
||||
if (write_count == 0 && read_count == 0) {
|
||||
struct i2c_msg msg;
|
||||
msg.addr = address;
|
||||
msg.flags = 0;
|
||||
msg.len = 0;
|
||||
msg.buf = nullptr;
|
||||
|
||||
struct i2c_rdwr_ioctl_data rdwr_data;
|
||||
rdwr_data.msgs = &msg;
|
||||
rdwr_data.nmsgs = 1;
|
||||
|
||||
int ret = ioctl(this->file_descriptor_, I2C_RDWR, &rdwr_data);
|
||||
if (ret < 0) {
|
||||
int err = errno;
|
||||
// If I2C_RDWR not supported, try SMBus Quick command (what i2cdetect uses)
|
||||
if (err == EOPNOTSUPP || err == ENOSYS) {
|
||||
ESP_LOGVV(TAG, "I2C_RDWR probe failed, trying SMBus Quick for addr=0x%02X", address);
|
||||
if (ioctl(this->file_descriptor_, I2C_SLAVE, address) < 0) { // NOLINT
|
||||
return this->map_errno_to_error_code_(errno);
|
||||
}
|
||||
// Use I2C_SMBUS ioctl with Quick command
|
||||
union i2c_smbus_data data;
|
||||
struct i2c_smbus_ioctl_data args;
|
||||
args.read_write = I2C_SMBUS_WRITE;
|
||||
args.command = 0;
|
||||
args.size = I2C_SMBUS_QUICK;
|
||||
args.data = &data;
|
||||
ret = ioctl(this->file_descriptor_, I2C_SMBUS, &args);
|
||||
if (ret < 0) {
|
||||
return this->map_errno_to_error_code_(errno);
|
||||
}
|
||||
return ERROR_OK;
|
||||
}
|
||||
return this->map_errno_to_error_code_(err);
|
||||
}
|
||||
return ERROR_OK;
|
||||
}
|
||||
|
||||
// i2c_msg.len is a 16-bit field; reject transfers that would silently truncate
|
||||
if (write_count > UINT16_MAX || read_count > UINT16_MAX) {
|
||||
ESP_LOGE(TAG, "I2C transfer too large: write=%zu read=%zu (max %u)", write_count, read_count,
|
||||
(unsigned) UINT16_MAX);
|
||||
return ERROR_TOO_LARGE;
|
||||
}
|
||||
|
||||
// Prepare messages for combined write-read transaction
|
||||
struct i2c_msg msgs[2];
|
||||
int num_msgs = 0;
|
||||
|
||||
// Add write message if write data present
|
||||
if (write_count > 0) {
|
||||
msgs[num_msgs].addr = address;
|
||||
msgs[num_msgs].flags = 0; // Write
|
||||
msgs[num_msgs].len = write_count;
|
||||
msgs[num_msgs].buf = const_cast<uint8_t *>(write_buffer);
|
||||
num_msgs++;
|
||||
}
|
||||
|
||||
// Add read message if read data requested
|
||||
if (read_count > 0) {
|
||||
msgs[num_msgs].addr = address;
|
||||
msgs[num_msgs].flags = I2C_M_RD; // Read
|
||||
msgs[num_msgs].len = read_count;
|
||||
msgs[num_msgs].buf = read_buffer;
|
||||
num_msgs++;
|
||||
}
|
||||
|
||||
// Execute I2C transaction
|
||||
struct i2c_rdwr_ioctl_data rdwr_data;
|
||||
rdwr_data.msgs = msgs;
|
||||
rdwr_data.nmsgs = num_msgs;
|
||||
|
||||
int ret = ioctl(this->file_descriptor_, I2C_RDWR, &rdwr_data);
|
||||
if (ret < 0) {
|
||||
int err = errno;
|
||||
if (err == EOPNOTSUPP || err == ENOSYS) {
|
||||
ESP_LOGV(TAG, "I2C_RDWR not supported, using I2C_SLAVE fallback for addr=0x%02X", address); // NOLINT
|
||||
if (ioctl(this->file_descriptor_, I2C_SLAVE, address) < 0) { // NOLINT
|
||||
ESP_LOGV(TAG, "I2C_SLAVE ioctl failed: %s", strerror(errno)); // NOLINT
|
||||
return this->map_errno_to_error_code_(errno);
|
||||
}
|
||||
// Perform write if needed
|
||||
if (write_count > 0) {
|
||||
ssize_t written = ::write(this->file_descriptor_, write_buffer, write_count);
|
||||
if (written != (ssize_t) write_count) {
|
||||
int write_err = errno;
|
||||
// If write() also fails with EOPNOTSUPP, try I2C_SMBUS as last resort
|
||||
if (write_err == EOPNOTSUPP || write_err == ENOSYS) {
|
||||
ESP_LOGV(TAG, "I2C_SLAVE write not supported, trying I2C_SMBUS for addr=0x%02X", address); // NOLINT
|
||||
// Use I2C_SMBUS_I2C_BLOCK_DATA for writes up to 32 bytes
|
||||
// Standard SMBus mapping: first byte is command, remaining bytes are data
|
||||
if (write_count < 1) {
|
||||
ESP_LOGE(TAG, "Write size too small for I2C_SMBUS");
|
||||
return ERROR_INVALID_ARGUMENT;
|
||||
}
|
||||
if (write_count > I2C_SMBUS_BLOCK_MAX + 1) {
|
||||
ESP_LOGE(TAG, "Write size %zu exceeds I2C_SMBUS_BLOCK_MAX+1 (%d)", write_count, I2C_SMBUS_BLOCK_MAX + 1);
|
||||
return ERROR_INVALID_ARGUMENT;
|
||||
}
|
||||
union i2c_smbus_data data;
|
||||
// Standard SMBus: first byte = command, rest = data
|
||||
uint8_t command = write_buffer[0];
|
||||
size_t data_len = write_count - 1;
|
||||
data.block[0] = data_len;
|
||||
if (data_len > 0) {
|
||||
memcpy(&data.block[1], write_buffer + 1, data_len);
|
||||
}
|
||||
|
||||
struct i2c_smbus_ioctl_data args;
|
||||
args.read_write = I2C_SMBUS_WRITE;
|
||||
args.command = command;
|
||||
args.size = I2C_SMBUS_I2C_BLOCK_DATA;
|
||||
args.data = &data;
|
||||
|
||||
ret = ioctl(this->file_descriptor_, I2C_SMBUS, &args);
|
||||
if (ret < 0) {
|
||||
ESP_LOGV(TAG, "I2C_SMBUS write failed: %s", strerror(errno));
|
||||
return this->map_errno_to_error_code_(errno);
|
||||
}
|
||||
} else {
|
||||
ESP_LOGV(TAG, "I2C write failed: %s", strerror(write_err));
|
||||
return this->map_errno_to_error_code_(write_err);
|
||||
}
|
||||
}
|
||||
}
|
||||
// Perform read if needed
|
||||
if (read_count > 0) {
|
||||
ssize_t bytes_read = ::read(this->file_descriptor_, read_buffer, read_count);
|
||||
if (bytes_read != (ssize_t) read_count) {
|
||||
int read_err = errno;
|
||||
// If read() also fails with EOPNOTSUPP, try I2C_SMBUS as last resort
|
||||
if (read_err == EOPNOTSUPP || read_err == ENOSYS) {
|
||||
ESP_LOGV(TAG, "I2C_SLAVE read not supported, trying I2C_SMBUS for addr=0x%02X", address); // NOLINT
|
||||
// Use I2C_SMBUS_I2C_BLOCK_DATA for reads up to 32 bytes
|
||||
if (read_count > I2C_SMBUS_BLOCK_MAX) {
|
||||
ESP_LOGE(TAG, "Read size %zu exceeds I2C_SMBUS_BLOCK_MAX (%d)", read_count, I2C_SMBUS_BLOCK_MAX);
|
||||
return ERROR_INVALID_ARGUMENT;
|
||||
}
|
||||
union i2c_smbus_data data;
|
||||
data.block[0] = read_count;
|
||||
|
||||
struct i2c_smbus_ioctl_data args;
|
||||
args.read_write = I2C_SMBUS_READ;
|
||||
args.command = 0; // Start register/command
|
||||
args.size = I2C_SMBUS_I2C_BLOCK_DATA;
|
||||
args.data = &data;
|
||||
|
||||
ret = ioctl(this->file_descriptor_, I2C_SMBUS, &args);
|
||||
if (ret < 0) {
|
||||
ESP_LOGV(TAG, "I2C_SMBUS read failed: %s", strerror(errno));
|
||||
return this->map_errno_to_error_code_(errno);
|
||||
}
|
||||
// I2C_SMBUS_I2C_BLOCK_DATA returns the actual byte count in block[0];
|
||||
// a short read means we did not receive all requested bytes
|
||||
if (data.block[0] < read_count) {
|
||||
ESP_LOGV(TAG, "I2C_SMBUS short read: got %u, expected %zu", data.block[0], read_count);
|
||||
return ERROR_NOT_ACKNOWLEDGED;
|
||||
}
|
||||
// Copy data from SMBus buffer to output buffer
|
||||
memcpy(read_buffer, &data.block[1], read_count);
|
||||
} else {
|
||||
ESP_LOGV(TAG, "I2C read failed: %s", strerror(read_err));
|
||||
return this->map_errno_to_error_code_(read_err);
|
||||
}
|
||||
}
|
||||
}
|
||||
ESP_LOGVV(TAG, "I2C transaction successful (I2C_SLAVE method)"); // NOLINT
|
||||
return ERROR_OK;
|
||||
}
|
||||
ESP_LOGV(TAG, "I2C transaction failed: %s", strerror(err));
|
||||
return this->map_errno_to_error_code_(err);
|
||||
}
|
||||
|
||||
ESP_LOGVV(TAG, "I2C transaction successful");
|
||||
return ERROR_OK;
|
||||
}
|
||||
|
||||
ErrorCode HostI2CBus::map_errno_to_error_code_(int err) {
|
||||
switch (err) {
|
||||
case ENXIO:
|
||||
return ERROR_NOT_ACKNOWLEDGED;
|
||||
case ETIMEDOUT:
|
||||
return ERROR_TIMEOUT;
|
||||
case EINVAL:
|
||||
return ERROR_INVALID_ARGUMENT;
|
||||
case ENODEV:
|
||||
case ENOTTY:
|
||||
return ERROR_NOT_INITIALIZED;
|
||||
case EOPNOTSUPP:
|
||||
case ENOSYS:
|
||||
// Operation not supported - some I2C adapters don't support zero-length transactions
|
||||
ESP_LOGVV(TAG, "I2C adapter does not support this operation (likely zero-length probe)");
|
||||
return ERROR_NOT_ACKNOWLEDGED;
|
||||
default:
|
||||
ESP_LOGV(TAG, "Unmapped error code: %d (%s)", err, strerror(err));
|
||||
return ERROR_UNKNOWN;
|
||||
}
|
||||
}
|
||||
|
||||
void HostI2CBus::update_error_(const std::string &error) {
|
||||
if (this->first_error_.empty()) {
|
||||
this->first_error_ = error;
|
||||
}
|
||||
ESP_LOGE(TAG, "[%s] %s", this->device_.c_str(), error.c_str());
|
||||
}
|
||||
|
||||
} // namespace esphome::i2c
|
||||
|
||||
#else
|
||||
#error "HostI2CBus is only supported on Linux"
|
||||
#endif // defined(__linux__)
|
||||
#endif // USE_HOST
|
||||
41
esphome/components/i2c/i2c_bus_host.h
Normal file
41
esphome/components/i2c/i2c_bus_host.h
Normal file
@@ -0,0 +1,41 @@
|
||||
#pragma once
|
||||
|
||||
#ifdef USE_HOST
|
||||
|
||||
#include "esphome/core/component.h"
|
||||
#include "esphome/core/log.h"
|
||||
#include "i2c_bus.h"
|
||||
|
||||
namespace esphome::i2c {
|
||||
|
||||
class HostI2CBus : public I2CBus, public Component {
|
||||
public:
|
||||
~HostI2CBus() override;
|
||||
|
||||
void setup() override;
|
||||
void dump_config() override;
|
||||
float get_setup_priority() const override { return setup_priority::BUS; }
|
||||
|
||||
ErrorCode write_readv(uint8_t address, const uint8_t *write_buffer, size_t write_count, uint8_t *read_buffer,
|
||||
size_t read_count) override;
|
||||
|
||||
void set_device(const std::string &device) { this->device_ = device; }
|
||||
void set_scan(bool scan) { this->scan_ = scan; }
|
||||
void set_frequency(uint32_t frequency) { this->frequency_ = frequency; }
|
||||
|
||||
const std::string &get_device() const { return this->device_; }
|
||||
|
||||
protected:
|
||||
void update_error_(const std::string &error);
|
||||
ErrorCode map_errno_to_error_code_(int err);
|
||||
|
||||
std::string device_;
|
||||
uint32_t frequency_{50000};
|
||||
int file_descriptor_{-1};
|
||||
bool initialized_{false};
|
||||
std::string first_error_;
|
||||
};
|
||||
|
||||
} // namespace esphome::i2c
|
||||
|
||||
#endif // USE_HOST
|
||||
@@ -170,7 +170,7 @@ def i2s_audio_component_schema(
|
||||
min=1
|
||||
),
|
||||
cv.Optional(CONF_BITS_PER_SAMPLE, default=default_bits_per_sample): cv.All(
|
||||
_validate_bits, cv.one_of(*I2S_BITS_PER_SAMPLE)
|
||||
_validate_bits, cv.int_, cv.one_of(*I2S_BITS_PER_SAMPLE)
|
||||
),
|
||||
cv.Optional(CONF_I2S_MODE, default=CONF_PRIMARY): cv.one_of(
|
||||
*I2S_MODE_OPTIONS, lower=True
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user