mirror of
https://github.com/esphome/esphome.git
synced 2026-06-24 11:25:35 +00:00
Merge branch 'dev' into sendspin-artwork
This commit is contained in:
@@ -1 +1 @@
|
|||||||
27aaab4e0ebfc10491720345aa746fc2dffa6a3985f73ec111b12dd99078d46f
|
a6ec18b82143e293ca6dee6947217f10a387ace99881a34b2c308ff627c8173c
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ Required fields:
|
|||||||
- **What does this implement/fix?**: Brief description of changes
|
- **What does this implement/fix?**: Brief description of changes
|
||||||
- **Types of changes**: Check ONE appropriate box (Bugfix, New feature, Breaking change, etc.)
|
- **Types of changes**: Check ONE appropriate box (Bugfix, New feature, Breaking change, etc.)
|
||||||
- **Related issue**: Use `fixes <link>` syntax if applicable
|
- **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
|
- **Test Environment**: Check platforms you tested on
|
||||||
- **Example config.yaml**: Include working example YAML
|
- **Example config.yaml**: Include working example YAML
|
||||||
- **Checklist**: Verify code is tested and tests added
|
- **Checklist**: Verify code is tested and tests added
|
||||||
@@ -54,9 +54,9 @@ Required fields:
|
|||||||
|
|
||||||
- fixes https://github.com/esphome/esphome/issues/XXX
|
- 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
|
## Test Environment
|
||||||
|
|
||||||
@@ -83,7 +83,7 @@ component_name:
|
|||||||
- [x] Tests have been added to verify that the new code works (under `tests/` folder).
|
- [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:
|
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
|
## 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
|
blank_issues_enabled: false
|
||||||
contact_links:
|
contact_links:
|
||||||
- name: Report an issue with the ESPHome documentation
|
- 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.
|
about: Report an issue with the ESPHome documentation.
|
||||||
- name: Report an issue with the ESPHome web server
|
- name: Report an issue with the ESPHome web server
|
||||||
url: https://github.com/esphome/esphome-webserver/issues/new/choose
|
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>
|
- 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
|
## Test Environment
|
||||||
|
|
||||||
@@ -43,4 +43,4 @@
|
|||||||
- [ ] Tests have been added to verify that the new code works (under `tests/` folder).
|
- [ ] Tests have been added to verify that the new code works (under `tests/` folder).
|
||||||
|
|
||||||
If user exposed functionality or configuration variables are added/changed:
|
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).
|
||||||
|
|||||||
46
.github/actions/cache-esp-idf/action.yml
vendored
Normal file
46
.github/actions/cache-esp-idf/action.yml
vendored
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
name: Cache ESP-IDF
|
||||||
|
description: >
|
||||||
|
Resolve the pinned ESP-IDF version and cache the native ESP-IDF install
|
||||||
|
(toolchains + source) at ~/.esphome-idf. Every job that installs ESP-IDF
|
||||||
|
natively (clang-tidy for IDF/Arduino and the native-IDF component build)
|
||||||
|
shares one cache, since the install is identical (ESPHOME_IDF_DEFAULT_TARGETS
|
||||||
|
defaults to "all", so all toolchains are present regardless of the chip).
|
||||||
|
Callers must set env ESPHOME_ESP_IDF_PREFIX: ~/.esphome-idf and have the
|
||||||
|
Python venv already restored.
|
||||||
|
inputs:
|
||||||
|
framework:
|
||||||
|
description: 'Which pinned IDF version to key on: "espidf" (recommended) or "arduino".'
|
||||||
|
default: espidf
|
||||||
|
runs:
|
||||||
|
using: composite
|
||||||
|
steps:
|
||||||
|
- name: Resolve ESP-IDF version for cache key
|
||||||
|
# The native-IDF version is pinned in code, not in any file that feeds the
|
||||||
|
# other cache keys, so resolve it explicitly. Keying on it means the cache
|
||||||
|
# invalidates on a version bump (actions/cache never overwrites a key).
|
||||||
|
id: version
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
. venv/bin/activate
|
||||||
|
if [ "${{ inputs.framework }}" = "arduino" ]; then
|
||||||
|
version=$(python -c 'from esphome.components.esp32 import ARDUINO_FRAMEWORK_VERSION_LOOKUP as A, ARDUINO_IDF_VERSION_LOOKUP as L; print(L[A["recommended"]])')
|
||||||
|
else
|
||||||
|
version=$(python -c 'from esphome.components.esp32 import ESP_IDF_FRAMEWORK_VERSION_LOOKUP as L; print(L["recommended"])')
|
||||||
|
fi
|
||||||
|
echo "version=$version" >> "$GITHUB_OUTPUT"
|
||||||
|
# Mirror the adjacent PlatformIO cache: only dev-branch runs write the
|
||||||
|
# shared cache (so it lives in the default-branch scope readable by all
|
||||||
|
# PRs), and PRs are restore-only -- they never push multi-GB artifacts into
|
||||||
|
# their own scope / the repo quota (e.g. on a version-bump PR).
|
||||||
|
- name: Cache ESP-IDF install (write on dev)
|
||||||
|
if: github.ref == 'refs/heads/dev'
|
||||||
|
uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
|
||||||
|
with:
|
||||||
|
path: ~/.esphome-idf
|
||||||
|
key: ${{ runner.os }}-esphome-idf-${{ steps.version.outputs.version }}
|
||||||
|
- name: Cache ESP-IDF install (restore-only off dev)
|
||||||
|
if: github.ref != 'refs/heads/dev'
|
||||||
|
uses: actions/cache/restore@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
|
||||||
|
with:
|
||||||
|
path: ~/.esphome-idf
|
||||||
|
key: ${{ runner.os }}-esphome-idf-${{ steps.version.outputs.version }}
|
||||||
2
.github/actions/restore-python/action.yml
vendored
2
.github/actions/restore-python/action.yml
vendored
@@ -32,7 +32,7 @@ runs:
|
|||||||
# detects the activated venv via ``VIRTUAL_ENV`` so the venv layout
|
# detects the activated venv via ``VIRTUAL_ENV`` so the venv layout
|
||||||
# downstream jobs rely on is preserved.
|
# downstream jobs rely on is preserved.
|
||||||
if: steps.cache-venv.outputs.cache-hit != 'true'
|
if: steps.cache-venv.outputs.cache-hit != 'true'
|
||||||
uses: astral-sh/setup-uv@08807647e7069bb48b6ef5acd8ec9567f424441b # v8.1.0
|
uses: astral-sh/setup-uv@fac544c07dec837d0ccb6301d7b5580bf5edae39 # v8.2.0
|
||||||
with:
|
with:
|
||||||
enable-cache: true
|
enable-cache: true
|
||||||
# Pin uv version so the action does not have to fetch the
|
# Pin uv version so the action does not have to fetch the
|
||||||
|
|||||||
1
.github/dependabot.yml
vendored
1
.github/dependabot.yml
vendored
@@ -5,6 +5,7 @@ updates:
|
|||||||
directory: "/"
|
directory: "/"
|
||||||
schedule:
|
schedule:
|
||||||
interval: daily
|
interval: daily
|
||||||
|
open-pull-requests-limit: 10
|
||||||
ignore:
|
ignore:
|
||||||
# Hypotehsis is only used for testing and is updated quite often
|
# Hypotehsis is only used for testing and is updated quite often
|
||||||
- dependency-name: hypothesis
|
- 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: [
|
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+/,
|
/https:\/\/github\.com\/esphome\/esphome-docs\/pull\/\d+/,
|
||||||
/esphome\/esphome-docs#\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$/,
|
/^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 file of addedFiles) {
|
||||||
for (const re of platformPathPatterns) {
|
for (const re of platformPathPatterns) {
|
||||||
const match = file.match(re);
|
const match = file.match(re);
|
||||||
@@ -114,6 +116,12 @@ async function detectNewPlatforms(github, context, prFiles, apiData) {
|
|||||||
const platform = match[2];
|
const platform = match[2];
|
||||||
if (!apiData.platformComponents.includes(platform)) break;
|
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');
|
labels.add('new-platform');
|
||||||
const content = await fetchPrFileContent(github, context, file);
|
const content = await fetchPrFileContent(github, context, file);
|
||||||
if (content === null) {
|
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')
|
if: github.event.pull_request.state == 'open' && (github.event.action != 'labeled' || github.event.sender.type != 'Bot')
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||||
|
|
||||||
- name: Generate a token
|
- name: Generate a token
|
||||||
id: generate-token
|
id: generate-token
|
||||||
|
|||||||
4
.github/workflows/ci-api-proto.yml
vendored
4
.github/workflows/ci-api-proto.yml
vendored
@@ -21,7 +21,7 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||||
- name: Set up Python
|
- name: Set up Python
|
||||||
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
|
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
|
||||||
with:
|
with:
|
||||||
@@ -29,7 +29,7 @@ jobs:
|
|||||||
- name: Set up uv
|
- name: Set up uv
|
||||||
# ``--system`` (below) installs into the setup-python interpreter;
|
# ``--system`` (below) installs into the setup-python interpreter;
|
||||||
# no venv is created or restored by this workflow.
|
# no venv is created or restored by this workflow.
|
||||||
uses: astral-sh/setup-uv@08807647e7069bb48b6ef5acd8ec9567f424441b # v8.1.0
|
uses: astral-sh/setup-uv@fac544c07dec837d0ccb6301d7b5580bf5edae39 # v8.2.0
|
||||||
with:
|
with:
|
||||||
enable-cache: true
|
enable-cache: true
|
||||||
# Pin uv version so the action does not have to fetch the
|
# Pin uv version so the action does not have to fetch the
|
||||||
|
|||||||
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
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||||
|
|
||||||
- name: Set up Python
|
- name: Set up Python
|
||||||
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
|
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"
|
- "docker"
|
||||||
# - "lint"
|
# - "lint"
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||||
- name: Set up Python
|
- name: Set up Python
|
||||||
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
|
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
|
||||||
with:
|
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
|
- name: Check out code from base repository
|
||||||
if: steps.pr.outputs.skip != 'true'
|
if: steps.pr.outputs.skip != 'true'
|
||||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||||
with:
|
with:
|
||||||
# Always check out from the base repository (esphome/esphome), never from forks
|
# 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
|
# Use the PR's target branch to ensure we run trusted code from the main repo
|
||||||
|
|||||||
265
.github/workflows/ci.yml
vendored
265
.github/workflows/ci.yml
vendored
@@ -28,7 +28,7 @@ jobs:
|
|||||||
cache-key: ${{ steps.cache-key.outputs.key }}
|
cache-key: ${{ steps.cache-key.outputs.key }}
|
||||||
steps:
|
steps:
|
||||||
- name: Check out code from GitHub
|
- name: Check out code from GitHub
|
||||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||||
- name: Generate cache-key
|
- name: Generate cache-key
|
||||||
id: cache-key
|
id: cache-key
|
||||||
run: echo key="${{ hashFiles('requirements.txt', 'requirements_dev.txt', 'requirements_test.txt', '.pre-commit-config.yaml') }}" >> $GITHUB_OUTPUT
|
run: echo key="${{ hashFiles('requirements.txt', 'requirements_dev.txt', 'requirements_test.txt', '.pre-commit-config.yaml') }}" >> $GITHUB_OUTPUT
|
||||||
@@ -49,7 +49,7 @@ jobs:
|
|||||||
# detects the activated venv via ``VIRTUAL_ENV`` so downstream jobs
|
# detects the activated venv via ``VIRTUAL_ENV`` so downstream jobs
|
||||||
# that ``. venv/bin/activate`` see an identical layout.
|
# that ``. venv/bin/activate`` see an identical layout.
|
||||||
if: steps.cache-venv.outputs.cache-hit != 'true'
|
if: steps.cache-venv.outputs.cache-hit != 'true'
|
||||||
uses: astral-sh/setup-uv@08807647e7069bb48b6ef5acd8ec9567f424441b # v8.1.0
|
uses: astral-sh/setup-uv@fac544c07dec837d0ccb6301d7b5580bf5edae39 # v8.2.0
|
||||||
with:
|
with:
|
||||||
enable-cache: true
|
enable-cache: true
|
||||||
# Pin uv version so the action does not have to fetch the
|
# Pin uv version so the action does not have to fetch the
|
||||||
@@ -74,7 +74,7 @@ jobs:
|
|||||||
if: needs.determine-jobs.outputs.python-linters == 'true'
|
if: needs.determine-jobs.outputs.python-linters == 'true'
|
||||||
steps:
|
steps:
|
||||||
- name: Check out code from GitHub
|
- name: Check out code from GitHub
|
||||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||||
- name: Restore Python
|
- name: Restore Python
|
||||||
uses: ./.github/actions/restore-python
|
uses: ./.github/actions/restore-python
|
||||||
with:
|
with:
|
||||||
@@ -97,7 +97,7 @@ jobs:
|
|||||||
if: needs.determine-jobs.outputs.core-ci == 'true'
|
if: needs.determine-jobs.outputs.core-ci == 'true'
|
||||||
steps:
|
steps:
|
||||||
- name: Check out code from GitHub
|
- name: Check out code from GitHub
|
||||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||||
- name: Restore Python
|
- name: Restore Python
|
||||||
uses: ./.github/actions/restore-python
|
uses: ./.github/actions/restore-python
|
||||||
with:
|
with:
|
||||||
@@ -113,6 +113,7 @@ jobs:
|
|||||||
script/build_language_schema.py --check
|
script/build_language_schema.py --check
|
||||||
script/generate-esp32-boards.py --check
|
script/generate-esp32-boards.py --check
|
||||||
script/generate-rp2040-boards.py --check
|
script/generate-rp2040-boards.py --check
|
||||||
|
script/ci_check_duplicate_test_ids.py
|
||||||
|
|
||||||
import-time:
|
import-time:
|
||||||
name: Check import esphome.__main__ time
|
name: Check import esphome.__main__ time
|
||||||
@@ -123,7 +124,7 @@ jobs:
|
|||||||
if: needs.determine-jobs.outputs.import-time == 'true'
|
if: needs.determine-jobs.outputs.import-time == 'true'
|
||||||
steps:
|
steps:
|
||||||
- name: Check out code from GitHub
|
- name: Check out code from GitHub
|
||||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||||
- name: Restore Python
|
- name: Restore Python
|
||||||
uses: ./.github/actions/restore-python
|
uses: ./.github/actions/restore-python
|
||||||
with:
|
with:
|
||||||
@@ -151,11 +152,11 @@ jobs:
|
|||||||
if: needs.determine-jobs.outputs.device-builder == 'true'
|
if: needs.determine-jobs.outputs.device-builder == 'true'
|
||||||
steps:
|
steps:
|
||||||
- name: Check out esphome (this PR)
|
- name: Check out esphome (this PR)
|
||||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||||
with:
|
with:
|
||||||
path: esphome
|
path: esphome
|
||||||
- name: Check out esphome/device-builder
|
- name: Check out esphome/device-builder
|
||||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||||
with:
|
with:
|
||||||
repository: esphome/device-builder
|
repository: esphome/device-builder
|
||||||
ref: main
|
ref: main
|
||||||
@@ -170,7 +171,7 @@ jobs:
|
|||||||
# install step (order-of-magnitude faster on cold boots,
|
# install step (order-of-magnitude faster on cold boots,
|
||||||
# with its own wheel cache). actions/setup-python still
|
# with its own wheel cache). actions/setup-python still
|
||||||
# provides the interpreter.
|
# provides the interpreter.
|
||||||
uses: astral-sh/setup-uv@08807647e7069bb48b6ef5acd8ec9567f424441b # v8.1.0
|
uses: astral-sh/setup-uv@fac544c07dec837d0ccb6301d7b5580bf5edae39 # v8.2.0
|
||||||
with:
|
with:
|
||||||
enable-cache: true
|
enable-cache: true
|
||||||
# Pin uv version so the action does not have to fetch the
|
# Pin uv version so the action does not have to fetch the
|
||||||
@@ -189,9 +190,12 @@ jobs:
|
|||||||
- name: Run device-builder pytest
|
- name: Run device-builder pytest
|
||||||
# ``-n auto`` runs under pytest-xdist (matches device-builder's
|
# ``-n auto`` runs under pytest-xdist (matches device-builder's
|
||||||
# own CI). No ``--cov`` here -- this is purely a downstream
|
# own CI). No ``--cov`` here -- this is purely a downstream
|
||||||
# smoke check against this PR's esphome code.
|
# smoke check against this PR's esphome code. ``tests/e2e/slow``
|
||||||
|
# is excluded: those are real multi-minute toolchain compiles
|
||||||
|
# (LibreTiny SDK clone, native ESP-IDF install) that device-builder
|
||||||
|
# runs in its own dedicated jobs, not this smoke check.
|
||||||
working-directory: device-builder
|
working-directory: device-builder
|
||||||
run: pytest -q -n auto --maxfail=5 --durations=30 --no-cov --ignore=tests/benchmarks
|
run: pytest -q -n auto --maxfail=5 --durations=30 --no-cov --ignore=tests/benchmarks --ignore=tests/e2e/slow
|
||||||
|
|
||||||
pytest:
|
pytest:
|
||||||
name: Run pytest
|
name: Run pytest
|
||||||
@@ -221,7 +225,7 @@ jobs:
|
|||||||
if: needs.determine-jobs.outputs.core-ci == 'true'
|
if: needs.determine-jobs.outputs.core-ci == 'true'
|
||||||
steps:
|
steps:
|
||||||
- name: Check out code from GitHub
|
- name: Check out code from GitHub
|
||||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||||
- name: Restore Python
|
- name: Restore Python
|
||||||
id: restore-python
|
id: restore-python
|
||||||
uses: ./.github/actions/restore-python
|
uses: ./.github/actions/restore-python
|
||||||
@@ -241,7 +245,7 @@ jobs:
|
|||||||
. venv/bin/activate
|
. venv/bin/activate
|
||||||
pytest -vv --cov-report=xml --tb=native --durations=30 -n auto tests --ignore=tests/integration/
|
pytest -vv --cov-report=xml --tb=native --durations=30 -n auto tests --ignore=tests/integration/
|
||||||
- name: Upload coverage to Codecov
|
- name: Upload coverage to Codecov
|
||||||
uses: codecov/codecov-action@e79a6962e0d4c0c17b229090214935d2e33f8354 # v6.0.1
|
uses: codecov/codecov-action@fb8b3582c8e4def4969c97caa2f19720cb33a72f # v7.0.0
|
||||||
with:
|
with:
|
||||||
token: ${{ secrets.CODECOV_TOKEN }}
|
token: ${{ secrets.CODECOV_TOKEN }}
|
||||||
- name: Save Python virtual environment cache
|
- name: Save Python virtual environment cache
|
||||||
@@ -281,7 +285,7 @@ jobs:
|
|||||||
benchmarks: ${{ steps.determine.outputs.benchmarks }}
|
benchmarks: ${{ steps.determine.outputs.benchmarks }}
|
||||||
steps:
|
steps:
|
||||||
- name: Check out code from GitHub
|
- name: Check out code from GitHub
|
||||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||||
with:
|
with:
|
||||||
# Fetch enough history to find the merge base
|
# Fetch enough history to find the merge base
|
||||||
fetch-depth: 2
|
fetch-depth: 2
|
||||||
@@ -353,7 +357,7 @@ jobs:
|
|||||||
bucket: ${{ fromJson(needs.determine-jobs.outputs.integration-test-buckets) }}
|
bucket: ${{ fromJson(needs.determine-jobs.outputs.integration-test-buckets) }}
|
||||||
steps:
|
steps:
|
||||||
- name: Check out code from GitHub
|
- 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
|
- name: Set up Python 3.13
|
||||||
id: python
|
id: python
|
||||||
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
|
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
|
||||||
@@ -368,7 +372,7 @@ jobs:
|
|||||||
- name: Set up uv
|
- name: Set up uv
|
||||||
# Only needed on cache miss to populate the venv.
|
# Only needed on cache miss to populate the venv.
|
||||||
if: steps.cache-venv.outputs.cache-hit != 'true'
|
if: steps.cache-venv.outputs.cache-hit != 'true'
|
||||||
uses: astral-sh/setup-uv@08807647e7069bb48b6ef5acd8ec9567f424441b # v8.1.0
|
uses: astral-sh/setup-uv@fac544c07dec837d0ccb6301d7b5580bf5edae39 # v8.2.0
|
||||||
with:
|
with:
|
||||||
enable-cache: true
|
enable-cache: true
|
||||||
# Pin uv version so the action does not have to fetch the
|
# Pin uv version so the action does not have to fetch the
|
||||||
@@ -405,7 +409,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 != '[]')
|
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:
|
steps:
|
||||||
- name: Check out code from GitHub
|
- name: Check out code from GitHub
|
||||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||||
|
|
||||||
- name: Restore Python
|
- name: Restore Python
|
||||||
uses: ./.github/actions/restore-python
|
uses: ./.github/actions/restore-python
|
||||||
@@ -434,7 +438,7 @@ jobs:
|
|||||||
(github.event_name == 'pull_request' && needs.determine-jobs.outputs.benchmarks == 'true')
|
(github.event_name == 'pull_request' && needs.determine-jobs.outputs.benchmarks == 'true')
|
||||||
steps:
|
steps:
|
||||||
- name: Check out code from GitHub
|
- name: Check out code from GitHub
|
||||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||||
|
|
||||||
- name: Restore Python
|
- name: Restore Python
|
||||||
uses: ./.github/actions/restore-python
|
uses: ./.github/actions/restore-python
|
||||||
@@ -452,7 +456,7 @@ jobs:
|
|||||||
echo "binary=$BINARY" >> $GITHUB_OUTPUT
|
echo "binary=$BINARY" >> $GITHUB_OUTPUT
|
||||||
|
|
||||||
- name: Run CodSpeed benchmarks
|
- name: Run CodSpeed benchmarks
|
||||||
uses: CodSpeedHQ/action@3194d9a39c4d46684cb44bf7207fc56626aad8fd # v4.15.1
|
uses: CodSpeedHQ/action@c145068895e045cc725ee76fcd2307624b65c3af # v4.17.5
|
||||||
with:
|
with:
|
||||||
run: |
|
run: |
|
||||||
. venv/bin/activate
|
. venv/bin/activate
|
||||||
@@ -469,6 +473,8 @@ jobs:
|
|||||||
if: needs.determine-jobs.outputs.clang-tidy == 'true'
|
if: needs.determine-jobs.outputs.clang-tidy == 'true'
|
||||||
env:
|
env:
|
||||||
GH_TOKEN: ${{ github.token }}
|
GH_TOKEN: ${{ github.token }}
|
||||||
|
# esp32-arduino-tidy installs ESP-IDF natively; share the native IDF cache.
|
||||||
|
ESPHOME_ESP_IDF_PREFIX: ~/.esphome-idf
|
||||||
strategy:
|
strategy:
|
||||||
fail-fast: false
|
fail-fast: false
|
||||||
max-parallel: 2
|
max-parallel: 2
|
||||||
@@ -479,9 +485,9 @@ jobs:
|
|||||||
options: --environment esp8266-arduino-tidy --grep USE_ESP8266
|
options: --environment esp8266-arduino-tidy --grep USE_ESP8266
|
||||||
pio_cache_key: tidyesp8266
|
pio_cache_key: tidyesp8266
|
||||||
- id: clang-tidy
|
- id: clang-tidy
|
||||||
name: Run script/clang-tidy for ESP32 IDF
|
name: Run script/clang-tidy for ESP32 Arduino
|
||||||
options: --environment esp32-idf-tidy --grep USE_ESP_IDF
|
options: --environment esp32-arduino-tidy --grep USE_ARDUINO
|
||||||
pio_cache_key: tidyesp32-idf
|
cache_idf: true
|
||||||
- id: clang-tidy
|
- id: clang-tidy
|
||||||
name: Run script/clang-tidy for ZEPHYR
|
name: Run script/clang-tidy for ZEPHYR
|
||||||
options: --environment nrf52-tidy --grep USE_ZEPHYR --grep USE_NRF52
|
options: --environment nrf52-tidy --grep USE_ZEPHYR --grep USE_NRF52
|
||||||
@@ -490,7 +496,7 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Check out code from GitHub
|
- name: Check out code from GitHub
|
||||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||||
with:
|
with:
|
||||||
# Need history for HEAD~1 to work for checking changed files
|
# Need history for HEAD~1 to work for checking changed files
|
||||||
fetch-depth: 2
|
fetch-depth: 2
|
||||||
@@ -502,31 +508,31 @@ jobs:
|
|||||||
cache-key: ${{ needs.common.outputs.cache-key }}
|
cache-key: ${{ needs.common.outputs.cache-key }}
|
||||||
|
|
||||||
- name: Cache platformio
|
- name: Cache platformio
|
||||||
if: github.ref == 'refs/heads/dev'
|
if: github.ref == 'refs/heads/dev' && matrix.pio_cache_key
|
||||||
uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
|
uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
|
||||||
with:
|
with:
|
||||||
path: ~/.platformio
|
path: ~/.platformio
|
||||||
key: platformio-${{ matrix.pio_cache_key }}-${{ hashFiles('platformio.ini') }}
|
key: platformio-${{ matrix.pio_cache_key }}-${{ hashFiles('platformio.ini') }}
|
||||||
|
|
||||||
- name: Cache platformio
|
- name: Cache platformio
|
||||||
if: github.ref != 'refs/heads/dev'
|
if: github.ref != 'refs/heads/dev' && matrix.pio_cache_key
|
||||||
uses: actions/cache/restore@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
|
uses: actions/cache/restore@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
|
||||||
with:
|
with:
|
||||||
path: ~/.platformio
|
path: ~/.platformio
|
||||||
key: platformio-${{ matrix.pio_cache_key }}-${{ hashFiles('platformio.ini') }}
|
key: platformio-${{ matrix.pio_cache_key }}-${{ hashFiles('platformio.ini') }}
|
||||||
|
|
||||||
|
- name: Cache ESP-IDF install
|
||||||
|
# Shared with the IDF tidy + native-IDF build jobs (same install).
|
||||||
|
if: matrix.cache_idf
|
||||||
|
uses: ./.github/actions/cache-esp-idf
|
||||||
|
with:
|
||||||
|
framework: arduino
|
||||||
|
|
||||||
- name: Register problem matchers
|
- name: Register problem matchers
|
||||||
run: |
|
run: |
|
||||||
echo "::add-matcher::.github/workflows/matchers/gcc.json"
|
echo "::add-matcher::.github/workflows/matchers/gcc.json"
|
||||||
echo "::add-matcher::.github/workflows/matchers/clang-tidy.json"
|
echo "::add-matcher::.github/workflows/matchers/clang-tidy.json"
|
||||||
|
|
||||||
- name: Run 'pio run --list-targets -e esp32-idf-tidy'
|
|
||||||
if: matrix.name == 'Run script/clang-tidy for ESP32 IDF'
|
|
||||||
run: |
|
|
||||||
. venv/bin/activate
|
|
||||||
mkdir -p .temp
|
|
||||||
pio run --list-targets -e esp32-idf-tidy
|
|
||||||
|
|
||||||
- name: Check if full clang-tidy scan needed
|
- name: Check if full clang-tidy scan needed
|
||||||
id: check_full_scan
|
id: check_full_scan
|
||||||
run: |
|
run: |
|
||||||
@@ -565,7 +571,7 @@ jobs:
|
|||||||
if: always()
|
if: always()
|
||||||
|
|
||||||
clang-tidy-nosplit:
|
clang-tidy-nosplit:
|
||||||
name: Run script/clang-tidy for ESP32 Arduino
|
name: Run script/clang-tidy for ESP32 IDF
|
||||||
runs-on: ubuntu-24.04
|
runs-on: ubuntu-24.04
|
||||||
needs:
|
needs:
|
||||||
- common
|
- common
|
||||||
@@ -573,9 +579,11 @@ jobs:
|
|||||||
if: needs.determine-jobs.outputs.clang-tidy-mode == 'nosplit'
|
if: needs.determine-jobs.outputs.clang-tidy-mode == 'nosplit'
|
||||||
env:
|
env:
|
||||||
GH_TOKEN: ${{ github.token }}
|
GH_TOKEN: ${{ github.token }}
|
||||||
|
# esp32-idf-tidy installs ESP-IDF natively; share the native IDF cache.
|
||||||
|
ESPHOME_ESP_IDF_PREFIX: ~/.esphome-idf
|
||||||
steps:
|
steps:
|
||||||
- name: Check out code from GitHub
|
- name: Check out code from GitHub
|
||||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||||
with:
|
with:
|
||||||
# Need history for HEAD~1 to work for checking changed files
|
# Need history for HEAD~1 to work for checking changed files
|
||||||
fetch-depth: 2
|
fetch-depth: 2
|
||||||
@@ -586,19 +594,9 @@ jobs:
|
|||||||
python-version: ${{ env.DEFAULT_PYTHON }}
|
python-version: ${{ env.DEFAULT_PYTHON }}
|
||||||
cache-key: ${{ needs.common.outputs.cache-key }}
|
cache-key: ${{ needs.common.outputs.cache-key }}
|
||||||
|
|
||||||
- name: Cache platformio
|
- name: Cache ESP-IDF install
|
||||||
if: github.ref == 'refs/heads/dev'
|
# Shared with the Arduino tidy + native-IDF build jobs (same install).
|
||||||
uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
|
uses: ./.github/actions/cache-esp-idf
|
||||||
with:
|
|
||||||
path: ~/.platformio
|
|
||||||
key: platformio-tidyesp32-${{ hashFiles('platformio.ini') }}
|
|
||||||
|
|
||||||
- name: Cache platformio
|
|
||||||
if: github.ref != 'refs/heads/dev'
|
|
||||||
uses: actions/cache/restore@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
|
|
||||||
with:
|
|
||||||
path: ~/.platformio
|
|
||||||
key: platformio-tidyesp32-${{ hashFiles('platformio.ini') }}
|
|
||||||
|
|
||||||
- name: Register problem matchers
|
- name: Register problem matchers
|
||||||
run: |
|
run: |
|
||||||
@@ -628,10 +626,10 @@ jobs:
|
|||||||
. venv/bin/activate
|
. venv/bin/activate
|
||||||
if [ "${{ steps.check_full_scan.outputs.full_scan }}" = "true" ]; then
|
if [ "${{ steps.check_full_scan.outputs.full_scan }}" = "true" ]; then
|
||||||
echo "Running FULL clang-tidy scan (reason: ${{ steps.check_full_scan.outputs.reason }})"
|
echo "Running FULL clang-tidy scan (reason: ${{ steps.check_full_scan.outputs.reason }})"
|
||||||
script/clang-tidy --all-headers --fix --environment esp32-arduino-tidy
|
script/clang-tidy --all-headers --fix --environment esp32-idf-tidy
|
||||||
else
|
else
|
||||||
echo "Running clang-tidy on changed files only"
|
echo "Running clang-tidy on changed files only"
|
||||||
script/clang-tidy --all-headers --fix --changed --environment esp32-arduino-tidy
|
script/clang-tidy --all-headers --fix --changed --environment esp32-idf-tidy
|
||||||
fi
|
fi
|
||||||
env:
|
env:
|
||||||
# Also cache libdeps, store them in a ~/.platformio subfolder
|
# Also cache libdeps, store them in a ~/.platformio subfolder
|
||||||
@@ -650,27 +648,26 @@ jobs:
|
|||||||
if: needs.determine-jobs.outputs.clang-tidy-mode == 'split'
|
if: needs.determine-jobs.outputs.clang-tidy-mode == 'split'
|
||||||
env:
|
env:
|
||||||
GH_TOKEN: ${{ github.token }}
|
GH_TOKEN: ${{ github.token }}
|
||||||
|
# esp32-idf-tidy installs ESP-IDF natively; share the native IDF cache.
|
||||||
|
ESPHOME_ESP_IDF_PREFIX: ~/.esphome-idf
|
||||||
strategy:
|
strategy:
|
||||||
fail-fast: false
|
fail-fast: false
|
||||||
max-parallel: 2
|
max-parallel: 3
|
||||||
matrix:
|
matrix:
|
||||||
include:
|
include:
|
||||||
- id: clang-tidy
|
- id: clang-tidy
|
||||||
name: Run script/clang-tidy for ESP32 Arduino 1/4
|
name: Run script/clang-tidy for ESP32 IDF 1/3
|
||||||
options: --environment esp32-arduino-tidy --split-num 4 --split-at 1
|
options: --environment esp32-idf-tidy --split-num 3 --split-at 1
|
||||||
- id: clang-tidy
|
- id: clang-tidy
|
||||||
name: Run script/clang-tidy for ESP32 Arduino 2/4
|
name: Run script/clang-tidy for ESP32 IDF 2/3
|
||||||
options: --environment esp32-arduino-tidy --split-num 4 --split-at 2
|
options: --environment esp32-idf-tidy --split-num 3 --split-at 2
|
||||||
- id: clang-tidy
|
- id: clang-tidy
|
||||||
name: Run script/clang-tidy for ESP32 Arduino 3/4
|
name: Run script/clang-tidy for ESP32 IDF 3/3
|
||||||
options: --environment esp32-arduino-tidy --split-num 4 --split-at 3
|
options: --environment esp32-idf-tidy --split-num 3 --split-at 3
|
||||||
- id: clang-tidy
|
|
||||||
name: Run script/clang-tidy for ESP32 Arduino 4/4
|
|
||||||
options: --environment esp32-arduino-tidy --split-num 4 --split-at 4
|
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Check out code from GitHub
|
- name: Check out code from GitHub
|
||||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||||
with:
|
with:
|
||||||
# Need history for HEAD~1 to work for checking changed files
|
# Need history for HEAD~1 to work for checking changed files
|
||||||
fetch-depth: 2
|
fetch-depth: 2
|
||||||
@@ -681,19 +678,9 @@ jobs:
|
|||||||
python-version: ${{ env.DEFAULT_PYTHON }}
|
python-version: ${{ env.DEFAULT_PYTHON }}
|
||||||
cache-key: ${{ needs.common.outputs.cache-key }}
|
cache-key: ${{ needs.common.outputs.cache-key }}
|
||||||
|
|
||||||
- name: Cache platformio
|
- name: Cache ESP-IDF install
|
||||||
if: github.ref == 'refs/heads/dev'
|
# Shared with the Arduino tidy + native-IDF build jobs (same install).
|
||||||
uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
|
uses: ./.github/actions/cache-esp-idf
|
||||||
with:
|
|
||||||
path: ~/.platformio
|
|
||||||
key: platformio-tidyesp32-${{ hashFiles('platformio.ini') }}
|
|
||||||
|
|
||||||
- name: Cache platformio
|
|
||||||
if: github.ref != 'refs/heads/dev'
|
|
||||||
uses: actions/cache/restore@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
|
|
||||||
with:
|
|
||||||
path: ~/.platformio
|
|
||||||
key: platformio-tidyesp32-${{ hashFiles('platformio.ini') }}
|
|
||||||
|
|
||||||
- name: Register problem matchers
|
- name: Register problem matchers
|
||||||
run: |
|
run: |
|
||||||
@@ -736,6 +723,93 @@ jobs:
|
|||||||
run: script/ci-suggest-changes
|
run: script/ci-suggest-changes
|
||||||
if: always()
|
if: always()
|
||||||
|
|
||||||
|
clang-tidy-esp32-variants:
|
||||||
|
name: ${{ matrix.name }}
|
||||||
|
runs-on: ubuntu-24.04
|
||||||
|
needs:
|
||||||
|
- common
|
||||||
|
- determine-jobs
|
||||||
|
if: needs.determine-jobs.outputs.clang-tidy == 'true'
|
||||||
|
env:
|
||||||
|
GH_TOKEN: ${{ github.token }}
|
||||||
|
# The variant tidy envs install ESP-IDF natively; share the native IDF cache.
|
||||||
|
ESPHOME_ESP_IDF_PREFIX: ~/.esphome-idf
|
||||||
|
strategy:
|
||||||
|
fail-fast: false
|
||||||
|
max-parallel: 3
|
||||||
|
matrix:
|
||||||
|
include:
|
||||||
|
- id: clang-tidy
|
||||||
|
name: Run script/clang-tidy for ESP32 S3
|
||||||
|
options: --environment esp32s3-idf-tidy --grep USE_ESP32_VARIANT_ESP32S3
|
||||||
|
- id: clang-tidy
|
||||||
|
name: Run script/clang-tidy for ESP32 P4
|
||||||
|
# P4 has no native Wi-Fi/BLE; those run over the hosted co-processor,
|
||||||
|
# so their code paths differ -- lint them under the P4 build too.
|
||||||
|
# yamllint disable-line rule:line-length
|
||||||
|
options: --environment esp32p4-idf-tidy --grep USE_ESP32_VARIANT_ESP32P4 --grep USE_ESP32_HOSTED --grep USE_WIFI --grep USE_BLE
|
||||||
|
- id: clang-tidy
|
||||||
|
name: Run script/clang-tidy for ESP32 C6
|
||||||
|
# yamllint disable-line rule:line-length
|
||||||
|
options: --environment esp32c6-idf-tidy --grep USE_ESP32_VARIANT_ESP32C6 --grep USE_OPENTHREAD --grep USE_ZIGBEE
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Check out code from GitHub
|
||||||
|
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||||
|
with:
|
||||||
|
# Need history for HEAD~1 to work for checking changed files
|
||||||
|
fetch-depth: 2
|
||||||
|
|
||||||
|
- name: Restore Python
|
||||||
|
uses: ./.github/actions/restore-python
|
||||||
|
with:
|
||||||
|
python-version: ${{ env.DEFAULT_PYTHON }}
|
||||||
|
cache-key: ${{ needs.common.outputs.cache-key }}
|
||||||
|
|
||||||
|
- name: Cache ESP-IDF install
|
||||||
|
# Shared with the IDF/Arduino clang-tidy jobs + native-IDF build (same install).
|
||||||
|
uses: ./.github/actions/cache-esp-idf
|
||||||
|
|
||||||
|
- name: Register problem matchers
|
||||||
|
run: |
|
||||||
|
echo "::add-matcher::.github/workflows/matchers/gcc.json"
|
||||||
|
echo "::add-matcher::.github/workflows/matchers/clang-tidy.json"
|
||||||
|
|
||||||
|
- name: Check if full clang-tidy scan needed
|
||||||
|
id: check_full_scan
|
||||||
|
run: |
|
||||||
|
. venv/bin/activate
|
||||||
|
# determine-jobs.clang-tidy-full-scan is true when core C++ changed
|
||||||
|
# OR the ci-run-all label forced --force-all. Independent of the
|
||||||
|
# hash check, both must produce a full scan in the job itself.
|
||||||
|
if [ "${{ needs.determine-jobs.outputs.clang-tidy-full-scan }}" = "true" ]; then
|
||||||
|
echo "full_scan=true" >> $GITHUB_OUTPUT
|
||||||
|
echo "reason=determine_jobs" >> $GITHUB_OUTPUT
|
||||||
|
elif python script/clang_tidy_hash.py --check; then
|
||||||
|
echo "full_scan=true" >> $GITHUB_OUTPUT
|
||||||
|
echo "reason=hash_changed" >> $GITHUB_OUTPUT
|
||||||
|
else
|
||||||
|
echo "full_scan=false" >> $GITHUB_OUTPUT
|
||||||
|
echo "reason=normal" >> $GITHUB_OUTPUT
|
||||||
|
fi
|
||||||
|
|
||||||
|
- name: Run clang-tidy
|
||||||
|
# Limited variant scan: only the files carrying that variant's code paths
|
||||||
|
# (no --all-headers; the comprehensive esp32-idf pass covers the shared tree).
|
||||||
|
run: |
|
||||||
|
. venv/bin/activate
|
||||||
|
if [ "${{ steps.check_full_scan.outputs.full_scan }}" = "true" ]; then
|
||||||
|
echo "Running FULL clang-tidy scan (reason: ${{ steps.check_full_scan.outputs.reason }})"
|
||||||
|
script/clang-tidy --fix ${{ matrix.options }}
|
||||||
|
else
|
||||||
|
echo "Running clang-tidy on changed files only"
|
||||||
|
script/clang-tidy --fix --changed ${{ matrix.options }}
|
||||||
|
fi
|
||||||
|
|
||||||
|
- name: Suggested changes
|
||||||
|
run: script/ci-suggest-changes
|
||||||
|
if: always()
|
||||||
|
|
||||||
test-build-components-split:
|
test-build-components-split:
|
||||||
name: Test components batch (${{ matrix.components }})
|
name: Test components batch (${{ matrix.components }})
|
||||||
runs-on: ubuntu-24.04
|
runs-on: ubuntu-24.04
|
||||||
@@ -764,7 +838,7 @@ jobs:
|
|||||||
version: 1.0
|
version: 1.0
|
||||||
|
|
||||||
- name: Check out code from GitHub
|
- name: Check out code from GitHub
|
||||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||||
- name: Restore Python
|
- name: Restore Python
|
||||||
uses: ./.github/actions/restore-python
|
uses: ./.github/actions/restore-python
|
||||||
with:
|
with:
|
||||||
@@ -821,7 +895,7 @@ jobs:
|
|||||||
fi
|
fi
|
||||||
echo ""
|
echo ""
|
||||||
|
|
||||||
# Show disk space before validation (after bind mounts setup)
|
# Show disk space before validation
|
||||||
echo "Disk space before config validation:"
|
echo "Disk space before config validation:"
|
||||||
df -h
|
df -h
|
||||||
echo ""
|
echo ""
|
||||||
@@ -889,7 +963,7 @@ jobs:
|
|||||||
TEST_COMPONENTS: ${{ needs.determine-jobs.outputs.native-idf-components }}
|
TEST_COMPONENTS: ${{ needs.determine-jobs.outputs.native-idf-components }}
|
||||||
steps:
|
steps:
|
||||||
- name: Check out code from GitHub
|
- name: Check out code from GitHub
|
||||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||||
|
|
||||||
- name: Restore Python
|
- name: Restore Python
|
||||||
uses: ./.github/actions/restore-python
|
uses: ./.github/actions/restore-python
|
||||||
@@ -897,33 +971,20 @@ jobs:
|
|||||||
python-version: ${{ env.DEFAULT_PYTHON }}
|
python-version: ${{ env.DEFAULT_PYTHON }}
|
||||||
cache-key: ${{ needs.common.outputs.cache-key }}
|
cache-key: ${{ needs.common.outputs.cache-key }}
|
||||||
|
|
||||||
- name: Cache ESPHome
|
- name: Prepare build storage on /mnt
|
||||||
uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
|
# Bind-mount the larger /mnt disk over the IDF install + build dirs BEFORE
|
||||||
with:
|
# restoring the cache, so the ~4.5GB restore lands on the roomier volume
|
||||||
path: ~/.esphome-idf
|
# instead of being shadowed by a mount set up later in the run step.
|
||||||
key: ${{ runner.os }}-esphome-${{ needs.common.outputs.cache-key }}
|
|
||||||
|
|
||||||
- name: Run native ESP-IDF compile test
|
|
||||||
run: |
|
run: |
|
||||||
. venv/bin/activate
|
|
||||||
|
|
||||||
# Check if /mnt has more free space than / before bind mounting
|
|
||||||
# Extract available space in KB for comparison
|
|
||||||
root_avail=$(df -k / | awk 'NR==2 {print $4}')
|
root_avail=$(df -k / | awk 'NR==2 {print $4}')
|
||||||
mnt_avail=$(df -k /mnt 2>/dev/null | awk 'NR==2 {print $4}')
|
mnt_avail=$(df -k /mnt 2>/dev/null | awk 'NR==2 {print $4}')
|
||||||
|
|
||||||
echo "Available space: / has ${root_avail}KB, /mnt has ${mnt_avail}KB"
|
echo "Available space: / has ${root_avail}KB, /mnt has ${mnt_avail}KB"
|
||||||
|
|
||||||
# Only use /mnt if it has more space than /
|
|
||||||
if [ -n "$mnt_avail" ] && [ "$mnt_avail" -gt "$root_avail" ]; then
|
if [ -n "$mnt_avail" ] && [ "$mnt_avail" -gt "$root_avail" ]; then
|
||||||
echo "Using /mnt for build files (more space available)"
|
echo "Using /mnt for build files (more space available)"
|
||||||
# Bind mount PlatformIO directory to /mnt (tools, packages, build cache all go there)
|
|
||||||
sudo mkdir -p /mnt/esphome-idf
|
sudo mkdir -p /mnt/esphome-idf
|
||||||
sudo chown $USER:$USER /mnt/esphome-idf
|
sudo chown $USER:$USER /mnt/esphome-idf
|
||||||
mkdir -p ~/.esphome-idf
|
mkdir -p ~/.esphome-idf
|
||||||
sudo mount --bind /mnt/esphome-idf ~/.esphome-idf
|
sudo mount --bind /mnt/esphome-idf ~/.esphome-idf
|
||||||
|
|
||||||
# Bind mount test build directory to /mnt
|
|
||||||
sudo mkdir -p /mnt/test_build_components_build
|
sudo mkdir -p /mnt/test_build_components_build
|
||||||
sudo chown $USER:$USER /mnt/test_build_components_build
|
sudo chown $USER:$USER /mnt/test_build_components_build
|
||||||
mkdir -p tests/test_build_components/build
|
mkdir -p tests/test_build_components/build
|
||||||
@@ -932,10 +993,19 @@ jobs:
|
|||||||
echo "Using / for build files (more space available than /mnt or /mnt unavailable)"
|
echo "Using / for build files (more space available than /mnt or /mnt unavailable)"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
- name: Cache ESP-IDF install
|
||||||
|
# Shared with the IDF/Arduino clang-tidy jobs (same install); restores
|
||||||
|
# into the /mnt bind-mount prepared above when present.
|
||||||
|
uses: ./.github/actions/cache-esp-idf
|
||||||
|
|
||||||
|
- name: Run native ESP-IDF compile test
|
||||||
|
run: |
|
||||||
|
. venv/bin/activate
|
||||||
|
|
||||||
echo "Testing components: $TEST_COMPONENTS"
|
echo "Testing components: $TEST_COMPONENTS"
|
||||||
echo ""
|
echo ""
|
||||||
|
|
||||||
# Show disk space before validation (after bind mounts setup)
|
# Show disk space before validation
|
||||||
echo "Disk space before config validation:"
|
echo "Disk space before config validation:"
|
||||||
df -h
|
df -h
|
||||||
echo ""
|
echo ""
|
||||||
@@ -971,7 +1041,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'
|
if: github.event_name == 'pull_request' && !startsWith(github.base_ref, 'beta') && !startsWith(github.base_ref, 'release') && needs.determine-jobs.outputs.core-ci == 'true'
|
||||||
steps:
|
steps:
|
||||||
- name: Check out code from GitHub
|
- name: Check out code from GitHub
|
||||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||||
- name: Restore Python
|
- name: Restore Python
|
||||||
uses: ./.github/actions/restore-python
|
uses: ./.github/actions/restore-python
|
||||||
with:
|
with:
|
||||||
@@ -997,7 +1067,7 @@ jobs:
|
|||||||
skip: ${{ steps.check-script.outputs.skip || steps.check-tests.outputs.skip }}
|
skip: ${{ steps.check-script.outputs.skip || steps.check-tests.outputs.skip }}
|
||||||
steps:
|
steps:
|
||||||
- name: Check out target branch
|
- name: Check out target branch
|
||||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||||
with:
|
with:
|
||||||
ref: ${{ github.base_ref }}
|
ref: ${{ github.base_ref }}
|
||||||
|
|
||||||
@@ -1179,7 +1249,7 @@ jobs:
|
|||||||
flash_usage: ${{ steps.extract.outputs.flash_usage }}
|
flash_usage: ${{ steps.extract.outputs.flash_usage }}
|
||||||
steps:
|
steps:
|
||||||
- name: Check out PR branch
|
- name: Check out PR branch
|
||||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||||
- name: Restore Python
|
- name: Restore Python
|
||||||
uses: ./.github/actions/restore-python
|
uses: ./.github/actions/restore-python
|
||||||
with:
|
with:
|
||||||
@@ -1248,7 +1318,7 @@ jobs:
|
|||||||
GH_TOKEN: ${{ github.token }}
|
GH_TOKEN: ${{ github.token }}
|
||||||
steps:
|
steps:
|
||||||
- name: Check out code
|
- name: Check out code
|
||||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||||
- name: Restore Python
|
- name: Restore Python
|
||||||
uses: ./.github/actions/restore-python
|
uses: ./.github/actions/restore-python
|
||||||
with:
|
with:
|
||||||
@@ -1291,6 +1361,7 @@ jobs:
|
|||||||
- clang-tidy-single
|
- clang-tidy-single
|
||||||
- clang-tidy-nosplit
|
- clang-tidy-nosplit
|
||||||
- clang-tidy-split
|
- clang-tidy-split
|
||||||
|
- clang-tidy-esp32-variants
|
||||||
- determine-jobs
|
- determine-jobs
|
||||||
- device-builder
|
- device-builder
|
||||||
- test-build-components-split
|
- test-build-components-split
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout base branch
|
- name: Checkout base branch
|
||||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||||
with:
|
with:
|
||||||
ref: ${{ github.event.pull_request.base.sha }}
|
ref: ${{ github.event.pull_request.base.sha }}
|
||||||
sparse-checkout: |
|
sparse-checkout: |
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout base branch
|
- name: Checkout base branch
|
||||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||||
with:
|
with:
|
||||||
ref: ${{ github.event.pull_request.base.sha }}
|
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
|
# 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:
|
steps:
|
||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||||
|
|
||||||
# Initializes the CodeQL tools for scanning.
|
# Initializes the CodeQL tools for scanning.
|
||||||
- name: Initialize CodeQL
|
- name: Initialize CodeQL
|
||||||
uses: github/codeql-action/init@7211b7c8077ea37d8641b6271f6a365a22a5fbfa # v4.36.0
|
uses: github/codeql-action/init@8aad20d150bbac5944a9f9d289da16a4b0d87c1e # v4.36.2
|
||||||
with:
|
with:
|
||||||
languages: ${{ matrix.language }}
|
languages: ${{ matrix.language }}
|
||||||
build-mode: ${{ matrix.build-mode }}
|
build-mode: ${{ matrix.build-mode }}
|
||||||
@@ -84,6 +84,6 @@ jobs:
|
|||||||
exit 1
|
exit 1
|
||||||
|
|
||||||
- name: Perform CodeQL Analysis
|
- name: Perform CodeQL Analysis
|
||||||
uses: github/codeql-action/analyze@7211b7c8077ea37d8641b6271f6a365a22a5fbfa # v4.36.0
|
uses: github/codeql-action/analyze@8aad20d150bbac5944a9f9d289da16a4b0d87c1e # v4.36.2
|
||||||
with:
|
with:
|
||||||
category: "/language:${{matrix.language}}"
|
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
|
name: Validate PR title
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||||
|
|
||||||
- uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
|
- uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
|
||||||
with:
|
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 }}
|
branch_build: ${{ steps.tag.outputs.branch_build }}
|
||||||
deploy_env: ${{ steps.tag.outputs.deploy_env }}
|
deploy_env: ${{ steps.tag.outputs.deploy_env }}
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||||
- name: Get tag
|
- name: Get tag
|
||||||
id: tag
|
id: tag
|
||||||
# yamllint disable rule:line-length
|
# yamllint disable rule:line-length
|
||||||
@@ -60,7 +60,7 @@ jobs:
|
|||||||
contents: read # actions/checkout to build the sdist/wheel
|
contents: read # actions/checkout to build the sdist/wheel
|
||||||
id-token: write # OIDC token for PyPI Trusted Publishing (pypa/gh-action-pypi-publish)
|
id-token: write # OIDC token for PyPI Trusted Publishing (pypa/gh-action-pypi-publish)
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||||
- name: Set up Python
|
- name: Set up Python
|
||||||
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
|
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
|
||||||
with:
|
with:
|
||||||
@@ -92,7 +92,7 @@ jobs:
|
|||||||
os: "ubuntu-24.04-arm"
|
os: "ubuntu-24.04-arm"
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||||
- name: Set up Python
|
- name: Set up Python
|
||||||
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
|
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
|
||||||
with:
|
with:
|
||||||
@@ -168,7 +168,7 @@ jobs:
|
|||||||
- ghcr
|
- ghcr
|
||||||
- dockerhub
|
- dockerhub
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||||
|
|
||||||
- name: Download digests
|
- name: Download digests
|
||||||
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
|
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
|
||||||
|
|||||||
6
.github/workflows/sync-device-classes.yml
vendored
6
.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
|
permission-pull-requests: write # pulls.create / pulls.update to open or refresh the sync PR
|
||||||
|
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||||
|
|
||||||
- name: Checkout Home Assistant
|
- name: Checkout Home Assistant
|
||||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||||
with:
|
with:
|
||||||
repository: home-assistant/core
|
repository: home-assistant/core
|
||||||
path: lib/home-assistant
|
path: lib/home-assistant
|
||||||
@@ -47,7 +47,7 @@ jobs:
|
|||||||
# setup-python interpreter so subsequent ``pre-commit`` /
|
# setup-python interpreter so subsequent ``pre-commit`` /
|
||||||
# ``script/run-in-env.py`` steps find the deps without a
|
# ``script/run-in-env.py`` steps find the deps without a
|
||||||
# ``uv run`` prefix.
|
# ``uv run`` prefix.
|
||||||
uses: astral-sh/setup-uv@08807647e7069bb48b6ef5acd8ec9567f424441b # v8.1.0
|
uses: astral-sh/setup-uv@fac544c07dec837d0ccb6301d7b5580bf5edae39 # v8.2.0
|
||||||
with:
|
with:
|
||||||
enable-cache: true
|
enable-cache: true
|
||||||
# Pin uv version so the action does not have to fetch the
|
# Pin uv version so the action does not have to fetch the
|
||||||
|
|||||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -141,6 +141,7 @@ tests/.esphome/
|
|||||||
|
|
||||||
sdkconfig.*
|
sdkconfig.*
|
||||||
!sdkconfig.defaults
|
!sdkconfig.defaults
|
||||||
|
!sdkconfig.defaults.*
|
||||||
|
|
||||||
.tests/
|
.tests/
|
||||||
|
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ ci:
|
|||||||
repos:
|
repos:
|
||||||
- repo: https://github.com/astral-sh/ruff-pre-commit
|
- repo: https://github.com/astral-sh/ruff-pre-commit
|
||||||
# Ruff version.
|
# Ruff version.
|
||||||
rev: v0.15.14
|
rev: v0.15.15
|
||||||
hooks:
|
hooks:
|
||||||
# Run the linter.
|
# Run the linter.
|
||||||
- id: ruff
|
- id: ruff
|
||||||
@@ -63,7 +63,7 @@ repos:
|
|||||||
name: Update clang-tidy hash
|
name: Update clang-tidy hash
|
||||||
entry: python script/clang_tidy_hash.py --update-if-changed
|
entry: python script/clang_tidy_hash.py --update-if-changed
|
||||||
language: python
|
language: python
|
||||||
files: ^(\.clang-tidy|platformio\.ini|requirements_dev\.txt)$
|
files: ^(\.clang-tidy|platformio\.ini|requirements_dev\.txt|sdkconfig\.defaults|esphome/idf_component\.yml)$
|
||||||
pass_filenames: false
|
pass_filenames: false
|
||||||
additional_dependencies: []
|
additional_dependencies: []
|
||||||
- id: ci-custom
|
- id: ci-custom
|
||||||
|
|||||||
17
AGENTS.md
17
AGENTS.md
@@ -59,6 +59,19 @@ This document provides essential context for AI models interacting with this pro
|
|||||||
- Protected/private fields: `lower_snake_case_with_trailing_underscore_`
|
- Protected/private fields: `lower_snake_case_with_trailing_underscore_`
|
||||||
- Favor descriptive names over abbreviations
|
- Favor descriptive names over abbreviations
|
||||||
|
|
||||||
|
* **Python Idioms:**
|
||||||
|
* **Assignment expressions (PEP 572):** Prefer the walrus operator (`:=`) wherever it removes a redundant lookup or a throwaway temporary. The most common case in component code is presence-checking a config key and then indexing it separately — fetch once with `.get()` and bind in the condition instead:
|
||||||
|
```python
|
||||||
|
# Bad - looks up CONF_BLAH twice
|
||||||
|
if CONF_BLAH in config:
|
||||||
|
cg.add(var.set_blah(config[CONF_BLAH]))
|
||||||
|
|
||||||
|
# Good - single lookup, value bound inline
|
||||||
|
if (blah := config.get(CONF_BLAH)) is not None:
|
||||||
|
cg.add(var.set_blah(blah))
|
||||||
|
```
|
||||||
|
The same applies to `while` loops and comprehensions where it avoids recomputing a value. Don't contort code to use it — reach for `:=` only when it genuinely cuts repetition or an extra assignment line.
|
||||||
|
|
||||||
* **C++ Field Visibility:**
|
* **C++ Field Visibility:**
|
||||||
* **Prefer `protected`:** Use `protected` for most class fields to enable extensibility and testing. Fields should be `lower_snake_case_with_trailing_underscore_`.
|
* **Prefer `protected`:** Use `protected` for most class fields to enable extensibility and testing. Fields should be `lower_snake_case_with_trailing_underscore_`.
|
||||||
* **Use `private` for safety-critical cases:** Use `private` visibility when direct field access could introduce bugs or violate invariants:
|
* **Use `private` for safety-critical cases:** Use `private` visibility when direct field access could introduce bugs or violate invariants:
|
||||||
@@ -462,7 +475,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.
|
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 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.
|
* 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.
|
* When editing a component's documentation page, also update the corresponding component index page to ensure both pages remain in sync.
|
||||||
|
|
||||||
@@ -681,7 +694,7 @@ This document provides essential context for AI models interacting with this pro
|
|||||||
- [ ] Explored non-breaking alternatives
|
- [ ] Explored non-breaking alternatives
|
||||||
- [ ] Added deprecation warnings if possible (use `ESPDEPRECATED` macro for C++)
|
- [ ] Added deprecation warnings if possible (use `ESPDEPRECATED` macro for C++)
|
||||||
- [ ] Documented migration path in PR description with before/after examples
|
- [ ] 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
|
- [ ] Tested backward compatibility during deprecation period
|
||||||
|
|
||||||
* **Deprecation Pattern (C++):**
|
* **Deprecation Pattern (C++):**
|
||||||
|
|||||||
11
CODEOWNERS
11
CODEOWNERS
@@ -19,7 +19,6 @@ esphome/components/ac_dimmer/* @glmnet
|
|||||||
esphome/components/adc/* @esphome/core
|
esphome/components/adc/* @esphome/core
|
||||||
esphome/components/adc128s102/* @DeerMaximum
|
esphome/components/adc128s102/* @DeerMaximum
|
||||||
esphome/components/addressable_light/* @justfalter
|
esphome/components/addressable_light/* @justfalter
|
||||||
esphome/components/ade7880/* @kpfleming
|
|
||||||
esphome/components/ade7953/* @angelnu
|
esphome/components/ade7953/* @angelnu
|
||||||
esphome/components/ade7953_base/* @angelnu
|
esphome/components/ade7953_base/* @angelnu
|
||||||
esphome/components/ade7953_i2c/* @angelnu
|
esphome/components/ade7953_i2c/* @angelnu
|
||||||
@@ -28,7 +27,7 @@ esphome/components/ads1118/* @solomondg1
|
|||||||
esphome/components/ags10/* @mak-42
|
esphome/components/ags10/* @mak-42
|
||||||
esphome/components/aic3204/* @kbx81
|
esphome/components/aic3204/* @kbx81
|
||||||
esphome/components/airthings_ble/* @jeromelaban
|
esphome/components/airthings_ble/* @jeromelaban
|
||||||
esphome/components/airthings_wave_base/* @jeromelaban @kpfleming @ncareau
|
esphome/components/airthings_wave_base/* @jeromelaban @ncareau
|
||||||
esphome/components/airthings_wave_mini/* @ncareau
|
esphome/components/airthings_wave_mini/* @ncareau
|
||||||
esphome/components/airthings_wave_plus/* @jeromelaban @precurse
|
esphome/components/airthings_wave_plus/* @jeromelaban @precurse
|
||||||
esphome/components/alarm_control_panel/* @grahambrown11 @hwstar
|
esphome/components/alarm_control_panel/* @grahambrown11 @hwstar
|
||||||
@@ -84,6 +83,7 @@ esphome/components/bme680_bsec/* @trvrnrth
|
|||||||
esphome/components/bme68x_bsec2/* @kbx81 @neffs
|
esphome/components/bme68x_bsec2/* @kbx81 @neffs
|
||||||
esphome/components/bme68x_bsec2_i2c/* @kbx81 @neffs
|
esphome/components/bme68x_bsec2_i2c/* @kbx81 @neffs
|
||||||
esphome/components/bmi160/* @flaviut
|
esphome/components/bmi160/* @flaviut
|
||||||
|
esphome/components/bmi270/* @clydebarrow
|
||||||
esphome/components/bmp280_base/* @ademuri
|
esphome/components/bmp280_base/* @ademuri
|
||||||
esphome/components/bmp280_i2c/* @ademuri
|
esphome/components/bmp280_i2c/* @ademuri
|
||||||
esphome/components/bmp280_spi/* @ademuri
|
esphome/components/bmp280_spi/* @ademuri
|
||||||
@@ -139,7 +139,7 @@ esphome/components/dfplayer/* @glmnet
|
|||||||
esphome/components/dfrobot_sen0395/* @niklasweber
|
esphome/components/dfrobot_sen0395/* @niklasweber
|
||||||
esphome/components/dht/* @OttoWinter
|
esphome/components/dht/* @OttoWinter
|
||||||
esphome/components/display_menu_base/* @numo68
|
esphome/components/display_menu_base/* @numo68
|
||||||
esphome/components/dlms_meter/* @SimonFischer04
|
esphome/components/dlms_meter/* @latonita @PolarGoose @SimonFischer04 @Tomer27cz
|
||||||
esphome/components/dps310/* @kbx81
|
esphome/components/dps310/* @kbx81
|
||||||
esphome/components/ds1307/* @badbadc0ffee
|
esphome/components/ds1307/* @badbadc0ffee
|
||||||
esphome/components/ds2484/* @mrk-its
|
esphome/components/ds2484/* @mrk-its
|
||||||
@@ -292,6 +292,7 @@ esphome/components/lock/* @esphome/core
|
|||||||
esphome/components/logger/* @esphome/core
|
esphome/components/logger/* @esphome/core
|
||||||
esphome/components/logger/select/* @clydebarrow
|
esphome/components/logger/select/* @clydebarrow
|
||||||
esphome/components/lps22/* @nagisa
|
esphome/components/lps22/* @nagisa
|
||||||
|
esphome/components/lsm6ds/* @clydebarrow
|
||||||
esphome/components/ltr390/* @latonita @sjtrny
|
esphome/components/ltr390/* @latonita @sjtrny
|
||||||
esphome/components/ltr501/* @latonita
|
esphome/components/ltr501/* @latonita
|
||||||
esphome/components/ltr_als_ps/* @latonita
|
esphome/components/ltr_als_ps/* @latonita
|
||||||
@@ -352,6 +353,7 @@ esphome/components/modbus_server/* @exciton
|
|||||||
esphome/components/mopeka_ble/* @Fabian-Schmidt @spbrogan
|
esphome/components/mopeka_ble/* @Fabian-Schmidt @spbrogan
|
||||||
esphome/components/mopeka_pro_check/* @spbrogan
|
esphome/components/mopeka_pro_check/* @spbrogan
|
||||||
esphome/components/mopeka_std_check/* @Fabian-Schmidt
|
esphome/components/mopeka_std_check/* @Fabian-Schmidt
|
||||||
|
esphome/components/motion/* @esphome/core
|
||||||
esphome/components/mpl3115a2/* @kbickar
|
esphome/components/mpl3115a2/* @kbickar
|
||||||
esphome/components/mpu6886/* @fabaff
|
esphome/components/mpu6886/* @fabaff
|
||||||
esphome/components/ms8607/* @e28eta
|
esphome/components/ms8607/* @e28eta
|
||||||
@@ -380,6 +382,7 @@ esphome/components/pca6416a/* @Mat931
|
|||||||
esphome/components/pca9554/* @bdraco @clydebarrow @hwstar
|
esphome/components/pca9554/* @bdraco @clydebarrow @hwstar
|
||||||
esphome/components/pcf85063/* @brogon
|
esphome/components/pcf85063/* @brogon
|
||||||
esphome/components/pcf8563/* @KoenBreeman
|
esphome/components/pcf8563/* @KoenBreeman
|
||||||
|
esphome/components/pcm5122/* @remcom
|
||||||
esphome/components/pi4ioe5v6408/* @jesserockz
|
esphome/components/pi4ioe5v6408/* @jesserockz
|
||||||
esphome/components/pid/* @OttoWinter
|
esphome/components/pid/* @OttoWinter
|
||||||
esphome/components/pipsolar/* @andreashergert1984
|
esphome/components/pipsolar/* @andreashergert1984
|
||||||
@@ -418,6 +421,7 @@ esphome/components/restart/* @esphome/core
|
|||||||
esphome/components/rf_bridge/* @jesserockz
|
esphome/components/rf_bridge/* @jesserockz
|
||||||
esphome/components/rgbct/* @jesserockz
|
esphome/components/rgbct/* @jesserockz
|
||||||
esphome/components/ring_buffer/* @kahrendt
|
esphome/components/ring_buffer/* @kahrendt
|
||||||
|
esphome/components/router/speaker/* @kahrendt
|
||||||
esphome/components/rp2040/* @jesserockz
|
esphome/components/rp2040/* @jesserockz
|
||||||
esphome/components/rp2040_ble/* @bdraco
|
esphome/components/rp2040_ble/* @bdraco
|
||||||
esphome/components/rp2040_pio_led_strip/* @Papa-DMan
|
esphome/components/rp2040_pio_led_strip/* @Papa-DMan
|
||||||
@@ -596,6 +600,7 @@ esphome/components/wk2212_spi/* @DrCoolZic
|
|||||||
esphome/components/wl_134/* @hobbypunk90
|
esphome/components/wl_134/* @hobbypunk90
|
||||||
esphome/components/wts01/* @alepee
|
esphome/components/wts01/* @alepee
|
||||||
esphome/components/x9c/* @EtienneMD
|
esphome/components/x9c/* @EtienneMD
|
||||||
|
esphome/components/xdb401/* @RT530
|
||||||
esphome/components/xgzp68xx/* @gcormier
|
esphome/components/xgzp68xx/* @gcormier
|
||||||
esphome/components/xiaomi_hhccjcy10/* @fariouche
|
esphome/components/xiaomi_hhccjcy10/* @fariouche
|
||||||
esphome/components/xiaomi_lywsd02mmc/* @juanluss31
|
esphome/components/xiaomi_lywsd02mmc/* @juanluss31
|
||||||
|
|||||||
2
Doxyfile
2
Doxyfile
@@ -48,7 +48,7 @@ PROJECT_NAME = ESPHome
|
|||||||
# could be handy for archiving the generated documentation or if some version
|
# could be handy for archiving the generated documentation or if some version
|
||||||
# control system is used.
|
# control system is used.
|
||||||
|
|
||||||
PROJECT_NUMBER = 2026.6.0-dev
|
PROJECT_NUMBER = 2026.7.0-dev
|
||||||
|
|
||||||
# Using the PROJECT_BRIEF tag one can provide an optional one line description
|
# Using the PROJECT_BRIEF tag one can provide an optional one line description
|
||||||
# for a project that appears at the top of each page and should give viewer a
|
# for a project that appears at the top of each page and should give viewer a
|
||||||
|
|||||||
18
codecov.yml
Normal file
18
codecov.yml
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
coverage:
|
||||||
|
status:
|
||||||
|
patch:
|
||||||
|
default:
|
||||||
|
target: 100%
|
||||||
|
threshold: 0%
|
||||||
|
project:
|
||||||
|
default:
|
||||||
|
informational: true
|
||||||
|
|
||||||
|
ignore:
|
||||||
|
- "esphome/components/**/*"
|
||||||
|
- "esphome/analyze_memory/**/*"
|
||||||
|
- "tests/integration/**/*"
|
||||||
|
|
||||||
|
comment:
|
||||||
|
layout: "reach, diff, flags, files"
|
||||||
|
require_changes: true
|
||||||
@@ -6,11 +6,15 @@
|
|||||||
# ==============================================================================
|
# ==============================================================================
|
||||||
|
|
||||||
# The new device builder handles HA ingress itself, so nginx is bypassed.
|
# The new device builder handles HA ingress itself, so nginx is bypassed.
|
||||||
# Block the longrun forever so s6 keeps the dependency satisfied and does
|
# Block the longrun so s6 keeps the dependency satisfied, but exit 0 on
|
||||||
# not respawn it.
|
# SIGTERM instead of being signal-killed; a 256/15 exit makes nginx/finish
|
||||||
|
# stamp the container exit 143, which trips the Supervisor's SIGTERM check.
|
||||||
if bashio::config.true 'use_new_device_builder'; then
|
if bashio::config.true 'use_new_device_builder'; then
|
||||||
bashio::log.info "NGINX bypassed: new device builder serves ingress directly."
|
bashio::log.info "NGINX bypassed: new device builder serves ingress directly."
|
||||||
exec sleep infinity
|
trap 'exit 0' TERM
|
||||||
|
sleep infinity &
|
||||||
|
wait
|
||||||
|
exit 0
|
||||||
fi
|
fi
|
||||||
|
|
||||||
bashio::log.info "Waiting for ESPHome dashboard to come up..."
|
bashio::log.info "Waiting for ESPHome dashboard to come up..."
|
||||||
|
|||||||
@@ -608,7 +608,7 @@ def run_miniterm(config: ConfigType, port: str, args) -> int:
|
|||||||
|
|
||||||
try:
|
try:
|
||||||
module = importlib.import_module("esphome.components." + CORE.target_platform)
|
module = importlib.import_module("esphome.components." + CORE.target_platform)
|
||||||
process_stacktrace = getattr(module, "process_stacktrace")
|
process_stacktrace = module.process_stacktrace
|
||||||
except (AttributeError, ImportError):
|
except (AttributeError, ImportError):
|
||||||
_LOGGER.info(
|
_LOGGER.info(
|
||||||
'Stacktrace analysis is unavailable: no compatible analyzer found for target platform "%s".',
|
'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)
|
chunk = ser.read(ser.in_waiting or 1)
|
||||||
if not chunk:
|
if not chunk:
|
||||||
continue
|
continue
|
||||||
time_ = datetime.now()
|
time_ = datetime.now().astimezone()
|
||||||
milliseconds = time_.microsecond // 1000
|
milliseconds = time_.microsecond // 1000
|
||||||
time_str = f"[{time_.hour:02}:{time_.minute:02}:{time_.second:02}.{milliseconds:03}]"
|
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:
|
def write_cpp(config: ConfigType) -> int:
|
||||||
from esphome import writer
|
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):
|
if not get_bool_env(ENV_NOGITIGNORE):
|
||||||
writer.write_gitignore()
|
writer.write_gitignore()
|
||||||
|
|
||||||
@@ -760,6 +765,7 @@ def compile_program(args: ArgsProtocol, config: ConfigType) -> int:
|
|||||||
toolchain.create_factory_bin()
|
toolchain.create_factory_bin()
|
||||||
toolchain.create_ota_bin()
|
toolchain.create_ota_bin()
|
||||||
toolchain.create_elf_copy()
|
toolchain.create_elf_copy()
|
||||||
|
toolchain.get_idedata()
|
||||||
else:
|
else:
|
||||||
from esphome.platformio import toolchain
|
from esphome.platformio import toolchain
|
||||||
|
|
||||||
@@ -794,7 +800,7 @@ def _check_and_emit_build_info() -> None:
|
|||||||
|
|
||||||
# Read build_info from JSON
|
# Read build_info from JSON
|
||||||
try:
|
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)
|
build_info = json.load(f)
|
||||||
except (OSError, json.JSONDecodeError) as e:
|
except (OSError, json.JSONDecodeError) as e:
|
||||||
_LOGGER.debug("Failed to read build_info: %s", e)
|
_LOGGER.debug("Failed to read build_info: %s", e)
|
||||||
@@ -1056,7 +1062,7 @@ def _wait_for_serial_port(
|
|||||||
def _port_found() -> bool:
|
def _port_found() -> bool:
|
||||||
if port is not None:
|
if port is not None:
|
||||||
if os.name == "posix":
|
if os.name == "posix":
|
||||||
return os.path.exists(port)
|
return Path(port).exists()
|
||||||
return any(p.path == port for p in get_serial_ports())
|
return any(p.path == port for p in get_serial_ports())
|
||||||
ports = get_serial_ports()
|
ports = get_serial_ports()
|
||||||
if known_ports is not None:
|
if known_ports is not None:
|
||||||
@@ -1101,7 +1107,7 @@ def upload_program(
|
|||||||
host = devices[0]
|
host = devices[0]
|
||||||
try:
|
try:
|
||||||
module = importlib.import_module("esphome.components." + CORE.target_platform)
|
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
|
return 0, host
|
||||||
except AttributeError:
|
except AttributeError:
|
||||||
pass
|
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:
|
def show_logs(config: ConfigType, args: ArgsProtocol, devices: list[str]) -> int | None:
|
||||||
try:
|
try:
|
||||||
module = importlib.import_module("esphome.components." + CORE.target_platform)
|
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
|
return 0
|
||||||
except AttributeError:
|
except AttributeError:
|
||||||
pass
|
pass
|
||||||
@@ -1379,7 +1398,7 @@ def show_logs(config: ConfigType, args: ArgsProtocol, devices: list[str]) -> int
|
|||||||
return run_logs(
|
return run_logs(
|
||||||
config,
|
config,
|
||||||
network_devices,
|
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():
|
if port_type in (PortType.NETWORK, PortType.MQTT) and has_mqtt_logging():
|
||||||
@@ -1409,20 +1428,59 @@ def command_wizard(args: ArgsProtocol) -> int | None:
|
|||||||
def command_config(args: ArgsProtocol, config: ConfigType) -> int | None:
|
def command_config(args: ArgsProtocol, config: ConfigType) -> int | None:
|
||||||
from esphome import yaml_util
|
from esphome import yaml_util
|
||||||
|
|
||||||
if not CORE.verbose:
|
if getattr(args, "no_defaults", False):
|
||||||
|
user_config = getattr(config, "user_config", None)
|
||||||
|
if user_config is None:
|
||||||
|
_LOGGER.warning(
|
||||||
|
"--no-defaults requested but the user-only config snapshot is "
|
||||||
|
"unavailable; falling back to the validated configuration."
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
config = user_config
|
||||||
|
elif not CORE.verbose:
|
||||||
config = strip_default_ids(config)
|
config = strip_default_ids(config)
|
||||||
output = yaml_util.dump(config, args.show_secrets)
|
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:
|
if not args.show_secrets:
|
||||||
output = re.sub(
|
output = _redact_with_legacy_fallback(output)
|
||||||
r"(password|key|psk|ssid)\: (.+)", r"\1: \\033[8m\2\\033[28m", output
|
|
||||||
)
|
|
||||||
if not CORE.quiet:
|
if not CORE.quiet:
|
||||||
safe_print(output)
|
safe_print(output)
|
||||||
_LOGGER.info("Configuration is valid!")
|
_LOGGER.info("Configuration is valid!")
|
||||||
return 0
|
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:
|
def command_config_hash(args: ArgsProtocol, config: ConfigType) -> int | None:
|
||||||
# generating code might modify config, so it must be done in order to generate
|
# 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
|
# a hash that will match what was generated when compiling and then running
|
||||||
@@ -1587,7 +1645,7 @@ def command_clean(args: ArgsProtocol, config: ConfigType) -> int | None:
|
|||||||
from esphome import writer
|
from esphome import writer
|
||||||
|
|
||||||
try:
|
try:
|
||||||
writer.clean_build()
|
writer.clean_build(full=True)
|
||||||
except OSError as err:
|
except OSError as err:
|
||||||
_LOGGER.error("Error deleting build files: %s", err)
|
_LOGGER.error("Error deleting build files: %s", err)
|
||||||
return 1
|
return 1
|
||||||
@@ -1800,7 +1858,7 @@ def command_analyze_memory(args: ArgsProtocol, config: ConfigType) -> int:
|
|||||||
ram_report = ram_analyzer.generate_report()
|
ram_report = ram_analyzer.generate_report()
|
||||||
print()
|
print()
|
||||||
print(ram_report)
|
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)
|
_LOGGER.warning("RAM strings analysis failed: %s", e)
|
||||||
|
|
||||||
return 0
|
return 0
|
||||||
@@ -1988,6 +2046,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):
|
def parse_args(argv):
|
||||||
options_parser = argparse.ArgumentParser(add_help=False)
|
options_parser = argparse.ArgumentParser(add_help=False)
|
||||||
options_parser.add_argument(
|
options_parser.add_argument(
|
||||||
@@ -2080,6 +2161,12 @@ def parse_args(argv):
|
|||||||
parser_config.add_argument(
|
parser_config.add_argument(
|
||||||
"--show-secrets", help="Show secrets in output.", action="store_true"
|
"--show-secrets", help="Show secrets in output.", action="store_true"
|
||||||
)
|
)
|
||||||
|
parser_config.add_argument(
|
||||||
|
"--no-defaults",
|
||||||
|
help="Only output the user-supplied configuration without "
|
||||||
|
"schema defaults applied.",
|
||||||
|
action="store_true",
|
||||||
|
)
|
||||||
|
|
||||||
parser_config_hash = subparsers.add_parser(
|
parser_config_hash = subparsers.add_parser(
|
||||||
"config-hash", help="Calculate the hash of the configuration."
|
"config-hash", help="Calculate the hash of the configuration."
|
||||||
@@ -2164,11 +2251,7 @@ def parse_args(argv):
|
|||||||
help="Reset the device before starting serial logs.",
|
help="Reset the device before starting serial logs.",
|
||||||
default=os.getenv("ESPHOME_SERIAL_LOGGING_RESET"),
|
default=os.getenv("ESPHOME_SERIAL_LOGGING_RESET"),
|
||||||
)
|
)
|
||||||
parser_logs.add_argument(
|
_add_states_args(parser_logs)
|
||||||
"--no-states",
|
|
||||||
action="store_true",
|
|
||||||
help="Do not show entity state changes in log output.",
|
|
||||||
)
|
|
||||||
|
|
||||||
parser_discover = subparsers.add_parser(
|
parser_discover = subparsers.add_parser(
|
||||||
"discover",
|
"discover",
|
||||||
@@ -2200,11 +2283,7 @@ def parse_args(argv):
|
|||||||
"--no-logs", help="Disable starting logs.", action="store_true"
|
"--no-logs", help="Disable starting logs.", action="store_true"
|
||||||
)
|
)
|
||||||
|
|
||||||
parser_run.add_argument(
|
_add_states_args(parser_run)
|
||||||
"--no-states",
|
|
||||||
action="store_true",
|
|
||||||
help="Do not show entity state changes in log output.",
|
|
||||||
)
|
|
||||||
|
|
||||||
parser_run.add_argument(
|
parser_run.add_argument(
|
||||||
"--reset",
|
"--reset",
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ from collections import defaultdict
|
|||||||
from collections.abc import Callable
|
from collections.abc import Callable
|
||||||
import heapq
|
import heapq
|
||||||
from operator import itemgetter
|
from operator import itemgetter
|
||||||
|
from pathlib import Path
|
||||||
import sys
|
import sys
|
||||||
from typing import TYPE_CHECKING
|
from typing import TYPE_CHECKING
|
||||||
|
|
||||||
@@ -509,7 +510,7 @@ class MemoryAnalyzerCLI(MemoryAnalyzer):
|
|||||||
lines.append(
|
lines.append(
|
||||||
f"{_COMPONENT_CORE} Symbols > {self.SYMBOL_SIZE_THRESHOLD} B ({len(large_core_symbols)} symbols):"
|
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,
|
# Core symbols only track (symbol, demangled, size) without section info,
|
||||||
# so we don't show section labels here
|
# so we don't show section labels here
|
||||||
lines.append(
|
lines.append(
|
||||||
@@ -601,7 +602,7 @@ class MemoryAnalyzerCLI(MemoryAnalyzer):
|
|||||||
lines.append(
|
lines.append(
|
||||||
f"{comp_name} Symbols > {self.SYMBOL_SIZE_THRESHOLD} B & storage ({len(large_symbols)} symbols):"
|
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(
|
lines.append(
|
||||||
f"{i + 1}. {self._format_symbol_with_section(demangled, size, section)}"
|
f"{i + 1}. {self._format_symbol_with_section(demangled, size, section)}"
|
||||||
)
|
)
|
||||||
@@ -640,7 +641,7 @@ class MemoryAnalyzerCLI(MemoryAnalyzer):
|
|||||||
lines.append(
|
lines.append(
|
||||||
f" Symbols > {self.RAM_SYMBOL_SIZE_THRESHOLD} B ({len(large_ram_syms)}):"
|
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
|
# Format section label consistently by stripping leading dot
|
||||||
section_label = section.lstrip(".") if section else ""
|
section_label = section.lstrip(".") if section else ""
|
||||||
display_name = _format_pstorage_name(demangled)
|
display_name = _format_pstorage_name(demangled)
|
||||||
@@ -699,7 +700,7 @@ class MemoryAnalyzerCLI(MemoryAnalyzer):
|
|||||||
content = "\n".join(lines)
|
content = "\n".join(lines)
|
||||||
|
|
||||||
if output_file:
|
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)
|
f.write(content)
|
||||||
else:
|
else:
|
||||||
print(content)
|
print(content)
|
||||||
@@ -737,7 +738,6 @@ def main():
|
|||||||
|
|
||||||
# Load build directory
|
# Load build directory
|
||||||
import json
|
import json
|
||||||
from pathlib import Path
|
|
||||||
|
|
||||||
from esphome.platformio.toolchain import IDEData
|
from esphome.platformio.toolchain import IDEData
|
||||||
|
|
||||||
@@ -785,7 +785,7 @@ def main():
|
|||||||
if not idedata_path.exists():
|
if not idedata_path.exists():
|
||||||
continue
|
continue
|
||||||
try:
|
try:
|
||||||
with open(idedata_path, encoding="utf-8") as f:
|
with idedata_path.open(encoding="utf-8") as f:
|
||||||
raw_data = json.load(f)
|
raw_data = json.load(f)
|
||||||
idedata = IDEData(raw_data)
|
idedata = IDEData(raw_data)
|
||||||
print(f"Loaded idedata from: {idedata_path}", file=sys.stderr)
|
print(f"Loaded idedata from: {idedata_path}", file=sys.stderr)
|
||||||
|
|||||||
@@ -154,7 +154,7 @@ def batch_demangle(
|
|||||||
failed_count = 0
|
failed_count = 0
|
||||||
|
|
||||||
for original, stripped, prefix, demangled in zip(
|
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
|
# Add back any prefix that was removed
|
||||||
demangled = _restore_symbol_prefix(prefix, stripped, demangled)
|
demangled = _restore_symbol_prefix(prefix, stripped, demangled)
|
||||||
|
|||||||
@@ -3,7 +3,6 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
import os
|
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
import subprocess
|
import subprocess
|
||||||
from typing import TYPE_CHECKING
|
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
|
Full path to the tool or None if not found
|
||||||
"""
|
"""
|
||||||
# Get PlatformIO packages directory
|
# Get PlatformIO packages directory
|
||||||
platformio_home = Path(os.path.expanduser("~/.platformio/packages"))
|
platformio_home = Path("~/.platformio/packages").expanduser()
|
||||||
if not platformio_home.exists():
|
if not platformio_home.exists():
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|||||||
@@ -45,7 +45,7 @@ class AsyncThreadRunner(threading.Thread, Generic[_T]):
|
|||||||
async def _runner(self) -> None:
|
async def _runner(self) -> None:
|
||||||
try:
|
try:
|
||||||
self.result = await self._coro_factory()
|
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
|
# Capture all exceptions so ``event`` is always set — otherwise a
|
||||||
# crash would hang the waiter forever.
|
# crash would hang the waiter forever.
|
||||||
self.exception = exc
|
self.exception = exc
|
||||||
|
|||||||
@@ -7,7 +7,17 @@ from esphome.components.esp32 import get_esp32_variant, idf_version
|
|||||||
import esphome.config_validation as cv
|
import esphome.config_validation as cv
|
||||||
from esphome.core import CORE
|
from esphome.core import CORE
|
||||||
from esphome.helpers import mkdir_p, write_file_if_changed
|
from esphome.helpers import mkdir_p, write_file_if_changed
|
||||||
from esphome.writer import update_storage_json
|
|
||||||
|
# Replaces the IDF default C++ standard (-std=gnu++2b appended to
|
||||||
|
# CXX_COMPILE_OPTIONS by project.cmake's __build_init) with the one set via
|
||||||
|
# cg.set_cpp_standard(). Emitted between include(project.cmake) and project(),
|
||||||
|
# i.e. after IDF appends its default and before the options are consumed, and
|
||||||
|
# applies project-wide like PlatformIO build_unflags.
|
||||||
|
CPP_STANDARD_TEMPLATE = """\
|
||||||
|
idf_build_get_property(esphome_cxx_compile_options CXX_COMPILE_OPTIONS)
|
||||||
|
list(FILTER esphome_cxx_compile_options EXCLUDE REGEX "^-std=")
|
||||||
|
list(APPEND esphome_cxx_compile_options "-std={standard}")
|
||||||
|
idf_build_set_property(CXX_COMPILE_OPTIONS "${{esphome_cxx_compile_options}}")"""
|
||||||
|
|
||||||
|
|
||||||
def get_available_components() -> list[str] | None:
|
def get_available_components() -> list[str] | None:
|
||||||
@@ -24,7 +34,7 @@ def get_available_components() -> list[str] | None:
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
try:
|
try:
|
||||||
with open(project_desc, encoding="utf-8") as f:
|
with project_desc.open(encoding="utf-8") as f:
|
||||||
data = json.load(f)
|
data = json.load(f)
|
||||||
|
|
||||||
component_info = data.get("build_component_info", {})
|
component_info = data.get("build_component_info", {})
|
||||||
@@ -85,6 +95,12 @@ def get_project_cmakelists(minimal: bool = False) -> str:
|
|||||||
for flag in project_compile_opts
|
for flag in project_compile_opts
|
||||||
)
|
)
|
||||||
|
|
||||||
|
cpp_standard_options = (
|
||||||
|
CPP_STANDARD_TEMPLATE.format(standard=CORE.cpp_standard)
|
||||||
|
if CORE.cpp_standard
|
||||||
|
else ""
|
||||||
|
)
|
||||||
|
|
||||||
# Per-project list exposed as a CMake variable so converted PIO libs
|
# Per-project list exposed as a CMake variable so converted PIO libs
|
||||||
# can reference ${ESPHOME_PROJECT_MANAGED_COMPONENTS} without baking
|
# can reference ${ESPHOME_PROJECT_MANAGED_COMPONENTS} without baking
|
||||||
# project-specific names into their cached CMakeLists.
|
# project-specific names into their cached CMakeLists.
|
||||||
@@ -141,6 +157,8 @@ set(EXTRA_COMPONENT_DIRS ${{CMAKE_SOURCE_DIR}}/src)
|
|||||||
|
|
||||||
include($ENV{{IDF_PATH}}/tools/cmake/project.cmake)
|
include($ENV{{IDF_PATH}}/tools/cmake/project.cmake)
|
||||||
|
|
||||||
|
{cpp_standard_options}
|
||||||
|
|
||||||
{extra_compile_options}
|
{extra_compile_options}
|
||||||
|
|
||||||
{managed_components_property}
|
{managed_components_property}
|
||||||
@@ -201,9 +219,6 @@ idf_component_register(
|
|||||||
REQUIRES ${{ESPHOME_PROJECT_BUILTIN_COMPONENTS}}
|
REQUIRES ${{ESPHOME_PROJECT_BUILTIN_COMPONENTS}}
|
||||||
)
|
)
|
||||||
|
|
||||||
# Apply C++ standard
|
|
||||||
target_compile_features(${{COMPONENT_LIB}} PUBLIC cxx_std_20)
|
|
||||||
|
|
||||||
# ESPHome linker options
|
# ESPHome linker options
|
||||||
target_link_options(${{COMPONENT_LIB}} PUBLIC
|
target_link_options(${{COMPONENT_LIB}} PUBLIC
|
||||||
{link_opts_str}
|
{link_opts_str}
|
||||||
@@ -213,11 +228,6 @@ target_link_options(${{COMPONENT_LIB}} PUBLIC
|
|||||||
|
|
||||||
def write_project(minimal: bool = False) -> None:
|
def write_project(minimal: bool = False) -> None:
|
||||||
"""Write ESP-IDF project files."""
|
"""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.build_path)
|
||||||
mkdir_p(CORE.relative_src_path())
|
mkdir_p(CORE.relative_src_path())
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
from esphome.const import __version__
|
from esphome.const import __version__
|
||||||
from esphome.core import CORE
|
from esphome.core import CORE
|
||||||
from esphome.helpers import mkdir_p, read_file, write_file_if_changed
|
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_BEGIN = "; ========== AUTO GENERATED CODE BEGIN ==========="
|
||||||
INI_AUTO_GENERATE_END = "; =========== AUTO GENERATED CODE END ============"
|
INI_AUTO_GENERATE_END = "; =========== AUTO GENERATED CODE END ============"
|
||||||
@@ -33,12 +33,27 @@ def format_ini(data: dict[str, str | list[str]]) -> str:
|
|||||||
return content
|
return content
|
||||||
|
|
||||||
|
|
||||||
|
# All -std= variants a platform/framework may set by default, in both the GNU
|
||||||
|
# and strict dialects; unflagged so the cg.set_cpp_standard() value is the
|
||||||
|
# only standard left in the build.
|
||||||
|
CPP_STD_VARIANTS = [
|
||||||
|
f"{prefix}{year}"
|
||||||
|
for year in ("11", "14", "17", "20", "23", "26", "2a", "2b", "2c")
|
||||||
|
for prefix in ("gnu++", "c++")
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
def get_ini_content():
|
def get_ini_content():
|
||||||
CORE.add_platformio_option(
|
CORE.add_platformio_option(
|
||||||
"lib_deps",
|
"lib_deps",
|
||||||
[x.as_lib_dep for x in CORE.platformio_libraries.values()]
|
[x.as_lib_dep for x in CORE.platformio_libraries.values()]
|
||||||
+ ["${common.lib_deps}"],
|
+ ["${common.lib_deps}"],
|
||||||
)
|
)
|
||||||
|
if CORE.cpp_standard:
|
||||||
|
for variant in CPP_STD_VARIANTS:
|
||||||
|
if variant != CORE.cpp_standard:
|
||||||
|
CORE.add_build_unflag(f"-std={variant}")
|
||||||
|
CORE.add_build_flag(f"-std={CORE.cpp_standard}")
|
||||||
# Sort to avoid changing build flags order
|
# Sort to avoid changing build flags order
|
||||||
CORE.add_platformio_option("build_flags", sorted(CORE.build_flags))
|
CORE.add_platformio_option("build_flags", sorted(CORE.build_flags))
|
||||||
|
|
||||||
@@ -58,7 +73,6 @@ def get_ini_content():
|
|||||||
|
|
||||||
|
|
||||||
def write_ini(content):
|
def write_ini(content):
|
||||||
update_storage_json()
|
|
||||||
path = CORE.relative_build_path("platformio.ini")
|
path = CORE.relative_build_path("platformio.ini")
|
||||||
|
|
||||||
if path.is_file():
|
if path.is_file():
|
||||||
|
|||||||
@@ -412,7 +412,7 @@ class ConfigBundleCreator:
|
|||||||
@staticmethod
|
@staticmethod
|
||||||
def _add_to_tar(tar: tarfile.TarFile, bf: BundleFile) -> None:
|
def _add_to_tar(tar: tarfile.TarFile, bf: BundleFile) -> None:
|
||||||
"""Add a BundleFile to the tar archive with deterministic metadata."""
|
"""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())
|
_add_bytes_to_tar(tar, bf.path, f.read())
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -43,7 +43,7 @@ def save_compiled_config(config: ConfigType) -> None:
|
|||||||
try:
|
try:
|
||||||
rendered = yaml_util.dump(config, show_secrets=True)
|
rendered = yaml_util.dump(config, show_secrets=True)
|
||||||
write_file(compiled_config_path(CORE.config_filename), rendered, private=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)
|
_LOGGER.debug("Skipping compiled config cache write: %s", err)
|
||||||
|
|
||||||
|
|
||||||
@@ -62,7 +62,7 @@ def load_compiled_config(conf_path: Path) -> ConfigType | None:
|
|||||||
|
|
||||||
try:
|
try:
|
||||||
config = yaml_util.load_yaml(cache_path, clear_secrets=False)
|
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
|
return None
|
||||||
|
|
||||||
storage = StorageJSON.load(ext_storage_path(conf_path.name))
|
storage = StorageJSON.load(ext_storage_path(conf_path.name))
|
||||||
|
|||||||
@@ -1 +0,0 @@
|
|||||||
CODEOWNERS = ["@kpfleming"]
|
|
||||||
|
|||||||
@@ -87,14 +87,24 @@ void ADE7880::update_sensor_from_s16_register16_(sensor::Sensor *sensor, uint16_
|
|||||||
sensor->publish_state(f(val));
|
sensor->publish_state(f(val));
|
||||||
}
|
}
|
||||||
|
|
||||||
template<typename F>
|
void ADE7880::update_active_energy_(PowerChannel *channel, uint16_t a_register) {
|
||||||
void ADE7880::update_sensor_from_s32_register16_(sensor::Sensor *sensor, uint16_t a_register, F &&f) {
|
if (channel->forward_active_energy == nullptr && channel->reverse_active_energy == nullptr) {
|
||||||
if (sensor == nullptr) {
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
float val = this->read_s32_register16_(a_register);
|
// The ADE7880 has no separate forward/reverse active energy accumulators. The xWATTHR registers
|
||||||
sensor->publish_state(f(val));
|
// accumulate signed energy since the last read (positive = imported/forward, negative = exported/
|
||||||
|
// reverse), so split the value by sign into the forward and reverse running totals.
|
||||||
|
float val = this->read_s32_register16_(a_register) / 14400.0f;
|
||||||
|
if (val >= 0.0f) {
|
||||||
|
if (channel->forward_active_energy != nullptr) {
|
||||||
|
channel->forward_active_energy->publish_state(channel->forward_active_energy_total += val);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (channel->reverse_active_energy != nullptr) {
|
||||||
|
channel->reverse_active_energy->publish_state(channel->reverse_active_energy_total -= val);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void ADE7880::update() {
|
void ADE7880::update() {
|
||||||
@@ -117,12 +127,7 @@ void ADE7880::update() {
|
|||||||
this->update_sensor_from_s24zp_register16_(chan->apparent_power, AVA, [](float val) { return val / 100.0f; });
|
this->update_sensor_from_s24zp_register16_(chan->apparent_power, AVA, [](float val) { return val / 100.0f; });
|
||||||
this->update_sensor_from_s16_register16_(chan->power_factor, APF,
|
this->update_sensor_from_s16_register16_(chan->power_factor, APF,
|
||||||
[](float val) { return std::abs(val / -327.68f); });
|
[](float val) { return std::abs(val / -327.68f); });
|
||||||
this->update_sensor_from_s32_register16_(chan->forward_active_energy, AFWATTHR, [&chan](float val) {
|
this->update_active_energy_(chan, AWATTHR);
|
||||||
return chan->forward_active_energy_total += val / 14400.0f;
|
|
||||||
});
|
|
||||||
this->update_sensor_from_s32_register16_(chan->reverse_active_energy, ARWATTHR, [&chan](float val) {
|
|
||||||
return chan->reverse_active_energy_total += val / 14400.0f;
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this->channel_b_ != nullptr) {
|
if (this->channel_b_ != nullptr) {
|
||||||
@@ -133,12 +138,7 @@ void ADE7880::update() {
|
|||||||
this->update_sensor_from_s24zp_register16_(chan->apparent_power, BVA, [](float val) { return val / 100.0f; });
|
this->update_sensor_from_s24zp_register16_(chan->apparent_power, BVA, [](float val) { return val / 100.0f; });
|
||||||
this->update_sensor_from_s16_register16_(chan->power_factor, BPF,
|
this->update_sensor_from_s16_register16_(chan->power_factor, BPF,
|
||||||
[](float val) { return std::abs(val / -327.68f); });
|
[](float val) { return std::abs(val / -327.68f); });
|
||||||
this->update_sensor_from_s32_register16_(chan->forward_active_energy, BFWATTHR, [&chan](float val) {
|
this->update_active_energy_(chan, BWATTHR);
|
||||||
return chan->forward_active_energy_total += val / 14400.0f;
|
|
||||||
});
|
|
||||||
this->update_sensor_from_s32_register16_(chan->reverse_active_energy, BRWATTHR, [&chan](float val) {
|
|
||||||
return chan->reverse_active_energy_total += val / 14400.0f;
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this->channel_c_ != nullptr) {
|
if (this->channel_c_ != nullptr) {
|
||||||
@@ -149,12 +149,7 @@ void ADE7880::update() {
|
|||||||
this->update_sensor_from_s24zp_register16_(chan->apparent_power, CVA, [](float val) { return val / 100.0f; });
|
this->update_sensor_from_s24zp_register16_(chan->apparent_power, CVA, [](float val) { return val / 100.0f; });
|
||||||
this->update_sensor_from_s16_register16_(chan->power_factor, CPF,
|
this->update_sensor_from_s16_register16_(chan->power_factor, CPF,
|
||||||
[](float val) { return std::abs(val / -327.68f); });
|
[](float val) { return std::abs(val / -327.68f); });
|
||||||
this->update_sensor_from_s32_register16_(chan->forward_active_energy, CFWATTHR, [&chan](float val) {
|
this->update_active_energy_(chan, CWATTHR);
|
||||||
return chan->forward_active_energy_total += val / 14400.0f;
|
|
||||||
});
|
|
||||||
this->update_sensor_from_s32_register16_(chan->reverse_active_energy, CRWATTHR, [&chan](float val) {
|
|
||||||
return chan->reverse_active_energy_total += val / 14400.0f;
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
ESP_LOGD(TAG, "update took %" PRIu32 " ms", millis() - start);
|
ESP_LOGD(TAG, "update took %" PRIu32 " ms", millis() - start);
|
||||||
|
|||||||
@@ -105,7 +105,8 @@ class ADE7880 : public i2c::I2CDevice, public PollingComponent {
|
|||||||
// the callable will be passed a 'float' value and is expected to return a 'float'
|
// the callable will be passed a 'float' value and is expected to return a 'float'
|
||||||
template<typename F> void update_sensor_from_s24zp_register16_(sensor::Sensor *sensor, uint16_t a_register, F &&f);
|
template<typename F> void update_sensor_from_s24zp_register16_(sensor::Sensor *sensor, uint16_t a_register, F &&f);
|
||||||
template<typename F> void update_sensor_from_s16_register16_(sensor::Sensor *sensor, uint16_t a_register, F &&f);
|
template<typename F> void update_sensor_from_s16_register16_(sensor::Sensor *sensor, uint16_t a_register, F &&f);
|
||||||
template<typename F> void update_sensor_from_s32_register16_(sensor::Sensor *sensor, uint16_t a_register, F &&f);
|
|
||||||
|
void update_active_energy_(PowerChannel *channel, uint16_t a_register);
|
||||||
|
|
||||||
void reset_device_();
|
void reset_device_();
|
||||||
|
|
||||||
|
|||||||
@@ -84,9 +84,7 @@ constexpr uint16_t CWATTHR = 0xE402;
|
|||||||
constexpr uint16_t AFWATTHR = 0xE403;
|
constexpr uint16_t AFWATTHR = 0xE403;
|
||||||
constexpr uint16_t BFWATTHR = 0xE404;
|
constexpr uint16_t BFWATTHR = 0xE404;
|
||||||
constexpr uint16_t CFWATTHR = 0xE405;
|
constexpr uint16_t CFWATTHR = 0xE405;
|
||||||
constexpr uint16_t ARWATTHR = 0xE406;
|
// 0xE406-0xE408 are reserved on the ADE7880 (it does not implement total reactive energy accumulation)
|
||||||
constexpr uint16_t BRWATTHR = 0xE407;
|
|
||||||
constexpr uint16_t CRWATTHR = 0xE408;
|
|
||||||
constexpr uint16_t AFVARHR = 0xE409;
|
constexpr uint16_t AFVARHR = 0xE409;
|
||||||
constexpr uint16_t BFVARHR = 0xE40A;
|
constexpr uint16_t BFVARHR = 0xE40A;
|
||||||
constexpr uint16_t CFVARHR = 0xE40B;
|
constexpr uint16_t CFVARHR = 0xE40B;
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ from esphome.const import (
|
|||||||
UNIT_VOLT,
|
UNIT_VOLT,
|
||||||
)
|
)
|
||||||
|
|
||||||
CODEOWNERS = ["@ncareau", "@jeromelaban", "@kpfleming"]
|
CODEOWNERS = ["@ncareau", "@jeromelaban"]
|
||||||
|
|
||||||
DEPENDENCIES = ["ble_client"]
|
DEPENDENCIES = ["ble_client"]
|
||||||
|
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import logging
|
|||||||
|
|
||||||
from esphome import automation
|
from esphome import automation
|
||||||
import esphome.codegen as cg
|
import esphome.codegen as cg
|
||||||
|
from esphome.components.const import CONF_LOOP
|
||||||
import esphome.components.image as espImage
|
import esphome.components.image as espImage
|
||||||
import esphome.config_validation as cv
|
import esphome.config_validation as cv
|
||||||
from esphome.const import CONF_ID, CONF_REPEAT
|
from esphome.const import CONF_ID, CONF_REPEAT
|
||||||
@@ -14,7 +15,6 @@ DEPENDENCIES = ["display"]
|
|||||||
MULTI_CONF = True
|
MULTI_CONF = True
|
||||||
MULTI_CONF_NO_DEFAULT = True
|
MULTI_CONF_NO_DEFAULT = True
|
||||||
|
|
||||||
CONF_LOOP = "loop"
|
|
||||||
CONF_START_FRAME = "start_frame"
|
CONF_START_FRAME = "start_frame"
|
||||||
CONF_END_FRAME = "end_frame"
|
CONF_END_FRAME = "end_frame"
|
||||||
CONF_FRAME = "frame"
|
CONF_FRAME = "frame"
|
||||||
|
|||||||
@@ -234,7 +234,7 @@ ACTIONS_SCHEMA = automation.validate_automation(
|
|||||||
|
|
||||||
ENCRYPTION_SCHEMA = cv.Schema(
|
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) {
|
void APIConnection::on_get_time_response(const GetTimeResponse &value) {
|
||||||
if (homeassistant::global_homeassistant_time != nullptr) {
|
if (homeassistant::global_homeassistant_time != nullptr) {
|
||||||
homeassistant::global_homeassistant_time->set_epoch_time(value.epoch_seconds);
|
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()) {
|
if (!value.timezone.empty()) {
|
||||||
// Check if the sender provided pre-parsed timezone data.
|
// 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.
|
// 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) {
|
bool APIConnection::send_voice_assistant_get_configuration_response_(const VoiceAssistantConfigurationRequest &msg) {
|
||||||
VoiceAssistantConfigurationResponse resp;
|
VoiceAssistantConfigurationResponse resp;
|
||||||
if (!this->check_voice_assistant_api_connection_()) {
|
if (!this->check_voice_assistant_api_connection_()) {
|
||||||
|
// 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);
|
return this->send_message(resp);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -186,8 +186,12 @@ void APIServer::remove_client_(uint8_t client_index) {
|
|||||||
if (client_index < last_index) {
|
if (client_index < last_index) {
|
||||||
std::swap(this->clients_[client_index], this->clients_[last_index]);
|
std::swap(this->clients_[client_index], this->clients_[last_index]);
|
||||||
}
|
}
|
||||||
this->clients_[last_index].reset();
|
// Drop the count before resetting the slot. reset() runs ~APIConnection(), which can reenter the
|
||||||
|
// server (e.g. voice_assistant unsubscribes in its disconnect trigger, publishing entity state ->
|
||||||
|
// on_*_update iterating active_clients()). Excluding the dying slot from the active range first
|
||||||
|
// keeps that reentrant iteration from dereferencing the now-null slot.
|
||||||
this->api_connection_count_--;
|
this->api_connection_count_--;
|
||||||
|
this->clients_[last_index].reset();
|
||||||
|
|
||||||
// Last client disconnected - set warning and start tracking for reboot timeout
|
// Last client disconnected - set warning and start tracking for reboot timeout
|
||||||
if (this->api_connection_count_ == 0 && this->reboot_timeout_ != 0) {
|
if (this->api_connection_count_ == 0 && this->reboot_timeout_ != 0) {
|
||||||
|
|||||||
@@ -101,13 +101,14 @@ async def async_run_logs(
|
|||||||
client_info=f"ESPHome Logs {__version__}",
|
client_info=f"ESPHome Logs {__version__}",
|
||||||
noise_psk=noise_psk,
|
noise_psk=noise_psk,
|
||||||
addresses=addresses, # Pass all addresses for automatic retry
|
addresses=addresses, # Pass all addresses for automatic retry
|
||||||
|
provide_time=False,
|
||||||
)
|
)
|
||||||
|
|
||||||
# Try platform-specific stacktrace handler first, fall back to generic
|
# Try platform-specific stacktrace handler first, fall back to generic
|
||||||
platform_process_stacktrace = None
|
platform_process_stacktrace = None
|
||||||
try:
|
try:
|
||||||
module = importlib.import_module("esphome.components." + CORE.target_platform)
|
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):
|
except (AttributeError, ImportError):
|
||||||
_LOGGER.info(
|
_LOGGER.info(
|
||||||
'Stacktrace analysis is unavailable: no compatible analyzer found for target platform "%s".',
|
'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:
|
def on_log(msg: SubscribeLogsResponse) -> None:
|
||||||
"""Handle a new log message."""
|
"""Handle a new log message."""
|
||||||
time_ = datetime.now()
|
time_ = datetime.now().astimezone()
|
||||||
message: bytes = msg.message
|
message: bytes = msg.message
|
||||||
text = message.decode("utf8", "backslashreplace")
|
text = message.decode("utf8", "backslashreplace")
|
||||||
nanoseconds = time_.microsecond // 1000
|
nanoseconds = time_.microsecond // 1000
|
||||||
|
|||||||
@@ -100,7 +100,7 @@ def position(min=-MAX_POSITION, max=MAX_POSITION):
|
|||||||
if isinstance(value, str) and value.endswith("%"):
|
if isinstance(value, str) and value.endswith("%"):
|
||||||
value = percent_to_position(value)
|
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(
|
return angle_to_position(
|
||||||
value,
|
value,
|
||||||
min=round(min * POSITION_TO_ANGLE),
|
min=round(min * POSITION_TO_ANGLE),
|
||||||
|
|||||||
@@ -335,7 +335,7 @@ async def to_code(config):
|
|||||||
|
|
||||||
add_idf_component(
|
add_idf_component(
|
||||||
name="esphome/esp-audio-libs",
|
name="esphome/esp-audio-libs",
|
||||||
ref="3.1.0",
|
ref="3.2.1",
|
||||||
)
|
)
|
||||||
|
|
||||||
data = _get_data()
|
data = _get_data()
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#include "esphome/core/defines.h"
|
#include "esphome/core/defines.h"
|
||||||
|
#include "esphome/core/helpers.h" // for ESPDEPRECATED
|
||||||
|
|
||||||
#include <cstddef>
|
#include <cstddef>
|
||||||
#include <cstdint>
|
#include <cstdint>
|
||||||
@@ -143,6 +144,8 @@ AudioFileType detect_audio_file_type(const char *content_type, const char *url);
|
|||||||
/// @param output_buffer Buffer to store the scaled samples
|
/// @param output_buffer Buffer to store the scaled samples
|
||||||
/// @param scale_factor Q15 fixed point scaling factor
|
/// @param scale_factor Q15 fixed point scaling factor
|
||||||
/// @param samples_to_scale Number of samples to scale
|
/// @param samples_to_scale Number of samples to scale
|
||||||
|
// Remove before 2026.12.0
|
||||||
|
ESPDEPRECATED("Use esp_audio_libs::gain::apply() (from <gain.h>) instead. Removed in 2026.12.0.", "2026.6.0")
|
||||||
void scale_audio_samples(const int16_t *audio_samples, int16_t *output_buffer, int16_t scale_factor,
|
void scale_audio_samples(const int16_t *audio_samples, int16_t *output_buffer, int16_t scale_factor,
|
||||||
size_t samples_to_scale);
|
size_t samples_to_scale);
|
||||||
|
|
||||||
|
|||||||
@@ -9,9 +9,12 @@ namespace esphome::audio {
|
|||||||
|
|
||||||
static const char *const TAG = "audio.decoder";
|
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
|
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;
|
static const uint32_t MAX_POTENTIALLY_FAILED_COUNT = 10;
|
||||||
|
|
||||||
AudioDecoder::AudioDecoder(size_t input_buffer_size, size_t output_buffer_size)
|
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) {
|
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) {
|
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);
|
this->input_buffer_ = std::move(source);
|
||||||
return ESP_OK;
|
return ESP_OK;
|
||||||
}
|
}
|
||||||
@@ -141,13 +146,7 @@ AudioDecoderState AudioDecoder::decode(bool stop_gracefully) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
FileDecoderState state = FileDecoderState::MORE_TO_PROCESS;
|
FileDecoderState state = FileDecoderState::MORE_TO_PROCESS;
|
||||||
|
uint8_t no_output_iterations = 0;
|
||||||
uint32_t decoding_start = millis();
|
|
||||||
|
|
||||||
bool first_loop_iteration = true;
|
|
||||||
|
|
||||||
size_t bytes_processed = 0;
|
|
||||||
size_t bytes_available_before_processing = 0;
|
|
||||||
|
|
||||||
while (state == FileDecoderState::MORE_TO_PROCESS) {
|
while (state == FileDecoderState::MORE_TO_PROCESS) {
|
||||||
// Transfer decoded out
|
// Transfer decoded out
|
||||||
@@ -161,45 +160,39 @@ AudioDecoderState AudioDecoder::decode(bool stop_gracefully) {
|
|||||||
this->playback_ms_ +=
|
this->playback_ms_ +=
|
||||||
this->audio_stream_info_.value().frames_to_milliseconds_with_remainder(&this->accumulated_frames_written_);
|
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 {
|
} else {
|
||||||
// If paused, block to avoid wasting CPU resources
|
// If paused, block to avoid wasting CPU resources
|
||||||
delay(READ_WRITE_TIMEOUT_MS);
|
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_->available() > 0) {
|
||||||
if ((this->output_transfer_buffer_->free() < this->free_buffer_required_) ||
|
// Output transfer buffer indicates backpressure, return so caller can handle other events;
|
||||||
(millis() - decoding_start > DECODING_TIMEOUT_MS)) {
|
// e.g., stop/pause, before trying again
|
||||||
return AudioDecoderState::DECODING;
|
return AudioDecoderState::DECODING;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Decode more audio
|
// 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
|
||||||
// Never shift the input buffer; every decoder buffers internally and consumes only what it processed.
|
// loop without yielding and trip the watchdog. The delay yields allowing other tasks to feed the watchdog and
|
||||||
size_t bytes_read = this->input_buffer_->fill(pdMS_TO_TICKS(READ_WRITE_TIMEOUT_MS), false);
|
// the return keeps stop/pause responsive.
|
||||||
|
if (++no_output_iterations >= MAX_NO_OUTPUT_ITERATIONS) {
|
||||||
if (!first_loop_iteration && (this->input_buffer_->available() < bytes_processed)) {
|
delay(1);
|
||||||
// Less data is available than what was processed in last iteration, so don't attempt to decode.
|
return AudioDecoderState::DECODING;
|
||||||
// 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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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)) {
|
const size_t available_before_decode = this->input_buffer_->available();
|
||||||
// Failed to decode in last attempt and there is no new data
|
|
||||||
|
|
||||||
if ((this->input_buffer_->free() == 0) && first_loop_iteration) {
|
if (available_before_decode == 0) {
|
||||||
// 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) {
|
|
||||||
// No data to decode, attempt to get more data next time
|
// No data to decode, attempt to get more data next time
|
||||||
state = FileDecoderState::IDLE;
|
state = FileDecoderState::IDLE;
|
||||||
} else {
|
} 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) {
|
if (state == FileDecoderState::POTENTIALLY_FAILED) {
|
||||||
++this->potentially_failed_count_;
|
++this->potentially_failed_count_;
|
||||||
} else if (state == FileDecoderState::END_OF_FILE) {
|
} else if (state == FileDecoderState::END_OF_FILE) {
|
||||||
@@ -241,7 +231,16 @@ AudioDecoderState AudioDecoder::decode(bool stop_gracefully) {
|
|||||||
} else if (state == FileDecoderState::FAILED) {
|
} else if (state == FileDecoderState::FAILED) {
|
||||||
return AudioDecoderState::FAILED;
|
return AudioDecoderState::FAILED;
|
||||||
} else if (state == FileDecoderState::MORE_TO_PROCESS) {
|
} 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;
|
return AudioDecoderState::DECODING;
|
||||||
|
|||||||
@@ -61,15 +61,16 @@ class AudioDecoder {
|
|||||||
*/
|
*/
|
||||||
public:
|
public:
|
||||||
/// @brief Allocates the output transfer buffer and stores the input buffer size for later use by add_source()
|
/// @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.
|
/// @param output_buffer_size Size of the output transfer buffer in bytes.
|
||||||
AudioDecoder(size_t input_buffer_size, size_t output_buffer_size);
|
AudioDecoder(size_t input_buffer_size, size_t output_buffer_size);
|
||||||
|
|
||||||
~AudioDecoder() = default;
|
~AudioDecoder() = default;
|
||||||
|
|
||||||
/// @brief Adds a source ring buffer for raw file data. Takes ownership of the ring buffer in a shared_ptr.
|
/// @brief Adds a source ring buffer for raw file data. Shares ownership of the ring buffer via a shared_ptr.
|
||||||
/// @param input_ring_buffer weak_ptr of a shared_ptr of the sink ring buffer to transfer ownership
|
/// The decoder reads directly from the ring buffer's internal storage with a zero-copy RingBufferAudioSource.
|
||||||
/// @return ESP_OK if successsful, ESP_ERR_NO_MEM if the transfer buffer wasn't allocated
|
/// @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);
|
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.
|
/// @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)
|
AudioResampler::AudioResampler(size_t input_buffer_size, size_t output_buffer_size)
|
||||||
: input_buffer_size_(input_buffer_size), output_buffer_size_(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);
|
this->output_transfer_buffer_ = AudioSinkTransferBuffer::create(output_buffer_size);
|
||||||
}
|
}
|
||||||
|
|
||||||
esp_err_t AudioResampler::add_source(std::weak_ptr<ring_buffer::RingBuffer> &input_ring_buffer) {
|
esp_err_t AudioResampler::add_source(std::weak_ptr<ring_buffer::RingBuffer> &input_ring_buffer) {
|
||||||
if (this->input_transfer_buffer_ != nullptr) {
|
// The zero-copy RingBufferAudioSource is created lazily on the first resample() call, once both the ring
|
||||||
this->input_transfer_buffer_->set_source(input_ring_buffer);
|
// buffer (stored here) and the input stream info (set by start()) are available, in either order.
|
||||||
return ESP_OK;
|
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) {
|
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->input_stream_info_ = input_stream_info;
|
||||||
this->output_stream_info_ = output_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;
|
return ESP_ERR_NO_MEM;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -56,6 +57,13 @@ esp_err_t AudioResampler::start(AudioStreamInfo &input_stream_info, AudioStreamI
|
|||||||
return ESP_ERR_NOT_SUPPORTED;
|
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()) ||
|
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())) {
|
(input_stream_info.get_bits_per_sample() != output_stream_info.get_bits_per_sample())) {
|
||||||
this->resampler_ = make_unique<esp_audio_libs::resampler::Resampler>(
|
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) {
|
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 (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;
|
return AudioResamplerState::FINISHED;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -102,9 +129,11 @@ AudioResamplerState AudioResampler::resample(bool stop_gracefully, int32_t *ms_d
|
|||||||
delay(READ_WRITE_TIMEOUT_MS);
|
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
|
// No samples available to process
|
||||||
return AudioResamplerState::RESAMPLING;
|
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 size_t bytes_free = this->output_transfer_buffer_->free();
|
||||||
const uint32_t frames_free = this->output_stream_info_.bytes_to_frames(bytes_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);
|
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()) ||
|
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())) {
|
(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
|
// Adjust gain by -3 dB to avoid clipping due to the resampling process
|
||||||
esp_audio_libs::resampler::ResamplerResults results =
|
esp_audio_libs::resampler::ResamplerResults results =
|
||||||
this->resampler_->resample(this->input_transfer_buffer_->get_buffer_start(),
|
this->resampler_->resample(this->audio_source_->data(), this->output_transfer_buffer_->get_buffer_end(),
|
||||||
this->output_transfer_buffer_->get_buffer_end(), frames_available, frames_free, -3);
|
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_transfer_buffer_->increase_buffer_length(
|
||||||
this->output_stream_info_.frames_to_bytes(results.frames_generated));
|
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),
|
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));
|
this->input_stream_info_.frames_to_bytes(frames_available));
|
||||||
|
|
||||||
std::memcpy((void *) this->output_transfer_buffer_->get_buffer_end(),
|
std::memcpy((void *) this->output_transfer_buffer_->get_buffer_end(), (const void *) this->audio_source_->data(),
|
||||||
(void *) this->input_transfer_buffer_->get_buffer_start(), bytes_to_transfer);
|
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);
|
this->output_transfer_buffer_->increase_buffer_length(bytes_to_transfer);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ namespace esphome::audio {
|
|||||||
enum class AudioResamplerState : uint8_t {
|
enum class AudioResamplerState : uint8_t {
|
||||||
RESAMPLING, // More data is available to resample
|
RESAMPLING, // More data is available to resample
|
||||||
FINISHED, // All file data has been resampled and transferred
|
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 {
|
class AudioResampler {
|
||||||
@@ -32,14 +32,16 @@ class AudioResampler {
|
|||||||
* component). Also supports converting bits per sample.
|
* component). Also supports converting bits per sample.
|
||||||
*/
|
*/
|
||||||
public:
|
public:
|
||||||
/// @brief Allocates the input and output transfer buffers
|
/// @brief Allocates the output transfer buffer. The input source is created later in resample().
|
||||||
/// @param input_buffer_size Size of the input transfer buffer in bytes.
|
/// @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.
|
/// @param output_buffer_size Size of the output transfer buffer in bytes.
|
||||||
AudioResampler(size_t input_buffer_size, size_t output_buffer_size);
|
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.
|
/// @brief Sets the ring buffer the audio is read from and takes shared ownership of it. The zero-copy
|
||||||
/// @param input_ring_buffer weak_ptr of a shared_ptr of the sink ring buffer to transfer ownership
|
/// RingBufferAudioSource that reads directly from its internal storage is created lazily on the first
|
||||||
/// @return ESP_OK if successsful, ESP_ERR_NO_MEM if the transfer buffer wasn't allocated
|
/// 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);
|
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.
|
/// @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; }
|
void set_pause_output_state(bool pause_state) { this->pause_output_ = pause_state; }
|
||||||
|
|
||||||
protected:
|
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_;
|
std::unique_ptr<AudioSinkTransferBuffer> output_transfer_buffer_;
|
||||||
|
|
||||||
size_t input_buffer_size_;
|
size_t input_buffer_size_;
|
||||||
|
|||||||
@@ -72,7 +72,7 @@ def _file_schema(value: ConfigType | str) -> ConfigType:
|
|||||||
|
|
||||||
def _validate_file_shorthand(value: str) -> ConfigType:
|
def _validate_file_shorthand(value: str) -> ConfigType:
|
||||||
value = cv.string_strict(value)
|
value = cv.string_strict(value)
|
||||||
if value.startswith("http://") or value.startswith("https://"):
|
if value.startswith(("http://", "https://")):
|
||||||
return _file_schema(
|
return _file_schema(
|
||||||
{
|
{
|
||||||
CONF_TYPE: TYPE_WEB,
|
CONF_TYPE: TYPE_WEB,
|
||||||
@@ -98,7 +98,7 @@ def read_audio_file_and_type(file_config: ConfigType) -> tuple[bytes, MockObj]:
|
|||||||
else:
|
else:
|
||||||
raise cv.Invalid("Unsupported file source")
|
raise cv.Invalid("Unsupported file source")
|
||||||
|
|
||||||
with open(path, "rb") as f:
|
with path.open("rb") as f:
|
||||||
data = f.read()
|
data = f.read()
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
|||||||
@@ -1,7 +1,5 @@
|
|||||||
from typing import Any
|
|
||||||
|
|
||||||
import esphome.codegen as cg
|
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
|
import esphome.config_validation as cv
|
||||||
from esphome.const import CONF_ID, CONF_TASK_STACK_IN_PSRAM
|
from esphome.const import CONF_ID, CONF_TASK_STACK_IN_PSRAM
|
||||||
from esphome.types import ConfigType
|
from esphome.types import ConfigType
|
||||||
@@ -21,19 +19,13 @@ def _request_micro_decoder(config: ConfigType) -> ConfigType:
|
|||||||
return config
|
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(
|
CONFIG_SCHEMA = cv.All(
|
||||||
media_source.media_source_schema(
|
media_source.media_source_schema(
|
||||||
AudioFileMediaSource,
|
AudioFileMediaSource,
|
||||||
)
|
)
|
||||||
.extend(
|
.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),
|
.extend(cv.COMPONENT_SCHEMA),
|
||||||
@@ -49,6 +41,4 @@ async def to_code(config: ConfigType) -> None:
|
|||||||
|
|
||||||
if config.get(CONF_TASK_STACK_IN_PSRAM):
|
if config.get(CONF_TASK_STACK_IN_PSRAM):
|
||||||
cg.add(var.set_task_stack_in_psram(True))
|
cg.add(var.set_task_stack_in_psram(True))
|
||||||
esp32.add_idf_sdkconfig_option(
|
psram.request_external_task_stack()
|
||||||
"CONFIG_SPIRAM_ALLOW_STACK_EXTERNAL_MEMORY", True
|
|
||||||
)
|
|
||||||
|
|||||||
@@ -1,7 +1,5 @@
|
|||||||
from typing import Any
|
|
||||||
|
|
||||||
import esphome.codegen as cg
|
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
|
import esphome.config_validation as cv
|
||||||
from esphome.const import CONF_BUFFER_SIZE, CONF_ID, CONF_TASK_STACK_IN_PSRAM
|
from esphome.const import CONF_BUFFER_SIZE, CONF_ID, CONF_TASK_STACK_IN_PSRAM
|
||||||
from esphome.types import ConfigType
|
from esphome.types import ConfigType
|
||||||
@@ -20,14 +18,6 @@ def _request_micro_decoder(config: ConfigType) -> ConfigType:
|
|||||||
return config
|
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(
|
CONFIG_SCHEMA = cv.All(
|
||||||
media_source.media_source_schema(
|
media_source.media_source_schema(
|
||||||
AudioHTTPMediaSource,
|
AudioHTTPMediaSource,
|
||||||
@@ -37,7 +27,7 @@ CONFIG_SCHEMA = cv.All(
|
|||||||
cv.Optional(CONF_BUFFER_SIZE, default=50000): cv.int_range(
|
cv.Optional(CONF_BUFFER_SIZE, default=50000): cv.int_range(
|
||||||
min=5000, max=1000000
|
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),
|
.extend(cv.COMPONENT_SCHEMA),
|
||||||
@@ -53,7 +43,5 @@ async def to_code(config: ConfigType) -> None:
|
|||||||
|
|
||||||
if config.get(CONF_TASK_STACK_IN_PSRAM):
|
if config.get(CONF_TASK_STACK_IN_PSRAM):
|
||||||
cg.add(var.set_task_stack_in_psram(True))
|
cg.add(var.set_task_stack_in_psram(True))
|
||||||
esp32.add_idf_sdkconfig_option(
|
psram.request_external_task_stack()
|
||||||
"CONFIG_SPIRAM_ALLOW_STACK_EXTERNAL_MEMORY", True
|
|
||||||
)
|
|
||||||
cg.add(var.set_buffer_size(config[CONF_BUFFER_SIZE]))
|
cg.add(var.set_buffer_size(config[CONF_BUFFER_SIZE]))
|
||||||
|
|||||||
@@ -169,7 +169,7 @@ async def to_code_base(config):
|
|||||||
path = _compute_local_file_path(_compute_url(config))
|
path = _compute_local_file_path(_compute_url(config))
|
||||||
|
|
||||||
try:
|
try:
|
||||||
with open(path, encoding="utf-8") as f:
|
with path.open(encoding="utf-8") as f:
|
||||||
bsec2_iaq_config = f.read()
|
bsec2_iaq_config = f.read()
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
raise core.EsphomeError(
|
raise core.EsphomeError(
|
||||||
|
|||||||
10
esphome/components/bmi270/__init__.py
Normal file
10
esphome/components/bmi270/__init__.py
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
import esphome.codegen as cg
|
||||||
|
from esphome.components import i2c
|
||||||
|
from esphome.components.motion import MotionComponent
|
||||||
|
|
||||||
|
CODEOWNERS = ["@clydebarrow"]
|
||||||
|
|
||||||
|
CONF_BMI270_ID = "bmi270_id"
|
||||||
|
# C++ namespace / class
|
||||||
|
bmi270_ns = cg.esphome_ns.namespace("bmi270")
|
||||||
|
BMI270Component = bmi270_ns.class_("BMI270Component", MotionComponent, i2c.I2CDevice)
|
||||||
209
esphome/components/bmi270/bmi270.cpp
Normal file
209
esphome/components/bmi270/bmi270.cpp
Normal file
@@ -0,0 +1,209 @@
|
|||||||
|
#include "bmi270.h"
|
||||||
|
#include "bmi270_config.h"
|
||||||
|
#include "esphome/core/log.h"
|
||||||
|
#include "esphome/core/hal.h"
|
||||||
|
|
||||||
|
namespace esphome::bmi270 {
|
||||||
|
|
||||||
|
static const char *const TAG = "bmi270";
|
||||||
|
|
||||||
|
#if defined(USE_ARDUINO) && !defined(USE_ESP32)
|
||||||
|
static const size_t MAX_I2C_BUFFER_SIZE = 32;
|
||||||
|
#else
|
||||||
|
static const size_t MAX_I2C_BUFFER_SIZE = 256;
|
||||||
|
#endif
|
||||||
|
|
||||||
|
// Configuration blob upload
|
||||||
|
// The BMI270 requires a firmware config blob to be written to its internal
|
||||||
|
// memory after every power-on before sensors can be used.
|
||||||
|
|
||||||
|
bool BMI270Component::load_config_file_() {
|
||||||
|
// 1. Disable advanced power-save so the config port is accessible
|
||||||
|
if (!this->write_byte(BMI270_REG_PWR_CONF, 0x00))
|
||||||
|
return false;
|
||||||
|
delay(1);
|
||||||
|
|
||||||
|
// 2. Prepare config load: write 0x00 to INIT_CTRL to start
|
||||||
|
if (!this->write_byte(BMI270_REG_INIT_CTRL, 0x00))
|
||||||
|
return false;
|
||||||
|
|
||||||
|
// 3. Burst-write the config in pages
|
||||||
|
const uint8_t *cfg = BMI270_CONFIG_FILE;
|
||||||
|
constexpr size_t cfg_len = sizeof(BMI270_CONFIG_FILE);
|
||||||
|
size_t index = 0;
|
||||||
|
|
||||||
|
while (index != cfg_len) {
|
||||||
|
// Set the page address in INIT_ADDR registers
|
||||||
|
uint8_t addr_lsb = (uint8_t) ((index / 2) & 0x0F);
|
||||||
|
uint8_t addr_msb = (uint8_t) ((index / 2) >> 4);
|
||||||
|
if (!this->write_byte(BMI270_REG_INIT_ADDR_0, addr_lsb))
|
||||||
|
return false;
|
||||||
|
if (!this->write_byte(BMI270_REG_INIT_ADDR_0 + 1, addr_msb))
|
||||||
|
return false;
|
||||||
|
|
||||||
|
// Write a burst of up to the maximum allowed size
|
||||||
|
size_t burst = clamp_at_most(cfg_len - index, MAX_I2C_BUFFER_SIZE);
|
||||||
|
if (this->write_register(BMI270_REG_INIT_DATA, cfg + index, burst) != i2c::ERROR_OK)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
index += burst;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4. Signal end of config load
|
||||||
|
if (!this->write_byte(BMI270_REG_INIT_CTRL, 0x01))
|
||||||
|
return false;
|
||||||
|
delay(20); // spec: wait ≥20 ms for init to complete
|
||||||
|
|
||||||
|
// 5. Check INTERNAL_STATUS: bit[0:3] should be 0x01 ("initialisation OK")
|
||||||
|
uint8_t status = 0;
|
||||||
|
if (!this->read_byte(BMI270_REG_INTERNAL_STATUS, &status))
|
||||||
|
return false;
|
||||||
|
if ((status & 0x0F) != 0x01) {
|
||||||
|
ESP_LOGE(TAG, "Config load failed: INTERNAL_STATUS=0x%02X (expected 0x01)", status);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// setup() ─
|
||||||
|
|
||||||
|
void BMI270Component::setup() {
|
||||||
|
MotionComponent::setup();
|
||||||
|
// 1. Verify chip ID
|
||||||
|
uint8_t chip_id = 0;
|
||||||
|
if (!this->read_byte(BMI270_REG_CHIP_ID, &chip_id)) {
|
||||||
|
ESP_LOGE(TAG, "Failed to read chip ID – check wiring / address");
|
||||||
|
this->mark_failed();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (chip_id != BMI270_CHIP_ID_VALUE) {
|
||||||
|
ESP_LOGE(TAG, "Wrong chip ID: 0x%02X (expected 0x%02X)", chip_id, BMI270_CHIP_ID_VALUE);
|
||||||
|
this->mark_failed();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
ESP_LOGD(TAG, "Chip ID: 0x%02X", chip_id);
|
||||||
|
|
||||||
|
// 2. Soft-reset via CMD register (0x7E = 0xB6)
|
||||||
|
if (!this->write_byte(0x7E, 0xB6)) {
|
||||||
|
this->mark_failed();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
delay(20);
|
||||||
|
|
||||||
|
// 4. Upload the configuration blob
|
||||||
|
if (!load_config_file_()) {
|
||||||
|
ESP_LOGE(TAG, "Config file upload failed");
|
||||||
|
this->mark_failed();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
ESP_LOGD(TAG, "Config blob uploaded ✓");
|
||||||
|
|
||||||
|
// 5. Configure accelerometer
|
||||||
|
// ACC_CONF: ODR | BWP(0x2 = normal avg4) | perf_mode(1)
|
||||||
|
uint8_t acc_conf = (uint8_t) (accel_odr_) | (0x2 << 4) | (1 << 7);
|
||||||
|
if (!this->write_byte(BMI270_REG_ACC_CONF, acc_conf)) {
|
||||||
|
this->mark_failed();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!this->write_byte(BMI270_REG_ACC_RANGE, (uint8_t) accel_range_)) {
|
||||||
|
this->mark_failed();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 6. Configure gyroscope
|
||||||
|
// GYR_CONF: ODR | BWP(0x2 = normal) | noise_perf(1) | filter_perf(1)
|
||||||
|
uint8_t gyr_conf = (uint8_t) (gyro_odr_) | (0x2 << 4) | (1 << 6) | (1 << 7);
|
||||||
|
if (!this->write_byte(BMI270_REG_GYR_CONF, gyr_conf)) {
|
||||||
|
this->mark_failed();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!this->write_byte(BMI270_REG_GYR_RANGE, (uint8_t) gyro_range_)) {
|
||||||
|
this->mark_failed();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 7. Enable accelerometer, gyroscope, and temperature sensor
|
||||||
|
// PWR_CTRL bits: temp_en[3] | gyr_en[2] | acc_en[1]
|
||||||
|
if (!this->write_byte(BMI270_REG_PWR_CTRL, 0x0E)) {
|
||||||
|
this->mark_failed();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
delay(5);
|
||||||
|
|
||||||
|
// 8. Re-enable advanced power save (optional; keeps current low between reads)
|
||||||
|
// Disabled here for simplicity – leave in performance mode
|
||||||
|
if (!this->write_byte(BMI270_REG_PWR_CONF, 0x02)) { // bit1 = fifo_self_wakeup
|
||||||
|
this->mark_failed();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
ESP_LOGCONFIG(TAG, "BMI270 initialised successfully");
|
||||||
|
}
|
||||||
|
|
||||||
|
void BMI270Component::dump_config() {
|
||||||
|
ESP_LOGCONFIG(TAG, "BMI270 IMU:");
|
||||||
|
LOG_I2C_DEVICE(this);
|
||||||
|
if (this->is_failed()) {
|
||||||
|
ESP_LOGE(TAG, " Communication failed!");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
static constexpr const char *const ACCEL_RANGE_STRS[] = {"±2g", "±4g", "±8g", "±16g"};
|
||||||
|
static constexpr const char *const GYRO_RANGE_STRS[] = {"±2000°/s", "±1000°/s", "±500°/s", "±250°/s", "±125°/s"};
|
||||||
|
|
||||||
|
ESP_LOGCONFIG(TAG, " Accel range : %s", ACCEL_RANGE_STRS[accel_range_]);
|
||||||
|
ESP_LOGCONFIG(TAG, " Gyro range : %s", GYRO_RANGE_STRS[gyro_range_]);
|
||||||
|
MotionComponent::dump_config();
|
||||||
|
}
|
||||||
|
|
||||||
|
// update() ─
|
||||||
|
// Reads all 6 axes + temperature in one block
|
||||||
|
|
||||||
|
bool BMI270Component::update_data(motion::MotionData &data) {
|
||||||
|
if (this->is_failed())
|
||||||
|
return false;
|
||||||
|
|
||||||
|
// Accelerometer: registers 0x0C–0x11 (6 bytes: x_lsb, x_msb, y_lsb, y_msb, z_lsb, z_msb)
|
||||||
|
uint8_t raw_data[REG_READ_LEN];
|
||||||
|
if (!this->read_bytes(BMI270_REG_DATA_8, raw_data, REG_READ_LEN)) {
|
||||||
|
ESP_LOGW(TAG, "Failed to read IMU data");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
// Scale factor: LSB/g depends on range
|
||||||
|
// raw is a signed 16-bit value; full-scale = range_g * 2^15 lsb
|
||||||
|
static constexpr float ACCEL_SCALE[] = {
|
||||||
|
2.0f / 32768.0f,
|
||||||
|
4.0f / 32768.0f,
|
||||||
|
8.0f / 32768.0f,
|
||||||
|
16.0f / 32768.0f,
|
||||||
|
};
|
||||||
|
float scale = ACCEL_SCALE[this->accel_range_];
|
||||||
|
|
||||||
|
data.acceleration[motion::X_AXIS] = (int16_t) ((raw_data[1] << 8) | raw_data[0]) * scale;
|
||||||
|
data.acceleration[motion::Y_AXIS] = (int16_t) ((raw_data[3] << 8) | raw_data[2]) * scale;
|
||||||
|
data.acceleration[motion::Z_AXIS] = (int16_t) ((raw_data[5] << 8) | raw_data[4]) * scale;
|
||||||
|
|
||||||
|
// Gyroscope: registers 0x12–0x17 (6 bytes)
|
||||||
|
// Scale: full-scale range / 2^15
|
||||||
|
static constexpr float GYRO_SCALE[] = {
|
||||||
|
2000.0f / 32768.0f, 1000.0f / 32768.0f, 500.0f / 32768.0f, 250.0f / 32768.0f, 125.0f / 32768.0f,
|
||||||
|
};
|
||||||
|
static constexpr uint8_t GYR_OFFS = BMI270_REG_DATA_14 - BMI270_REG_DATA_8;
|
||||||
|
scale = GYRO_SCALE[this->gyro_range_];
|
||||||
|
|
||||||
|
data.angular_rate[motion::X_AXIS] = (int16_t) ((raw_data[GYR_OFFS + 1] << 8) | raw_data[GYR_OFFS + 0]) * scale;
|
||||||
|
data.angular_rate[motion::Y_AXIS] = (int16_t) ((raw_data[GYR_OFFS + 3] << 8) | raw_data[GYR_OFFS + 2]) * scale;
|
||||||
|
data.angular_rate[motion::Z_AXIS] = (int16_t) ((raw_data[GYR_OFFS + 5] << 8) | raw_data[GYR_OFFS + 4]) * scale;
|
||||||
|
|
||||||
|
if (this->temperature_callback_.empty())
|
||||||
|
return true;
|
||||||
|
// Temperature: registers 0x22–0x23
|
||||||
|
// Formula from datasheet: T[°C] = raw / 512 + 23
|
||||||
|
static constexpr uint8_t TEMP_OFFS = BMI270_REG_TEMP_0 - BMI270_REG_DATA_8;
|
||||||
|
int16_t raw_t = (int16_t) ((raw_data[TEMP_OFFS + 1] << 8) | raw_data[TEMP_OFFS + 0]);
|
||||||
|
float temperature = (raw_t / 512.0f) + 23.0f;
|
||||||
|
this->temperature_callback_.call(temperature);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace esphome::bmi270
|
||||||
108
esphome/components/bmi270/bmi270.h
Normal file
108
esphome/components/bmi270/bmi270.h
Normal file
@@ -0,0 +1,108 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "esphome/components/motion/motion_component.h"
|
||||||
|
#include "esphome/core/component.h"
|
||||||
|
#include "esphome/core/helpers.h"
|
||||||
|
#include "esphome/components/i2c/i2c.h"
|
||||||
|
#include <functional>
|
||||||
|
|
||||||
|
namespace esphome::bmi270 {
|
||||||
|
|
||||||
|
// Register map
|
||||||
|
static const uint8_t BMI270_REG_CHIP_ID = 0x00;
|
||||||
|
static const uint8_t BMI270_REG_ERR_REG = 0x02;
|
||||||
|
static const uint8_t BMI270_REG_STATUS = 0x03;
|
||||||
|
static const uint8_t BMI270_REG_DATA_8 = 0x0C; // ACC_X LSB
|
||||||
|
static const uint8_t BMI270_REG_DATA_14 = 0x12; // GYR_X LSB
|
||||||
|
static const uint8_t BMI270_REG_TEMP_0 = 0x22;
|
||||||
|
static const uint8_t BMI270_REG_TEMP_MSB = 0x23; // temperature (2 bytes big-endian ish)
|
||||||
|
|
||||||
|
static constexpr uint8_t REG_READ_LEN =
|
||||||
|
BMI270_REG_TEMP_MSB - BMI270_REG_DATA_8 +
|
||||||
|
1; // 0x23 - 0x0C + 1 = 0x18 bytes total for accel(6) + gyro(6) + temp(2) + padding(4)
|
||||||
|
|
||||||
|
static const uint8_t BMI270_REG_PWR_CONF = 0x7C;
|
||||||
|
static const uint8_t BMI270_REG_PWR_CTRL = 0x7D;
|
||||||
|
static const uint8_t BMI270_REG_INIT_CTRL = 0x59;
|
||||||
|
static const uint8_t BMI270_REG_INIT_DATA = 0x5E;
|
||||||
|
static const uint8_t BMI270_REG_INIT_ADDR_0 = 0x5B;
|
||||||
|
static const uint8_t BMI270_REG_INTERNAL_STATUS = 0x21;
|
||||||
|
static const uint8_t BMI270_REG_ACC_CONF = 0x40;
|
||||||
|
static const uint8_t BMI270_REG_ACC_RANGE = 0x41;
|
||||||
|
static const uint8_t BMI270_REG_GYR_CONF = 0x42;
|
||||||
|
static const uint8_t BMI270_REG_GYR_RANGE = 0x43;
|
||||||
|
|
||||||
|
static const uint8_t BMI270_CHIP_ID_VALUE = 0x24;
|
||||||
|
|
||||||
|
// Accelerometer range options
|
||||||
|
enum BMI270AccelRange : uint8_t {
|
||||||
|
BMI270_ACCEL_RANGE_2G = 0x00,
|
||||||
|
BMI270_ACCEL_RANGE_4G = 0x01,
|
||||||
|
BMI270_ACCEL_RANGE_8G = 0x02,
|
||||||
|
BMI270_ACCEL_RANGE_16G = 0x03,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Accelerometer ODR options
|
||||||
|
enum BMI270AccelODR : uint8_t {
|
||||||
|
BMI270_ACCEL_ODR_12_5 = 0x05,
|
||||||
|
BMI270_ACCEL_ODR_25 = 0x06,
|
||||||
|
BMI270_ACCEL_ODR_50 = 0x07,
|
||||||
|
BMI270_ACCEL_ODR_100 = 0x08,
|
||||||
|
BMI270_ACCEL_ODR_200 = 0x09,
|
||||||
|
BMI270_ACCEL_ODR_400 = 0x0A,
|
||||||
|
BMI270_ACCEL_ODR_800 = 0x0B,
|
||||||
|
BMI270_ACCEL_ODR_1600 = 0x0C,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Gyroscope range options
|
||||||
|
enum BMI270GyroRange : uint8_t {
|
||||||
|
BMI270_GYRO_RANGE_2000 = 0x00,
|
||||||
|
BMI270_GYRO_RANGE_1000 = 0x01,
|
||||||
|
BMI270_GYRO_RANGE_500 = 0x02,
|
||||||
|
BMI270_GYRO_RANGE_250 = 0x03,
|
||||||
|
BMI270_GYRO_RANGE_125 = 0x04,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Gyroscope ODR options
|
||||||
|
enum BMI270GyroODR : uint8_t {
|
||||||
|
BMI270_GYRO_ODR_25 = 0x06,
|
||||||
|
BMI270_GYRO_ODR_50 = 0x07,
|
||||||
|
BMI270_GYRO_ODR_100 = 0x08,
|
||||||
|
BMI270_GYRO_ODR_200 = 0x09,
|
||||||
|
BMI270_GYRO_ODR_400 = 0x0A,
|
||||||
|
BMI270_GYRO_ODR_800 = 0x0B,
|
||||||
|
BMI270_GYRO_ODR_1600 = 0x0C,
|
||||||
|
BMI270_GYRO_ODR_3200 = 0x0D,
|
||||||
|
};
|
||||||
|
|
||||||
|
// ---Data class
|
||||||
|
|
||||||
|
// Main component class
|
||||||
|
class BMI270Component : public motion::MotionComponent, public i2c::I2CDevice {
|
||||||
|
public:
|
||||||
|
// Lifecycle
|
||||||
|
void setup() override;
|
||||||
|
void dump_config() override;
|
||||||
|
float get_setup_priority() const override { return setup_priority::DATA; }
|
||||||
|
|
||||||
|
// Configuration setters
|
||||||
|
void set_accel_range(BMI270AccelRange r) { this->accel_range_ = r; }
|
||||||
|
void set_accel_odr(BMI270AccelODR o) { this->accel_odr_ = o; }
|
||||||
|
void set_gyro_range(BMI270GyroRange r) { this->gyro_range_ = r; }
|
||||||
|
void set_gyro_odr(BMI270GyroODR o) { this->gyro_odr_ = o; }
|
||||||
|
template<typename F> void add_temperature_listener(F &&cb) { this->temperature_callback_.add(std::forward<F>(cb)); }
|
||||||
|
|
||||||
|
protected:
|
||||||
|
bool update_data(motion::MotionData &data) override;
|
||||||
|
bool load_config_file_();
|
||||||
|
|
||||||
|
// Config
|
||||||
|
BMI270AccelRange accel_range_{BMI270_ACCEL_RANGE_4G};
|
||||||
|
BMI270AccelODR accel_odr_{BMI270_ACCEL_ODR_100};
|
||||||
|
BMI270GyroRange gyro_range_{BMI270_GYRO_RANGE_2000};
|
||||||
|
BMI270GyroODR gyro_odr_{BMI270_GYRO_ODR_200};
|
||||||
|
|
||||||
|
LazyCallbackManager<void(float)> temperature_callback_{};
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace esphome::bmi270
|
||||||
483
esphome/components/bmi270/bmi270_config.h
Normal file
483
esphome/components/bmi270/bmi270_config.h
Normal file
@@ -0,0 +1,483 @@
|
|||||||
|
#pragma once
|
||||||
|
#include <cstdint>
|
||||||
|
|
||||||
|
namespace esphome::bmi270 {
|
||||||
|
|
||||||
|
/**
|
||||||
|
BMI270 configuration file (chip ID 0x24, firmware v2.86.1)
|
||||||
|
Source: Bosch Sensortec BMI270_SensorAPI (BSD-3-Clause)
|
||||||
|
https://github.com/boschsensortec/BMI270_SensorAPI
|
||||||
|
|
||||||
|
Copyright (c) 2023 Bosch Sensortec GmbH. All rights reserved.
|
||||||
|
|
||||||
|
BSD-3-Clause
|
||||||
|
|
||||||
|
Redistribution and use in source and binary forms, with or without
|
||||||
|
modification, are permitted provided that the following conditions are met:
|
||||||
|
|
||||||
|
1. Redistributions of source code must retain the above copyright
|
||||||
|
notice, this list of conditions and the following disclaimer.
|
||||||
|
|
||||||
|
2. Redistributions in binary form must reproduce the above copyright
|
||||||
|
notice, this list of conditions and the following disclaimer in the
|
||||||
|
documentation and/or other materials provided with the distribution.
|
||||||
|
|
||||||
|
3. Neither the name of the copyright holder nor the names of its
|
||||||
|
contributors may be used to endorse or promote products derived from
|
||||||
|
this software without specific prior written permission.
|
||||||
|
|
||||||
|
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
||||||
|
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
||||||
|
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
|
||||||
|
FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
|
||||||
|
COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
|
||||||
|
INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
|
||||||
|
(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
||||||
|
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
|
||||||
|
HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
|
||||||
|
STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING
|
||||||
|
IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
|
||||||
|
POSSIBILITY OF SUCH DAMAGE.
|
||||||
|
|
||||||
|
|
||||||
|
This blob MUST be written to the chip's internal INIT_DATA register
|
||||||
|
after every power cycle, before any sensor data can be read.
|
||||||
|
--------------------------------------------------------------------------- */
|
||||||
|
|
||||||
|
static constexpr uint8_t BMI270_CONFIG_FILE[] = {
|
||||||
|
0xc8, 0x2e, 0x00, 0x2e, 0x80, 0x2e, 0x3d, 0xb1, 0xc8, 0x2e, 0x00, 0x2e, 0x80, 0x2e, 0x91, 0x03, 0x80, 0x2e, 0xbc,
|
||||||
|
0xb0, 0x80, 0x2e, 0xa3, 0x03, 0xc8, 0x2e, 0x00, 0x2e, 0x80, 0x2e, 0x00, 0xb0, 0x50, 0x30, 0x21, 0x2e, 0x59, 0xf5,
|
||||||
|
0x10, 0x30, 0x21, 0x2e, 0x6a, 0xf5, 0x80, 0x2e, 0x3b, 0x03, 0x00, 0x00, 0x00, 0x00, 0x08, 0x19, 0x01, 0x00, 0x22,
|
||||||
|
0x00, 0x75, 0x00, 0x00, 0x10, 0x00, 0x10, 0xd1, 0x00, 0xb3, 0x43, 0x80, 0x2e, 0x00, 0xc1, 0x80, 0x2e, 0x00, 0xc1,
|
||||||
|
0x80, 0x2e, 0x00, 0xc1, 0x80, 0x2e, 0x00, 0xc1, 0x80, 0x2e, 0x00, 0xc1, 0x80, 0x2e, 0x00, 0xc1, 0x80, 0x2e, 0x00,
|
||||||
|
0xc1, 0x80, 0x2e, 0x00, 0xc1, 0x80, 0x2e, 0x00, 0xc1, 0x80, 0x2e, 0x00, 0xc1, 0x80, 0x2e, 0x00, 0xc1, 0x80, 0x2e,
|
||||||
|
0x00, 0xc1, 0x80, 0x2e, 0x00, 0xc1, 0x80, 0x2e, 0x00, 0xc1, 0x80, 0x2e, 0x00, 0xc1, 0x80, 0x2e, 0x00, 0xc1, 0x80,
|
||||||
|
0x2e, 0x00, 0xc1, 0x80, 0x2e, 0x00, 0xc1, 0x80, 0x2e, 0x00, 0xc1, 0x80, 0x2e, 0x00, 0xc1, 0x80, 0x2e, 0x00, 0xc1,
|
||||||
|
0x80, 0x2e, 0x00, 0xc1, 0x80, 0x2e, 0x00, 0xc1, 0x80, 0x2e, 0x00, 0xc1, 0x80, 0x2e, 0x00, 0xc1, 0x80, 0x2e, 0x00,
|
||||||
|
0xc1, 0x80, 0x2e, 0x00, 0xc1, 0x80, 0x2e, 0x00, 0xc1, 0x80, 0x2e, 0x00, 0xc1, 0x80, 0x2e, 0x00, 0xc1, 0x80, 0x2e,
|
||||||
|
0x00, 0xc1, 0x80, 0x2e, 0x00, 0xc1, 0x80, 0x2e, 0x00, 0xc1, 0x80, 0x2e, 0x00, 0xc1, 0x80, 0x2e, 0x00, 0xc1, 0x80,
|
||||||
|
0x2e, 0x00, 0xc1, 0x80, 0x2e, 0x00, 0xc1, 0x80, 0x2e, 0x00, 0xc1, 0x80, 0x2e, 0x00, 0xc1, 0x80, 0x2e, 0x00, 0xc1,
|
||||||
|
0x80, 0x2e, 0x00, 0xc1, 0x80, 0x2e, 0x00, 0xc1, 0xe0, 0x5f, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00,
|
||||||
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x92, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||||
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x08, 0x19, 0x00, 0x00, 0x88, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x05,
|
||||||
|
0xe0, 0xaa, 0x38, 0x05, 0xe0, 0x90, 0x30, 0xfa, 0x00, 0x96, 0x00, 0x4b, 0x09, 0x11, 0x00, 0x11, 0x00, 0x02, 0x00,
|
||||||
|
0x2d, 0x01, 0xd4, 0x7b, 0x3b, 0x01, 0xdb, 0x7a, 0x04, 0x00, 0x3f, 0x7b, 0xcd, 0x6c, 0xc3, 0x04, 0x85, 0x09, 0xc3,
|
||||||
|
0x04, 0xec, 0xe6, 0x0c, 0x46, 0x01, 0x00, 0x27, 0x00, 0x19, 0x00, 0x96, 0x00, 0xa0, 0x00, 0x01, 0x00, 0x0c, 0x00,
|
||||||
|
0xf0, 0x3c, 0x00, 0x01, 0x01, 0x00, 0x03, 0x00, 0x01, 0x00, 0x0e, 0x00, 0x00, 0x00, 0x32, 0x00, 0x05, 0x00, 0xee,
|
||||||
|
0x06, 0x04, 0x00, 0xc8, 0x00, 0x00, 0x00, 0x04, 0x00, 0xa8, 0x05, 0xee, 0x06, 0x00, 0x04, 0xbc, 0x02, 0xb3, 0x00,
|
||||||
|
0x85, 0x07, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||||
|
0x00, 0x00, 0x00, 0xb4, 0x00, 0x01, 0x00, 0xb9, 0x00, 0x01, 0x00, 0x98, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||||
|
0x00, 0x00, 0x01, 0x00, 0x80, 0x00, 0x04, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||||
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||||
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||||
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||||
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0x2e, 0x00, 0xc1, 0xfd, 0x2d, 0xde,
|
||||||
|
0x00, 0xeb, 0x00, 0xda, 0x00, 0x00, 0x0c, 0xff, 0x0f, 0x00, 0x04, 0xc0, 0x00, 0x5b, 0xf5, 0xc9, 0x01, 0x1e, 0xf2,
|
||||||
|
0x80, 0x00, 0x3f, 0xff, 0x19, 0xf4, 0x58, 0xf5, 0x66, 0xf5, 0x64, 0xf5, 0xc0, 0xf1, 0xf0, 0x00, 0xe0, 0x00, 0xcd,
|
||||||
|
0x01, 0xd3, 0x01, 0xdb, 0x01, 0xff, 0x7f, 0xff, 0x01, 0xe4, 0x00, 0x74, 0xf7, 0xf3, 0x00, 0xfa, 0x00, 0xff, 0x3f,
|
||||||
|
0xca, 0x03, 0x6c, 0x38, 0x56, 0xfe, 0x44, 0xfd, 0xbc, 0x02, 0xf9, 0x06, 0x00, 0xfc, 0x12, 0x02, 0xae, 0x01, 0x58,
|
||||||
|
0xfa, 0x9a, 0xfd, 0x77, 0x05, 0xbb, 0x02, 0x96, 0x01, 0x95, 0x01, 0x7f, 0x01, 0x82, 0x01, 0x89, 0x01, 0x87, 0x01,
|
||||||
|
0x88, 0x01, 0x8a, 0x01, 0x8c, 0x01, 0x8f, 0x01, 0x8d, 0x01, 0x92, 0x01, 0x91, 0x01, 0xdd, 0x00, 0x9f, 0x01, 0x7e,
|
||||||
|
0x01, 0xdb, 0x00, 0xb6, 0x01, 0x70, 0x69, 0x26, 0xd3, 0x9c, 0x07, 0x1f, 0x05, 0x9d, 0x00, 0x00, 0x08, 0xbc, 0x05,
|
||||||
|
0x37, 0xfa, 0xa2, 0x01, 0xaa, 0x01, 0xa1, 0x01, 0xa8, 0x01, 0xa0, 0x01, 0xa8, 0x05, 0xb4, 0x01, 0xb4, 0x01, 0xce,
|
||||||
|
0x00, 0xd0, 0x00, 0xfc, 0x00, 0xc5, 0x01, 0xff, 0xfb, 0xb1, 0x00, 0x00, 0x38, 0x00, 0x30, 0xfd, 0xf5, 0xfc, 0xf5,
|
||||||
|
0xcd, 0x01, 0xa0, 0x00, 0x5f, 0xff, 0x00, 0x40, 0xff, 0x00, 0x00, 0x80, 0x6d, 0x0f, 0xeb, 0x00, 0x7f, 0xff, 0xc2,
|
||||||
|
0xf5, 0x68, 0xf7, 0xb3, 0xf1, 0x67, 0x0f, 0x5b, 0x0f, 0x61, 0x0f, 0x80, 0x0f, 0x58, 0xf7, 0x5b, 0xf7, 0x83, 0x0f,
|
||||||
|
0x86, 0x00, 0x72, 0x0f, 0x85, 0x0f, 0xc6, 0xf1, 0x7f, 0x0f, 0x6c, 0xf7, 0x00, 0xe0, 0x00, 0xff, 0xd1, 0xf5, 0x87,
|
||||||
|
0x0f, 0x8a, 0x0f, 0xff, 0x03, 0xf0, 0x3f, 0x8b, 0x00, 0x8e, 0x00, 0x90, 0x00, 0xb9, 0x00, 0x2d, 0xf5, 0xca, 0xf5,
|
||||||
|
0xcb, 0x01, 0x20, 0xf2, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||||
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||||
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||||
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||||
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||||
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||||
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||||
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||||
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||||
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||||
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||||
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x30, 0x50, 0x98, 0x2e,
|
||||||
|
0xd7, 0x0e, 0x50, 0x32, 0x98, 0x2e, 0xfa, 0x03, 0x00, 0x30, 0xf0, 0x7f, 0x00, 0x2e, 0x00, 0x2e, 0xd0, 0x2e, 0x00,
|
||||||
|
0x2e, 0x01, 0x80, 0x08, 0xa2, 0xfb, 0x2f, 0x98, 0x2e, 0xba, 0x03, 0x21, 0x2e, 0x19, 0x00, 0x01, 0x2e, 0xee, 0x00,
|
||||||
|
0x00, 0xb2, 0x07, 0x2f, 0x01, 0x2e, 0x19, 0x00, 0x00, 0xb2, 0x03, 0x2f, 0x01, 0x50, 0x03, 0x52, 0x98, 0x2e, 0x07,
|
||||||
|
0xcc, 0x01, 0x2e, 0xdd, 0x00, 0x00, 0xb2, 0x27, 0x2f, 0x05, 0x2e, 0x8a, 0x00, 0x05, 0x52, 0x98, 0x2e, 0xc7, 0xc1,
|
||||||
|
0x03, 0x2e, 0xe9, 0x00, 0x40, 0xb2, 0xf0, 0x7f, 0x08, 0x2f, 0x01, 0x2e, 0x19, 0x00, 0x00, 0xb2, 0x04, 0x2f, 0x00,
|
||||||
|
0x30, 0x21, 0x2e, 0xe9, 0x00, 0x98, 0x2e, 0xb4, 0xb1, 0x01, 0x2e, 0x18, 0x00, 0x00, 0xb2, 0x10, 0x2f, 0x05, 0x50,
|
||||||
|
0x98, 0x2e, 0x4d, 0xc3, 0x05, 0x50, 0x98, 0x2e, 0x5a, 0xc7, 0x98, 0x2e, 0xf9, 0xb4, 0x98, 0x2e, 0x54, 0xb2, 0x98,
|
||||||
|
0x2e, 0x67, 0xb6, 0x98, 0x2e, 0x17, 0xb2, 0x10, 0x30, 0x21, 0x2e, 0x77, 0x00, 0x01, 0x2e, 0xef, 0x00, 0x00, 0xb2,
|
||||||
|
0x04, 0x2f, 0x98, 0x2e, 0x7a, 0xb7, 0x00, 0x30, 0x21, 0x2e, 0xef, 0x00, 0x01, 0x2e, 0xd4, 0x00, 0x04, 0xae, 0x0b,
|
||||||
|
0x2f, 0x01, 0x2e, 0xdd, 0x00, 0x00, 0xb2, 0x07, 0x2f, 0x05, 0x52, 0x98, 0x2e, 0x8e, 0x0e, 0x00, 0xb2, 0x02, 0x2f,
|
||||||
|
0x10, 0x30, 0x21, 0x2e, 0x7d, 0x00, 0x01, 0x2e, 0x7d, 0x00, 0x00, 0x90, 0x90, 0x2e, 0xf1, 0x02, 0x01, 0x2e, 0xd7,
|
||||||
|
0x00, 0x00, 0xb2, 0x04, 0x2f, 0x98, 0x2e, 0x2f, 0x0e, 0x00, 0x30, 0x21, 0x2e, 0x7b, 0x00, 0x01, 0x2e, 0x7b, 0x00,
|
||||||
|
0x00, 0xb2, 0x12, 0x2f, 0x01, 0x2e, 0xd4, 0x00, 0x00, 0x90, 0x02, 0x2f, 0x98, 0x2e, 0x1f, 0x0e, 0x09, 0x2d, 0x98,
|
||||||
|
0x2e, 0x81, 0x0d, 0x01, 0x2e, 0xd4, 0x00, 0x04, 0x90, 0x02, 0x2f, 0x50, 0x32, 0x98, 0x2e, 0xfa, 0x03, 0x00, 0x30,
|
||||||
|
0x21, 0x2e, 0x7b, 0x00, 0x01, 0x2e, 0x7c, 0x00, 0x00, 0xb2, 0x90, 0x2e, 0x09, 0x03, 0x01, 0x2e, 0x7c, 0x00, 0x01,
|
||||||
|
0x31, 0x01, 0x08, 0x00, 0xb2, 0x04, 0x2f, 0x98, 0x2e, 0x47, 0xcb, 0x10, 0x30, 0x21, 0x2e, 0x77, 0x00, 0x81, 0x30,
|
||||||
|
0x01, 0x2e, 0x7c, 0x00, 0x01, 0x08, 0x00, 0xb2, 0x61, 0x2f, 0x03, 0x2e, 0x89, 0x00, 0x01, 0x2e, 0xd4, 0x00, 0x98,
|
||||||
|
0xbc, 0x98, 0xb8, 0x05, 0xb2, 0x0f, 0x58, 0x23, 0x2f, 0x07, 0x90, 0x09, 0x54, 0x00, 0x30, 0x37, 0x2f, 0x15, 0x41,
|
||||||
|
0x04, 0x41, 0xdc, 0xbe, 0x44, 0xbe, 0xdc, 0xba, 0x2c, 0x01, 0x61, 0x00, 0x0f, 0x56, 0x4a, 0x0f, 0x0c, 0x2f, 0xd1,
|
||||||
|
0x42, 0x94, 0xb8, 0xc1, 0x42, 0x11, 0x30, 0x05, 0x2e, 0x6a, 0xf7, 0x2c, 0xbd, 0x2f, 0xb9, 0x80, 0xb2, 0x08, 0x22,
|
||||||
|
0x98, 0x2e, 0xc3, 0xb7, 0x21, 0x2d, 0x61, 0x30, 0x23, 0x2e, 0xd4, 0x00, 0x98, 0x2e, 0xc3, 0xb7, 0x00, 0x30, 0x21,
|
||||||
|
0x2e, 0x5a, 0xf5, 0x18, 0x2d, 0xe1, 0x7f, 0x50, 0x30, 0x98, 0x2e, 0xfa, 0x03, 0x0f, 0x52, 0x07, 0x50, 0x50, 0x42,
|
||||||
|
0x70, 0x30, 0x0d, 0x54, 0x42, 0x42, 0x7e, 0x82, 0xe2, 0x6f, 0x80, 0xb2, 0x42, 0x42, 0x05, 0x2f, 0x21, 0x2e, 0xd4,
|
||||||
|
0x00, 0x10, 0x30, 0x98, 0x2e, 0xc3, 0xb7, 0x03, 0x2d, 0x60, 0x30, 0x21, 0x2e, 0xd4, 0x00, 0x01, 0x2e, 0xd4, 0x00,
|
||||||
|
0x06, 0x90, 0x18, 0x2f, 0x01, 0x2e, 0x76, 0x00, 0x0b, 0x54, 0x07, 0x52, 0xe0, 0x7f, 0x98, 0x2e, 0x7a, 0xc1, 0xe1,
|
||||||
|
0x6f, 0x08, 0x1a, 0x40, 0x30, 0x08, 0x2f, 0x21, 0x2e, 0xd4, 0x00, 0x20, 0x30, 0x98, 0x2e, 0xaf, 0xb7, 0x50, 0x32,
|
||||||
|
0x98, 0x2e, 0xfa, 0x03, 0x05, 0x2d, 0x98, 0x2e, 0x38, 0x0e, 0x00, 0x30, 0x21, 0x2e, 0xd4, 0x00, 0x00, 0x30, 0x21,
|
||||||
|
0x2e, 0x7c, 0x00, 0x18, 0x2d, 0x01, 0x2e, 0xd4, 0x00, 0x03, 0xaa, 0x01, 0x2f, 0x98, 0x2e, 0x45, 0x0e, 0x01, 0x2e,
|
||||||
|
0xd4, 0x00, 0x3f, 0x80, 0x03, 0xa2, 0x01, 0x2f, 0x00, 0x2e, 0x02, 0x2d, 0x98, 0x2e, 0x5b, 0x0e, 0x30, 0x30, 0x98,
|
||||||
|
0x2e, 0xce, 0xb7, 0x00, 0x30, 0x21, 0x2e, 0x7d, 0x00, 0x50, 0x32, 0x98, 0x2e, 0xfa, 0x03, 0x01, 0x2e, 0x77, 0x00,
|
||||||
|
0x00, 0xb2, 0x24, 0x2f, 0x98, 0x2e, 0xf5, 0xcb, 0x03, 0x2e, 0xd5, 0x00, 0x11, 0x54, 0x01, 0x0a, 0xbc, 0x84, 0x83,
|
||||||
|
0x86, 0x21, 0x2e, 0xc9, 0x01, 0xe0, 0x40, 0x13, 0x52, 0xc4, 0x40, 0x82, 0x40, 0xa8, 0xb9, 0x52, 0x42, 0x43, 0xbe,
|
||||||
|
0x53, 0x42, 0x04, 0x0a, 0x50, 0x42, 0xe1, 0x7f, 0xf0, 0x31, 0x41, 0x40, 0xf2, 0x6f, 0x25, 0xbd, 0x08, 0x08, 0x02,
|
||||||
|
0x0a, 0xd0, 0x7f, 0x98, 0x2e, 0xa8, 0xcf, 0x06, 0xbc, 0xd1, 0x6f, 0xe2, 0x6f, 0x08, 0x0a, 0x80, 0x42, 0x98, 0x2e,
|
||||||
|
0x58, 0xb7, 0x00, 0x30, 0x21, 0x2e, 0xee, 0x00, 0x21, 0x2e, 0x77, 0x00, 0x21, 0x2e, 0xdd, 0x00, 0x80, 0x2e, 0xf4,
|
||||||
|
0x01, 0x1a, 0x24, 0x22, 0x00, 0x80, 0x2e, 0xec, 0x01, 0x10, 0x50, 0xfb, 0x7f, 0x98, 0x2e, 0xf3, 0x03, 0x57, 0x50,
|
||||||
|
0xfb, 0x6f, 0x01, 0x30, 0x71, 0x54, 0x11, 0x42, 0x42, 0x0e, 0xfc, 0x2f, 0xc0, 0x2e, 0x01, 0x42, 0xf0, 0x5f, 0x80,
|
||||||
|
0x2e, 0x00, 0xc1, 0xfd, 0x2d, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x9a, 0x01,
|
||||||
|
0x34, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||||
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||||
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||||
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||||
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||||
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||||
|
0x00, 0x00, 0x20, 0x50, 0xe7, 0x7f, 0xf6, 0x7f, 0x06, 0x32, 0x0f, 0x2e, 0x61, 0xf5, 0xfe, 0x09, 0xc0, 0xb3, 0x04,
|
||||||
|
0x2f, 0x17, 0x30, 0x2f, 0x2e, 0xef, 0x00, 0x2d, 0x2e, 0x61, 0xf5, 0xf6, 0x6f, 0xe7, 0x6f, 0xe0, 0x5f, 0xc8, 0x2e,
|
||||||
|
0x20, 0x50, 0xe7, 0x7f, 0xf6, 0x7f, 0x46, 0x30, 0x0f, 0x2e, 0xa4, 0xf1, 0xbe, 0x09, 0x80, 0xb3, 0x06, 0x2f, 0x0d,
|
||||||
|
0x2e, 0xd4, 0x00, 0x84, 0xaf, 0x02, 0x2f, 0x16, 0x30, 0x2d, 0x2e, 0x7b, 0x00, 0x86, 0x30, 0x2d, 0x2e, 0x60, 0xf5,
|
||||||
|
0xf6, 0x6f, 0xe7, 0x6f, 0xe0, 0x5f, 0xc8, 0x2e, 0x01, 0x2e, 0x77, 0xf7, 0x09, 0xbc, 0x0f, 0xb8, 0x00, 0xb2, 0x10,
|
||||||
|
0x50, 0xfb, 0x7f, 0x10, 0x30, 0x0b, 0x2f, 0x03, 0x2e, 0x8a, 0x00, 0x96, 0xbc, 0x9f, 0xb8, 0x40, 0xb2, 0x05, 0x2f,
|
||||||
|
0x03, 0x2e, 0x68, 0xf7, 0x9e, 0xbc, 0x9f, 0xb8, 0x40, 0xb2, 0x07, 0x2f, 0x03, 0x2e, 0x7e, 0x00, 0x41, 0x90, 0x01,
|
||||||
|
0x2f, 0x98, 0x2e, 0xdc, 0x03, 0x03, 0x2c, 0x00, 0x30, 0x21, 0x2e, 0x7e, 0x00, 0xfb, 0x6f, 0xf0, 0x5f, 0xb8, 0x2e,
|
||||||
|
0x20, 0x50, 0xe0, 0x7f, 0xfb, 0x7f, 0x00, 0x2e, 0x27, 0x50, 0x98, 0x2e, 0x3b, 0xc8, 0x29, 0x50, 0x98, 0x2e, 0xa7,
|
||||||
|
0xc8, 0x01, 0x50, 0x98, 0x2e, 0x55, 0xcc, 0xe1, 0x6f, 0x2b, 0x50, 0x98, 0x2e, 0xe0, 0xc9, 0xfb, 0x6f, 0x00, 0x30,
|
||||||
|
0xe0, 0x5f, 0x21, 0x2e, 0x7e, 0x00, 0xb8, 0x2e, 0x73, 0x50, 0x01, 0x30, 0x57, 0x54, 0x11, 0x42, 0x42, 0x0e, 0xfc,
|
||||||
|
0x2f, 0xb8, 0x2e, 0x21, 0x2e, 0x59, 0xf5, 0x10, 0x30, 0xc0, 0x2e, 0x21, 0x2e, 0x4a, 0xf1, 0x90, 0x50, 0xf7, 0x7f,
|
||||||
|
0xe6, 0x7f, 0xd5, 0x7f, 0xc4, 0x7f, 0xb3, 0x7f, 0xa1, 0x7f, 0x90, 0x7f, 0x82, 0x7f, 0x7b, 0x7f, 0x98, 0x2e, 0x35,
|
||||||
|
0xb7, 0x00, 0xb2, 0x90, 0x2e, 0x97, 0xb0, 0x03, 0x2e, 0x8f, 0x00, 0x07, 0x2e, 0x91, 0x00, 0x05, 0x2e, 0xb1, 0x00,
|
||||||
|
0x3f, 0xba, 0x9f, 0xb8, 0x01, 0x2e, 0xb1, 0x00, 0xa3, 0xbd, 0x4c, 0x0a, 0x05, 0x2e, 0xb1, 0x00, 0x04, 0xbe, 0xbf,
|
||||||
|
0xb9, 0xcb, 0x0a, 0x4f, 0xba, 0x22, 0xbd, 0x01, 0x2e, 0xb3, 0x00, 0xdc, 0x0a, 0x2f, 0xb9, 0x03, 0x2e, 0xb8, 0x00,
|
||||||
|
0x0a, 0xbe, 0x9a, 0x0a, 0xcf, 0xb9, 0x9b, 0xbc, 0x01, 0x2e, 0x97, 0x00, 0x9f, 0xb8, 0x93, 0x0a, 0x0f, 0xbc, 0x91,
|
||||||
|
0x0a, 0x0f, 0xb8, 0x90, 0x0a, 0x25, 0x2e, 0x18, 0x00, 0x05, 0x2e, 0xc1, 0xf5, 0x2e, 0xbd, 0x2e, 0xb9, 0x01, 0x2e,
|
||||||
|
0x19, 0x00, 0x31, 0x30, 0x8a, 0x04, 0x00, 0x90, 0x07, 0x2f, 0x01, 0x2e, 0xd4, 0x00, 0x04, 0xa2, 0x03, 0x2f, 0x01,
|
||||||
|
0x2e, 0x18, 0x00, 0x00, 0xb2, 0x0c, 0x2f, 0x19, 0x50, 0x05, 0x52, 0x98, 0x2e, 0x4d, 0xb7, 0x05, 0x2e, 0x78, 0x00,
|
||||||
|
0x80, 0x90, 0x10, 0x30, 0x01, 0x2f, 0x21, 0x2e, 0x78, 0x00, 0x25, 0x2e, 0xdd, 0x00, 0x98, 0x2e, 0x3e, 0xb7, 0x00,
|
||||||
|
0xb2, 0x02, 0x30, 0x01, 0x30, 0x04, 0x2f, 0x01, 0x2e, 0x19, 0x00, 0x00, 0xb2, 0x00, 0x2f, 0x21, 0x30, 0x01, 0x2e,
|
||||||
|
0xea, 0x00, 0x08, 0x1a, 0x0e, 0x2f, 0x23, 0x2e, 0xea, 0x00, 0x33, 0x30, 0x1b, 0x50, 0x0b, 0x09, 0x01, 0x40, 0x17,
|
||||||
|
0x56, 0x46, 0xbe, 0x4b, 0x08, 0x4c, 0x0a, 0x01, 0x42, 0x0a, 0x80, 0x15, 0x52, 0x01, 0x42, 0x00, 0x2e, 0x01, 0x2e,
|
||||||
|
0x18, 0x00, 0x00, 0xb2, 0x1f, 0x2f, 0x03, 0x2e, 0xc0, 0xf5, 0xf0, 0x30, 0x48, 0x08, 0x47, 0xaa, 0x74, 0x30, 0x07,
|
||||||
|
0x2e, 0x7a, 0x00, 0x61, 0x22, 0x4b, 0x1a, 0x05, 0x2f, 0x07, 0x2e, 0x66, 0xf5, 0xbf, 0xbd, 0xbf, 0xb9, 0xc0, 0x90,
|
||||||
|
0x0b, 0x2f, 0x1d, 0x56, 0x2b, 0x30, 0xd2, 0x42, 0xdb, 0x42, 0x01, 0x04, 0xc2, 0x42, 0x04, 0xbd, 0xfe, 0x80, 0x81,
|
||||||
|
0x84, 0x23, 0x2e, 0x7a, 0x00, 0x02, 0x42, 0x02, 0x32, 0x25, 0x2e, 0x62, 0xf5, 0x05, 0x2e, 0xd6, 0x00, 0x81, 0x84,
|
||||||
|
0x25, 0x2e, 0xd6, 0x00, 0x02, 0x31, 0x25, 0x2e, 0x60, 0xf5, 0x05, 0x2e, 0x8a, 0x00, 0x0b, 0x50, 0x90, 0x08, 0x80,
|
||||||
|
0xb2, 0x0b, 0x2f, 0x05, 0x2e, 0xca, 0xf5, 0xf0, 0x3e, 0x90, 0x08, 0x25, 0x2e, 0xca, 0xf5, 0x05, 0x2e, 0x59, 0xf5,
|
||||||
|
0xe0, 0x3f, 0x90, 0x08, 0x25, 0x2e, 0x59, 0xf5, 0x90, 0x6f, 0xa1, 0x6f, 0xb3, 0x6f, 0xc4, 0x6f, 0xd5, 0x6f, 0xe6,
|
||||||
|
0x6f, 0xf7, 0x6f, 0x7b, 0x6f, 0x82, 0x6f, 0x70, 0x5f, 0xc8, 0x2e, 0xc0, 0x50, 0x90, 0x7f, 0xe5, 0x7f, 0xd4, 0x7f,
|
||||||
|
0xc3, 0x7f, 0xb1, 0x7f, 0xa2, 0x7f, 0x87, 0x7f, 0xf6, 0x7f, 0x7b, 0x7f, 0x00, 0x2e, 0x01, 0x2e, 0x60, 0xf5, 0x60,
|
||||||
|
0x7f, 0x98, 0x2e, 0x35, 0xb7, 0x02, 0x30, 0x63, 0x6f, 0x15, 0x52, 0x50, 0x7f, 0x62, 0x7f, 0x5a, 0x2c, 0x02, 0x32,
|
||||||
|
0x1a, 0x09, 0x00, 0xb3, 0x14, 0x2f, 0x00, 0xb2, 0x03, 0x2f, 0x09, 0x2e, 0x18, 0x00, 0x00, 0x91, 0x0c, 0x2f, 0x43,
|
||||||
|
0x7f, 0x98, 0x2e, 0x97, 0xb7, 0x1f, 0x50, 0x02, 0x8a, 0x02, 0x32, 0x04, 0x30, 0x25, 0x2e, 0x64, 0xf5, 0x15, 0x52,
|
||||||
|
0x50, 0x6f, 0x43, 0x6f, 0x44, 0x43, 0x25, 0x2e, 0x60, 0xf5, 0xd9, 0x08, 0xc0, 0xb2, 0x36, 0x2f, 0x98, 0x2e, 0x3e,
|
||||||
|
0xb7, 0x00, 0xb2, 0x06, 0x2f, 0x01, 0x2e, 0x19, 0x00, 0x00, 0xb2, 0x02, 0x2f, 0x50, 0x6f, 0x00, 0x90, 0x0a, 0x2f,
|
||||||
|
0x01, 0x2e, 0x79, 0x00, 0x00, 0x90, 0x19, 0x2f, 0x10, 0x30, 0x21, 0x2e, 0x79, 0x00, 0x00, 0x30, 0x98, 0x2e, 0xdc,
|
||||||
|
0x03, 0x13, 0x2d, 0x01, 0x2e, 0xc3, 0xf5, 0x0c, 0xbc, 0x0f, 0xb8, 0x12, 0x30, 0x10, 0x04, 0x03, 0xb0, 0x26, 0x25,
|
||||||
|
0x21, 0x50, 0x03, 0x52, 0x98, 0x2e, 0x4d, 0xb7, 0x10, 0x30, 0x21, 0x2e, 0xee, 0x00, 0x02, 0x30, 0x60, 0x7f, 0x25,
|
||||||
|
0x2e, 0x79, 0x00, 0x60, 0x6f, 0x00, 0x90, 0x05, 0x2f, 0x00, 0x30, 0x21, 0x2e, 0xea, 0x00, 0x15, 0x50, 0x21, 0x2e,
|
||||||
|
0x64, 0xf5, 0x15, 0x52, 0x23, 0x2e, 0x60, 0xf5, 0x02, 0x32, 0x50, 0x6f, 0x00, 0x90, 0x02, 0x2f, 0x03, 0x30, 0x27,
|
||||||
|
0x2e, 0x78, 0x00, 0x07, 0x2e, 0x60, 0xf5, 0x1a, 0x09, 0x00, 0x91, 0xa3, 0x2f, 0x19, 0x09, 0x00, 0x91, 0xa0, 0x2f,
|
||||||
|
0x90, 0x6f, 0xa2, 0x6f, 0xb1, 0x6f, 0xc3, 0x6f, 0xd4, 0x6f, 0xe5, 0x6f, 0x7b, 0x6f, 0xf6, 0x6f, 0x87, 0x6f, 0x40,
|
||||||
|
0x5f, 0xc8, 0x2e, 0xc0, 0x50, 0xe7, 0x7f, 0xf6, 0x7f, 0x26, 0x30, 0x0f, 0x2e, 0x61, 0xf5, 0x2f, 0x2e, 0x7c, 0x00,
|
||||||
|
0x0f, 0x2e, 0x7c, 0x00, 0xbe, 0x09, 0xa2, 0x7f, 0x80, 0x7f, 0x80, 0xb3, 0xd5, 0x7f, 0xc4, 0x7f, 0xb3, 0x7f, 0x91,
|
||||||
|
0x7f, 0x7b, 0x7f, 0x0b, 0x2f, 0x23, 0x50, 0x1a, 0x25, 0x12, 0x40, 0x42, 0x7f, 0x74, 0x82, 0x12, 0x40, 0x52, 0x7f,
|
||||||
|
0x00, 0x2e, 0x00, 0x40, 0x60, 0x7f, 0x98, 0x2e, 0x6a, 0xd6, 0x81, 0x30, 0x01, 0x2e, 0x7c, 0x00, 0x01, 0x08, 0x00,
|
||||||
|
0xb2, 0x42, 0x2f, 0x03, 0x2e, 0x89, 0x00, 0x01, 0x2e, 0x89, 0x00, 0x97, 0xbc, 0x06, 0xbc, 0x9f, 0xb8, 0x0f, 0xb8,
|
||||||
|
0x00, 0x90, 0x23, 0x2e, 0xd8, 0x00, 0x10, 0x30, 0x01, 0x30, 0x2a, 0x2f, 0x03, 0x2e, 0xd4, 0x00, 0x44, 0xb2, 0x05,
|
||||||
|
0x2f, 0x47, 0xb2, 0x00, 0x30, 0x2d, 0x2f, 0x21, 0x2e, 0x7c, 0x00, 0x2b, 0x2d, 0x03, 0x2e, 0xfd, 0xf5, 0x9e, 0xbc,
|
||||||
|
0x9f, 0xb8, 0x40, 0x90, 0x14, 0x2f, 0x03, 0x2e, 0xfc, 0xf5, 0x99, 0xbc, 0x9f, 0xb8, 0x40, 0x90, 0x0e, 0x2f, 0x03,
|
||||||
|
0x2e, 0x49, 0xf1, 0x25, 0x54, 0x4a, 0x08, 0x40, 0x90, 0x08, 0x2f, 0x98, 0x2e, 0x35, 0xb7, 0x00, 0xb2, 0x10, 0x30,
|
||||||
|
0x03, 0x2f, 0x50, 0x30, 0x21, 0x2e, 0xd4, 0x00, 0x10, 0x2d, 0x98, 0x2e, 0xaf, 0xb7, 0x00, 0x30, 0x21, 0x2e, 0x7c,
|
||||||
|
0x00, 0x0a, 0x2d, 0x05, 0x2e, 0x69, 0xf7, 0x2d, 0xbd, 0x2f, 0xb9, 0x80, 0xb2, 0x01, 0x2f, 0x21, 0x2e, 0x7d, 0x00,
|
||||||
|
0x23, 0x2e, 0x7c, 0x00, 0xe0, 0x31, 0x21, 0x2e, 0x61, 0xf5, 0xf6, 0x6f, 0xe7, 0x6f, 0x80, 0x6f, 0xa2, 0x6f, 0xb3,
|
||||||
|
0x6f, 0xc4, 0x6f, 0xd5, 0x6f, 0x7b, 0x6f, 0x91, 0x6f, 0x40, 0x5f, 0xc8, 0x2e, 0x60, 0x51, 0x0a, 0x25, 0x36, 0x88,
|
||||||
|
0xf4, 0x7f, 0xeb, 0x7f, 0x00, 0x32, 0x31, 0x52, 0x32, 0x30, 0x13, 0x30, 0x98, 0x2e, 0x15, 0xcb, 0x0a, 0x25, 0x33,
|
||||||
|
0x84, 0xd2, 0x7f, 0x43, 0x30, 0x05, 0x50, 0x2d, 0x52, 0x98, 0x2e, 0x95, 0xc1, 0xd2, 0x6f, 0x27, 0x52, 0x98, 0x2e,
|
||||||
|
0xd7, 0xc7, 0x2a, 0x25, 0xb0, 0x86, 0xc0, 0x7f, 0xd3, 0x7f, 0xaf, 0x84, 0x29, 0x50, 0xf1, 0x6f, 0x98, 0x2e, 0x4d,
|
||||||
|
0xc8, 0x2a, 0x25, 0xae, 0x8a, 0xaa, 0x88, 0xf2, 0x6e, 0x2b, 0x50, 0xc1, 0x6f, 0xd3, 0x6f, 0xf4, 0x7f, 0x98, 0x2e,
|
||||||
|
0xb6, 0xc8, 0xe0, 0x6e, 0x00, 0xb2, 0x32, 0x2f, 0x33, 0x54, 0x83, 0x86, 0xf1, 0x6f, 0xc3, 0x7f, 0x04, 0x30, 0x30,
|
||||||
|
0x30, 0xf4, 0x7f, 0xd0, 0x7f, 0xb2, 0x7f, 0xe3, 0x30, 0xc5, 0x6f, 0x56, 0x40, 0x45, 0x41, 0x28, 0x08, 0x03, 0x14,
|
||||||
|
0x0e, 0xb4, 0x08, 0xbc, 0x82, 0x40, 0x10, 0x0a, 0x2f, 0x54, 0x26, 0x05, 0x91, 0x7f, 0x44, 0x28, 0xa3, 0x7f, 0x98,
|
||||||
|
0x2e, 0xd9, 0xc0, 0x08, 0xb9, 0x33, 0x30, 0x53, 0x09, 0xc1, 0x6f, 0xd3, 0x6f, 0xf4, 0x6f, 0x83, 0x17, 0x47, 0x40,
|
||||||
|
0x6c, 0x15, 0xb2, 0x6f, 0xbe, 0x09, 0x75, 0x0b, 0x90, 0x42, 0x45, 0x42, 0x51, 0x0e, 0x32, 0xbc, 0x02, 0x89, 0xa1,
|
||||||
|
0x6f, 0x7e, 0x86, 0xf4, 0x7f, 0xd0, 0x7f, 0xb2, 0x7f, 0x04, 0x30, 0x91, 0x6f, 0xd6, 0x2f, 0xeb, 0x6f, 0xa0, 0x5e,
|
||||||
|
0xb8, 0x2e, 0x03, 0x2e, 0x97, 0x00, 0x1b, 0xbc, 0x60, 0x50, 0x9f, 0xbc, 0x0c, 0xb8, 0xf0, 0x7f, 0x40, 0xb2, 0xeb,
|
||||||
|
0x7f, 0x2b, 0x2f, 0x03, 0x2e, 0x7f, 0x00, 0x41, 0x40, 0x01, 0x2e, 0xc8, 0x00, 0x01, 0x1a, 0x11, 0x2f, 0x37, 0x58,
|
||||||
|
0x23, 0x2e, 0xc8, 0x00, 0x10, 0x41, 0xa0, 0x7f, 0x38, 0x81, 0x01, 0x41, 0xd0, 0x7f, 0xb1, 0x7f, 0x98, 0x2e, 0x64,
|
||||||
|
0xcf, 0xd0, 0x6f, 0x07, 0x80, 0xa1, 0x6f, 0x11, 0x42, 0x00, 0x2e, 0xb1, 0x6f, 0x01, 0x42, 0x11, 0x30, 0x01, 0x2e,
|
||||||
|
0xfc, 0x00, 0x00, 0xa8, 0x03, 0x30, 0xcb, 0x22, 0x4a, 0x25, 0x01, 0x2e, 0x7f, 0x00, 0x3c, 0x89, 0x35, 0x52, 0x05,
|
||||||
|
0x54, 0x98, 0x2e, 0xc4, 0xce, 0xc1, 0x6f, 0xf0, 0x6f, 0x98, 0x2e, 0x95, 0xcf, 0x04, 0x2d, 0x01, 0x30, 0xf0, 0x6f,
|
||||||
|
0x98, 0x2e, 0x95, 0xcf, 0xeb, 0x6f, 0xa0, 0x5f, 0xb8, 0x2e, 0x03, 0x2e, 0xb3, 0x00, 0x02, 0x32, 0xf0, 0x30, 0x03,
|
||||||
|
0x31, 0x30, 0x50, 0x8a, 0x08, 0x08, 0x08, 0xcb, 0x08, 0xe0, 0x7f, 0x80, 0xb2, 0xf3, 0x7f, 0xdb, 0x7f, 0x25, 0x2f,
|
||||||
|
0x03, 0x2e, 0xca, 0x00, 0x41, 0x90, 0x04, 0x2f, 0x01, 0x30, 0x23, 0x2e, 0xca, 0x00, 0x98, 0x2e, 0x3f, 0x03, 0xc0,
|
||||||
|
0xb2, 0x05, 0x2f, 0x03, 0x2e, 0xda, 0x00, 0x00, 0x30, 0x41, 0x04, 0x23, 0x2e, 0xda, 0x00, 0x98, 0x2e, 0x92, 0xb2,
|
||||||
|
0x10, 0x25, 0xf0, 0x6f, 0x00, 0xb2, 0x05, 0x2f, 0x01, 0x2e, 0xda, 0x00, 0x02, 0x30, 0x10, 0x04, 0x21, 0x2e, 0xda,
|
||||||
|
0x00, 0x40, 0xb2, 0x01, 0x2f, 0x23, 0x2e, 0xc8, 0x01, 0xdb, 0x6f, 0xe0, 0x6f, 0xd0, 0x5f, 0x80, 0x2e, 0x95, 0xcf,
|
||||||
|
0x01, 0x30, 0xe0, 0x6f, 0x98, 0x2e, 0x95, 0xcf, 0x11, 0x30, 0x23, 0x2e, 0xca, 0x00, 0xdb, 0x6f, 0xd0, 0x5f, 0xb8,
|
||||||
|
0x2e, 0xd0, 0x50, 0x0a, 0x25, 0x33, 0x84, 0x55, 0x50, 0xd2, 0x7f, 0xe2, 0x7f, 0x03, 0x8c, 0xc0, 0x7f, 0xbb, 0x7f,
|
||||||
|
0x00, 0x30, 0x05, 0x5a, 0x39, 0x54, 0x51, 0x41, 0xa5, 0x7f, 0x96, 0x7f, 0x80, 0x7f, 0x98, 0x2e, 0xd9, 0xc0, 0x05,
|
||||||
|
0x30, 0xf5, 0x7f, 0x20, 0x25, 0x91, 0x6f, 0x3b, 0x58, 0x3d, 0x5c, 0x3b, 0x56, 0x98, 0x2e, 0x67, 0xcc, 0xc1, 0x6f,
|
||||||
|
0xd5, 0x6f, 0x52, 0x40, 0x50, 0x43, 0xc1, 0x7f, 0xd5, 0x7f, 0x10, 0x25, 0x98, 0x2e, 0xfe, 0xc9, 0x10, 0x25, 0x98,
|
||||||
|
0x2e, 0x74, 0xc0, 0x86, 0x6f, 0x30, 0x28, 0x92, 0x6f, 0x82, 0x8c, 0xa5, 0x6f, 0x6f, 0x52, 0x69, 0x0e, 0x39, 0x54,
|
||||||
|
0xdb, 0x2f, 0x19, 0xa0, 0x15, 0x30, 0x03, 0x2f, 0x00, 0x30, 0x21, 0x2e, 0x81, 0x01, 0x0a, 0x2d, 0x01, 0x2e, 0x81,
|
||||||
|
0x01, 0x05, 0x28, 0x42, 0x36, 0x21, 0x2e, 0x81, 0x01, 0x02, 0x0e, 0x01, 0x2f, 0x98, 0x2e, 0xf3, 0x03, 0x57, 0x50,
|
||||||
|
0x12, 0x30, 0x01, 0x40, 0x98, 0x2e, 0xfe, 0xc9, 0x51, 0x6f, 0x0b, 0x5c, 0x8e, 0x0e, 0x3b, 0x6f, 0x57, 0x58, 0x02,
|
||||||
|
0x30, 0x21, 0x2e, 0x95, 0x01, 0x45, 0x6f, 0x2a, 0x8d, 0xd2, 0x7f, 0xcb, 0x7f, 0x13, 0x2f, 0x02, 0x30, 0x3f, 0x50,
|
||||||
|
0xd2, 0x7f, 0xa8, 0x0e, 0x0e, 0x2f, 0xc0, 0x6f, 0x53, 0x54, 0x02, 0x00, 0x51, 0x54, 0x42, 0x0e, 0x10, 0x30, 0x59,
|
||||||
|
0x52, 0x02, 0x30, 0x01, 0x2f, 0x00, 0x2e, 0x03, 0x2d, 0x50, 0x42, 0x42, 0x42, 0x12, 0x30, 0xd2, 0x7f, 0x80, 0xb2,
|
||||||
|
0x03, 0x2f, 0x00, 0x30, 0x21, 0x2e, 0x80, 0x01, 0x12, 0x2d, 0x01, 0x2e, 0xc9, 0x00, 0x02, 0x80, 0x05, 0x2e, 0x80,
|
||||||
|
0x01, 0x11, 0x30, 0x91, 0x28, 0x00, 0x40, 0x25, 0x2e, 0x80, 0x01, 0x10, 0x0e, 0x05, 0x2f, 0x01, 0x2e, 0x7f, 0x01,
|
||||||
|
0x01, 0x90, 0x01, 0x2f, 0x98, 0x2e, 0xf3, 0x03, 0x00, 0x2e, 0xa0, 0x41, 0x01, 0x90, 0xa6, 0x7f, 0x90, 0x2e, 0xe3,
|
||||||
|
0xb4, 0x01, 0x2e, 0x95, 0x01, 0x00, 0xa8, 0x90, 0x2e, 0xe3, 0xb4, 0x5b, 0x54, 0x95, 0x80, 0x82, 0x40, 0x80, 0xb2,
|
||||||
|
0x02, 0x40, 0x2d, 0x8c, 0x3f, 0x52, 0x96, 0x7f, 0x90, 0x2e, 0xc2, 0xb3, 0x29, 0x0e, 0x76, 0x2f, 0x01, 0x2e, 0xc9,
|
||||||
|
0x00, 0x00, 0x40, 0x81, 0x28, 0x45, 0x52, 0xb3, 0x30, 0x98, 0x2e, 0x0f, 0xca, 0x5d, 0x54, 0x80, 0x7f, 0x00, 0x2e,
|
||||||
|
0xa1, 0x40, 0x72, 0x7f, 0x82, 0x80, 0x82, 0x40, 0x60, 0x7f, 0x98, 0x2e, 0xfe, 0xc9, 0x10, 0x25, 0x98, 0x2e, 0x74,
|
||||||
|
0xc0, 0x62, 0x6f, 0x05, 0x30, 0x87, 0x40, 0xc0, 0x91, 0x04, 0x30, 0x05, 0x2f, 0x05, 0x2e, 0x83, 0x01, 0x80, 0xb2,
|
||||||
|
0x14, 0x30, 0x00, 0x2f, 0x04, 0x30, 0x05, 0x2e, 0xc9, 0x00, 0x73, 0x6f, 0x81, 0x40, 0xe2, 0x40, 0x69, 0x04, 0x11,
|
||||||
|
0x0f, 0xe1, 0x40, 0x16, 0x30, 0xfe, 0x29, 0xcb, 0x40, 0x02, 0x2f, 0x83, 0x6f, 0x83, 0x0f, 0x22, 0x2f, 0x47, 0x56,
|
||||||
|
0x13, 0x0f, 0x12, 0x30, 0x77, 0x2f, 0x49, 0x54, 0x42, 0x0e, 0x12, 0x30, 0x73, 0x2f, 0x00, 0x91, 0x0a, 0x2f, 0x01,
|
||||||
|
0x2e, 0x8b, 0x01, 0x19, 0xa8, 0x02, 0x30, 0x6c, 0x2f, 0x63, 0x50, 0x00, 0x2e, 0x17, 0x42, 0x05, 0x42, 0x68, 0x2c,
|
||||||
|
0x12, 0x30, 0x0b, 0x25, 0x08, 0x0f, 0x50, 0x30, 0x02, 0x2f, 0x21, 0x2e, 0x83, 0x01, 0x03, 0x2d, 0x40, 0x30, 0x21,
|
||||||
|
0x2e, 0x83, 0x01, 0x2b, 0x2e, 0x85, 0x01, 0x5a, 0x2c, 0x12, 0x30, 0x00, 0x91, 0x2b, 0x25, 0x04, 0x2f, 0x63, 0x50,
|
||||||
|
0x02, 0x30, 0x17, 0x42, 0x17, 0x2c, 0x02, 0x42, 0x98, 0x2e, 0xfe, 0xc9, 0x10, 0x25, 0x98, 0x2e, 0x74, 0xc0, 0x05,
|
||||||
|
0x2e, 0xc9, 0x00, 0x81, 0x84, 0x5b, 0x30, 0x82, 0x40, 0x37, 0x2e, 0x83, 0x01, 0x02, 0x0e, 0x07, 0x2f, 0x5f, 0x52,
|
||||||
|
0x40, 0x30, 0x62, 0x40, 0x41, 0x40, 0x91, 0x0e, 0x01, 0x2f, 0x21, 0x2e, 0x83, 0x01, 0x05, 0x30, 0x2b, 0x2e, 0x85,
|
||||||
|
0x01, 0x12, 0x30, 0x36, 0x2c, 0x16, 0x30, 0x15, 0x25, 0x81, 0x7f, 0x98, 0x2e, 0xfe, 0xc9, 0x10, 0x25, 0x98, 0x2e,
|
||||||
|
0x74, 0xc0, 0x19, 0xa2, 0x16, 0x30, 0x15, 0x2f, 0x05, 0x2e, 0x97, 0x01, 0x80, 0x6f, 0x82, 0x0e, 0x05, 0x2f, 0x01,
|
||||||
|
0x2e, 0x86, 0x01, 0x06, 0x28, 0x21, 0x2e, 0x86, 0x01, 0x0b, 0x2d, 0x03, 0x2e, 0x87, 0x01, 0x5f, 0x54, 0x4e, 0x28,
|
||||||
|
0x91, 0x42, 0x00, 0x2e, 0x82, 0x40, 0x90, 0x0e, 0x01, 0x2f, 0x21, 0x2e, 0x88, 0x01, 0x02, 0x30, 0x13, 0x2c, 0x05,
|
||||||
|
0x30, 0xc0, 0x6f, 0x08, 0x1c, 0xa8, 0x0f, 0x16, 0x30, 0x05, 0x30, 0x5b, 0x50, 0x09, 0x2f, 0x02, 0x80, 0x2d, 0x2e,
|
||||||
|
0x82, 0x01, 0x05, 0x42, 0x05, 0x80, 0x00, 0x2e, 0x02, 0x42, 0x3e, 0x80, 0x00, 0x2e, 0x06, 0x42, 0x02, 0x30, 0x90,
|
||||||
|
0x6f, 0x3e, 0x88, 0x01, 0x40, 0x04, 0x41, 0x4c, 0x28, 0x01, 0x42, 0x07, 0x80, 0x10, 0x25, 0x24, 0x40, 0x00, 0x40,
|
||||||
|
0x00, 0xa8, 0xf5, 0x22, 0x23, 0x29, 0x44, 0x42, 0x7a, 0x82, 0x7e, 0x88, 0x43, 0x40, 0x04, 0x41, 0x00, 0xab, 0xf5,
|
||||||
|
0x23, 0xdf, 0x28, 0x43, 0x42, 0xd9, 0xa0, 0x14, 0x2f, 0x00, 0x90, 0x02, 0x2f, 0xd2, 0x6f, 0x81, 0xb2, 0x05, 0x2f,
|
||||||
|
0x63, 0x54, 0x06, 0x28, 0x90, 0x42, 0x85, 0x42, 0x09, 0x2c, 0x02, 0x30, 0x5b, 0x50, 0x03, 0x80, 0x29, 0x2e, 0x7e,
|
||||||
|
0x01, 0x2b, 0x2e, 0x82, 0x01, 0x05, 0x42, 0x12, 0x30, 0x2b, 0x2e, 0x83, 0x01, 0x45, 0x82, 0x00, 0x2e, 0x40, 0x40,
|
||||||
|
0x7a, 0x82, 0x02, 0xa0, 0x08, 0x2f, 0x63, 0x50, 0x3b, 0x30, 0x15, 0x42, 0x05, 0x42, 0x37, 0x80, 0x37, 0x2e, 0x7e,
|
||||||
|
0x01, 0x05, 0x42, 0x12, 0x30, 0x01, 0x2e, 0xc9, 0x00, 0x02, 0x8c, 0x40, 0x40, 0x84, 0x41, 0x7a, 0x8c, 0x04, 0x0f,
|
||||||
|
0x03, 0x2f, 0x01, 0x2e, 0x8b, 0x01, 0x19, 0xa4, 0x04, 0x2f, 0x2b, 0x2e, 0x82, 0x01, 0x98, 0x2e, 0xf3, 0x03, 0x12,
|
||||||
|
0x30, 0x81, 0x90, 0x61, 0x52, 0x08, 0x2f, 0x65, 0x42, 0x65, 0x42, 0x43, 0x80, 0x39, 0x84, 0x82, 0x88, 0x05, 0x42,
|
||||||
|
0x45, 0x42, 0x85, 0x42, 0x05, 0x43, 0x00, 0x2e, 0x80, 0x41, 0x00, 0x90, 0x90, 0x2e, 0xe1, 0xb4, 0x65, 0x54, 0xc1,
|
||||||
|
0x6f, 0x80, 0x40, 0x00, 0xb2, 0x43, 0x58, 0x69, 0x50, 0x44, 0x2f, 0x55, 0x5c, 0xb7, 0x87, 0x8c, 0x0f, 0x0d, 0x2e,
|
||||||
|
0x96, 0x01, 0xc4, 0x40, 0x36, 0x2f, 0x41, 0x56, 0x8b, 0x0e, 0x2a, 0x2f, 0x0b, 0x52, 0xa1, 0x0e, 0x0a, 0x2f, 0x05,
|
||||||
|
0x2e, 0x8f, 0x01, 0x14, 0x25, 0x98, 0x2e, 0xfe, 0xc9, 0x4b, 0x54, 0x02, 0x0f, 0x69, 0x50, 0x05, 0x30, 0x65, 0x54,
|
||||||
|
0x15, 0x2f, 0x03, 0x2e, 0x8e, 0x01, 0x4d, 0x5c, 0x8e, 0x0f, 0x3a, 0x2f, 0x05, 0x2e, 0x8f, 0x01, 0x98, 0x2e, 0xfe,
|
||||||
|
0xc9, 0x4f, 0x54, 0x82, 0x0f, 0x05, 0x30, 0x69, 0x50, 0x65, 0x54, 0x30, 0x2f, 0x6d, 0x52, 0x15, 0x30, 0x42, 0x8c,
|
||||||
|
0x45, 0x42, 0x04, 0x30, 0x2b, 0x2c, 0x84, 0x43, 0x6b, 0x52, 0x42, 0x8c, 0x00, 0x2e, 0x85, 0x43, 0x15, 0x30, 0x24,
|
||||||
|
0x2c, 0x45, 0x42, 0x8e, 0x0f, 0x20, 0x2f, 0x0d, 0x2e, 0x8e, 0x01, 0xb1, 0x0e, 0x1c, 0x2f, 0x23, 0x2e, 0x8e, 0x01,
|
||||||
|
0x1a, 0x2d, 0x0e, 0x0e, 0x17, 0x2f, 0xa1, 0x0f, 0x15, 0x2f, 0x23, 0x2e, 0x8d, 0x01, 0x13, 0x2d, 0x98, 0x2e, 0x74,
|
||||||
|
0xc0, 0x43, 0x54, 0xc2, 0x0e, 0x0a, 0x2f, 0x65, 0x50, 0x04, 0x80, 0x0b, 0x30, 0x06, 0x82, 0x0b, 0x42, 0x79, 0x80,
|
||||||
|
0x41, 0x40, 0x12, 0x30, 0x25, 0x2e, 0x8c, 0x01, 0x01, 0x42, 0x05, 0x30, 0x69, 0x50, 0x65, 0x54, 0x84, 0x82, 0x43,
|
||||||
|
0x84, 0xbe, 0x8c, 0x84, 0x40, 0x86, 0x41, 0x26, 0x29, 0x94, 0x42, 0xbe, 0x8e, 0xd5, 0x7f, 0x19, 0xa1, 0x43, 0x40,
|
||||||
|
0x0b, 0x2e, 0x8c, 0x01, 0x84, 0x40, 0xc7, 0x41, 0x5d, 0x29, 0x27, 0x29, 0x45, 0x42, 0x84, 0x42, 0xc2, 0x7f, 0x01,
|
||||||
|
0x2f, 0xc0, 0xb3, 0x1d, 0x2f, 0x05, 0x2e, 0x94, 0x01, 0x99, 0xa0, 0x01, 0x2f, 0x80, 0xb3, 0x13, 0x2f, 0x80, 0xb3,
|
||||||
|
0x18, 0x2f, 0xc0, 0xb3, 0x16, 0x2f, 0x12, 0x40, 0x01, 0x40, 0x92, 0x7f, 0x98, 0x2e, 0x74, 0xc0, 0x92, 0x6f, 0x10,
|
||||||
|
0x0f, 0x20, 0x30, 0x03, 0x2f, 0x10, 0x30, 0x21, 0x2e, 0x7e, 0x01, 0x0a, 0x2d, 0x21, 0x2e, 0x7e, 0x01, 0x07, 0x2d,
|
||||||
|
0x20, 0x30, 0x21, 0x2e, 0x7e, 0x01, 0x03, 0x2d, 0x10, 0x30, 0x21, 0x2e, 0x7e, 0x01, 0xc2, 0x6f, 0x01, 0x2e, 0xc9,
|
||||||
|
0x00, 0xbc, 0x84, 0x02, 0x80, 0x82, 0x40, 0x00, 0x40, 0x90, 0x0e, 0xd5, 0x6f, 0x02, 0x2f, 0x15, 0x30, 0x98, 0x2e,
|
||||||
|
0xf3, 0x03, 0x41, 0x91, 0x05, 0x30, 0x07, 0x2f, 0x67, 0x50, 0x3d, 0x80, 0x2b, 0x2e, 0x8f, 0x01, 0x05, 0x42, 0x04,
|
||||||
|
0x80, 0x00, 0x2e, 0x05, 0x42, 0x02, 0x2c, 0x00, 0x30, 0x00, 0x30, 0xa2, 0x6f, 0x98, 0x8a, 0x86, 0x40, 0x80, 0xa7,
|
||||||
|
0x05, 0x2f, 0x98, 0x2e, 0xf3, 0x03, 0xc0, 0x30, 0x21, 0x2e, 0x95, 0x01, 0x06, 0x25, 0x1a, 0x25, 0xe2, 0x6f, 0x76,
|
||||||
|
0x82, 0x96, 0x40, 0x56, 0x43, 0x51, 0x0e, 0xfb, 0x2f, 0xbb, 0x6f, 0x30, 0x5f, 0xb8, 0x2e, 0x01, 0x2e, 0xb8, 0x00,
|
||||||
|
0x01, 0x31, 0x41, 0x08, 0x40, 0xb2, 0x20, 0x50, 0xf2, 0x30, 0x02, 0x08, 0xfb, 0x7f, 0x01, 0x30, 0x10, 0x2f, 0x05,
|
||||||
|
0x2e, 0xcc, 0x00, 0x81, 0x90, 0xe0, 0x7f, 0x03, 0x2f, 0x23, 0x2e, 0xcc, 0x00, 0x98, 0x2e, 0x55, 0xb6, 0x98, 0x2e,
|
||||||
|
0x1d, 0xb5, 0x10, 0x25, 0xfb, 0x6f, 0xe0, 0x6f, 0xe0, 0x5f, 0x80, 0x2e, 0x95, 0xcf, 0x98, 0x2e, 0x95, 0xcf, 0x10,
|
||||||
|
0x30, 0x21, 0x2e, 0xcc, 0x00, 0xfb, 0x6f, 0xe0, 0x5f, 0xb8, 0x2e, 0x00, 0x51, 0x05, 0x58, 0xeb, 0x7f, 0x2a, 0x25,
|
||||||
|
0x89, 0x52, 0x6f, 0x5a, 0x89, 0x50, 0x13, 0x41, 0x06, 0x40, 0xb3, 0x01, 0x16, 0x42, 0xcb, 0x16, 0x06, 0x40, 0xf3,
|
||||||
|
0x02, 0x13, 0x42, 0x65, 0x0e, 0xf5, 0x2f, 0x05, 0x40, 0x14, 0x30, 0x2c, 0x29, 0x04, 0x42, 0x08, 0xa1, 0x00, 0x30,
|
||||||
|
0x90, 0x2e, 0x52, 0xb6, 0xb3, 0x88, 0xb0, 0x8a, 0xb6, 0x84, 0xa4, 0x7f, 0xc4, 0x7f, 0xb5, 0x7f, 0xd5, 0x7f, 0x92,
|
||||||
|
0x7f, 0x73, 0x30, 0x04, 0x30, 0x55, 0x40, 0x42, 0x40, 0x8a, 0x17, 0xf3, 0x08, 0x6b, 0x01, 0x90, 0x02, 0x53, 0xb8,
|
||||||
|
0x4b, 0x82, 0xad, 0xbe, 0x71, 0x7f, 0x45, 0x0a, 0x09, 0x54, 0x84, 0x7f, 0x98, 0x2e, 0xd9, 0xc0, 0xa3, 0x6f, 0x7b,
|
||||||
|
0x54, 0xd0, 0x42, 0xa3, 0x7f, 0xf2, 0x7f, 0x60, 0x7f, 0x20, 0x25, 0x71, 0x6f, 0x75, 0x5a, 0x77, 0x58, 0x79, 0x5c,
|
||||||
|
0x75, 0x56, 0x98, 0x2e, 0x67, 0xcc, 0xb1, 0x6f, 0x62, 0x6f, 0x50, 0x42, 0xb1, 0x7f, 0xb3, 0x30, 0x10, 0x25, 0x98,
|
||||||
|
0x2e, 0x0f, 0xca, 0x84, 0x6f, 0x20, 0x29, 0x71, 0x6f, 0x92, 0x6f, 0xa5, 0x6f, 0x76, 0x82, 0x6a, 0x0e, 0x73, 0x30,
|
||||||
|
0x00, 0x30, 0xd0, 0x2f, 0xd2, 0x6f, 0xd1, 0x7f, 0xb4, 0x7f, 0x98, 0x2e, 0x2b, 0xb7, 0x15, 0xbd, 0x0b, 0xb8, 0x02,
|
||||||
|
0x0a, 0xc2, 0x6f, 0xc0, 0x7f, 0x98, 0x2e, 0x2b, 0xb7, 0x15, 0xbd, 0x0b, 0xb8, 0x42, 0x0a, 0xc0, 0x6f, 0x08, 0x17,
|
||||||
|
0x41, 0x18, 0x89, 0x16, 0xe1, 0x18, 0xd0, 0x18, 0xa1, 0x7f, 0x27, 0x25, 0x16, 0x25, 0x98, 0x2e, 0x79, 0xc0, 0x8b,
|
||||||
|
0x54, 0x90, 0x7f, 0xb3, 0x30, 0x82, 0x40, 0x80, 0x90, 0x0d, 0x2f, 0x7d, 0x52, 0x92, 0x6f, 0x98, 0x2e, 0x0f, 0xca,
|
||||||
|
0xb2, 0x6f, 0x90, 0x0e, 0x06, 0x2f, 0x8b, 0x50, 0x14, 0x30, 0x42, 0x6f, 0x51, 0x6f, 0x14, 0x42, 0x12, 0x42, 0x01,
|
||||||
|
0x42, 0x00, 0x2e, 0x31, 0x6f, 0x98, 0x2e, 0x74, 0xc0, 0x41, 0x6f, 0x80, 0x7f, 0x98, 0x2e, 0x74, 0xc0, 0x82, 0x6f,
|
||||||
|
0x10, 0x04, 0x43, 0x52, 0x01, 0x0f, 0x05, 0x2e, 0xcb, 0x00, 0x00, 0x30, 0x04, 0x30, 0x21, 0x2f, 0x51, 0x6f, 0x43,
|
||||||
|
0x58, 0x8c, 0x0e, 0x04, 0x30, 0x1c, 0x2f, 0x85, 0x88, 0x41, 0x6f, 0x04, 0x41, 0x8c, 0x0f, 0x04, 0x30, 0x16, 0x2f,
|
||||||
|
0x84, 0x88, 0x00, 0x2e, 0x04, 0x41, 0x04, 0x05, 0x8c, 0x0e, 0x04, 0x30, 0x0f, 0x2f, 0x82, 0x88, 0x31, 0x6f, 0x04,
|
||||||
|
0x41, 0x04, 0x05, 0x8c, 0x0e, 0x04, 0x30, 0x08, 0x2f, 0x83, 0x88, 0x00, 0x2e, 0x04, 0x41, 0x8c, 0x0f, 0x04, 0x30,
|
||||||
|
0x02, 0x2f, 0x21, 0x2e, 0xad, 0x01, 0x14, 0x30, 0x00, 0x91, 0x14, 0x2f, 0x03, 0x2e, 0xa1, 0x01, 0x41, 0x90, 0x0e,
|
||||||
|
0x2f, 0x03, 0x2e, 0xad, 0x01, 0x14, 0x30, 0x4c, 0x28, 0x23, 0x2e, 0xad, 0x01, 0x46, 0xa0, 0x06, 0x2f, 0x81, 0x84,
|
||||||
|
0x8d, 0x52, 0x48, 0x82, 0x82, 0x40, 0x21, 0x2e, 0xa1, 0x01, 0x42, 0x42, 0x5c, 0x2c, 0x02, 0x30, 0x05, 0x2e, 0xaa,
|
||||||
|
0x01, 0x80, 0xb2, 0x02, 0x30, 0x55, 0x2f, 0x03, 0x2e, 0xa9, 0x01, 0x92, 0x6f, 0xb3, 0x30, 0x98, 0x2e, 0x0f, 0xca,
|
||||||
|
0xb2, 0x6f, 0x90, 0x0f, 0x00, 0x30, 0x02, 0x30, 0x4a, 0x2f, 0xa2, 0x6f, 0x87, 0x52, 0x91, 0x00, 0x85, 0x52, 0x51,
|
||||||
|
0x0e, 0x02, 0x2f, 0x00, 0x2e, 0x43, 0x2c, 0x02, 0x30, 0xc2, 0x6f, 0x7f, 0x52, 0x91, 0x0e, 0x02, 0x30, 0x3c, 0x2f,
|
||||||
|
0x51, 0x6f, 0x81, 0x54, 0x98, 0x2e, 0xfe, 0xc9, 0x10, 0x25, 0xb3, 0x30, 0x21, 0x25, 0x98, 0x2e, 0x0f, 0xca, 0x32,
|
||||||
|
0x6f, 0xc0, 0x7f, 0xb3, 0x30, 0x12, 0x25, 0x98, 0x2e, 0x0f, 0xca, 0x42, 0x6f, 0xb0, 0x7f, 0xb3, 0x30, 0x12, 0x25,
|
||||||
|
0x98, 0x2e, 0x0f, 0xca, 0xb2, 0x6f, 0x90, 0x28, 0x83, 0x52, 0x98, 0x2e, 0xfe, 0xc9, 0xc2, 0x6f, 0x90, 0x0f, 0x00,
|
||||||
|
0x30, 0x02, 0x30, 0x1d, 0x2f, 0x05, 0x2e, 0xa1, 0x01, 0x80, 0xb2, 0x12, 0x30, 0x0f, 0x2f, 0x42, 0x6f, 0x03, 0x2e,
|
||||||
|
0xab, 0x01, 0x91, 0x0e, 0x02, 0x30, 0x12, 0x2f, 0x52, 0x6f, 0x03, 0x2e, 0xac, 0x01, 0x91, 0x0f, 0x02, 0x30, 0x0c,
|
||||||
|
0x2f, 0x21, 0x2e, 0xaa, 0x01, 0x0a, 0x2c, 0x12, 0x30, 0x03, 0x2e, 0xcb, 0x00, 0x8d, 0x58, 0x08, 0x89, 0x41, 0x40,
|
||||||
|
0x11, 0x43, 0x00, 0x43, 0x25, 0x2e, 0xa1, 0x01, 0xd4, 0x6f, 0x8f, 0x52, 0x00, 0x43, 0x3a, 0x89, 0x00, 0x2e, 0x10,
|
||||||
|
0x43, 0x10, 0x43, 0x61, 0x0e, 0xfb, 0x2f, 0x03, 0x2e, 0xa0, 0x01, 0x11, 0x1a, 0x02, 0x2f, 0x02, 0x25, 0x21, 0x2e,
|
||||||
|
0xa0, 0x01, 0xeb, 0x6f, 0x00, 0x5f, 0xb8, 0x2e, 0x91, 0x52, 0x10, 0x30, 0x02, 0x30, 0x95, 0x56, 0x52, 0x42, 0x4b,
|
||||||
|
0x0e, 0xfc, 0x2f, 0x8d, 0x54, 0x88, 0x82, 0x93, 0x56, 0x80, 0x42, 0x53, 0x42, 0x40, 0x42, 0x42, 0x86, 0x83, 0x54,
|
||||||
|
0xc0, 0x2e, 0xc2, 0x42, 0x00, 0x2e, 0xa3, 0x52, 0x00, 0x51, 0x52, 0x40, 0x47, 0x40, 0x1a, 0x25, 0x01, 0x2e, 0x97,
|
||||||
|
0x00, 0x8f, 0xbe, 0x72, 0x86, 0xfb, 0x7f, 0x0b, 0x30, 0x7c, 0xbf, 0xa5, 0x50, 0x10, 0x08, 0xdf, 0xba, 0x70, 0x88,
|
||||||
|
0xf8, 0xbf, 0xcb, 0x42, 0xd3, 0x7f, 0x6c, 0xbb, 0xfc, 0xbb, 0xc5, 0x0a, 0x90, 0x7f, 0x1b, 0x7f, 0x0b, 0x43, 0xc0,
|
||||||
|
0xb2, 0xe5, 0x7f, 0xb7, 0x7f, 0xa6, 0x7f, 0xc4, 0x7f, 0x90, 0x2e, 0x1c, 0xb7, 0x07, 0x2e, 0xd2, 0x00, 0xc0, 0xb2,
|
||||||
|
0x0b, 0x2f, 0x97, 0x52, 0x01, 0x2e, 0xcd, 0x00, 0x82, 0x7f, 0x98, 0x2e, 0xbb, 0xcc, 0x0b, 0x30, 0x37, 0x2e, 0xd2,
|
||||||
|
0x00, 0x82, 0x6f, 0x90, 0x6f, 0x1a, 0x25, 0x00, 0xb2, 0x8b, 0x7f, 0x14, 0x2f, 0xa6, 0xbd, 0x25, 0xbd, 0xb6, 0xb9,
|
||||||
|
0x2f, 0xb9, 0x80, 0xb2, 0xd4, 0xb0, 0x0c, 0x2f, 0x99, 0x54, 0x9b, 0x56, 0x0b, 0x30, 0x0b, 0x2e, 0xb1, 0x00, 0xa1,
|
||||||
|
0x58, 0x9b, 0x42, 0xdb, 0x42, 0x6c, 0x09, 0x2b, 0x2e, 0xb1, 0x00, 0x8b, 0x42, 0xcb, 0x42, 0x86, 0x7f, 0x73, 0x84,
|
||||||
|
0xa7, 0x56, 0xc3, 0x08, 0x39, 0x52, 0x05, 0x50, 0x72, 0x7f, 0x63, 0x7f, 0x98, 0x2e, 0xc2, 0xc0, 0xe1, 0x6f, 0x62,
|
||||||
|
0x6f, 0xd1, 0x0a, 0x01, 0x2e, 0xcd, 0x00, 0xd5, 0x6f, 0xc4, 0x6f, 0x72, 0x6f, 0x97, 0x52, 0x9d, 0x5c, 0x98, 0x2e,
|
||||||
|
0x06, 0xcd, 0x23, 0x6f, 0x90, 0x6f, 0x99, 0x52, 0xc0, 0xb2, 0x04, 0xbd, 0x54, 0x40, 0xaf, 0xb9, 0x45, 0x40, 0xe1,
|
||||||
|
0x7f, 0x02, 0x30, 0x06, 0x2f, 0xc0, 0xb2, 0x02, 0x30, 0x03, 0x2f, 0x9b, 0x5c, 0x12, 0x30, 0x94, 0x43, 0x85, 0x43,
|
||||||
|
0x03, 0xbf, 0x6f, 0xbb, 0x80, 0xb3, 0x20, 0x2f, 0x06, 0x6f, 0x26, 0x01, 0x16, 0x6f, 0x6e, 0x03, 0x45, 0x42, 0xc0,
|
||||||
|
0x90, 0x29, 0x2e, 0xce, 0x00, 0x9b, 0x52, 0x14, 0x2f, 0x9b, 0x5c, 0x00, 0x2e, 0x93, 0x41, 0x86, 0x41, 0xe3, 0x04,
|
||||||
|
0xae, 0x07, 0x80, 0xab, 0x04, 0x2f, 0x80, 0x91, 0x0a, 0x2f, 0x86, 0x6f, 0x73, 0x0f, 0x07, 0x2f, 0x83, 0x6f, 0xc0,
|
||||||
|
0xb2, 0x04, 0x2f, 0x54, 0x42, 0x45, 0x42, 0x12, 0x30, 0x04, 0x2c, 0x11, 0x30, 0x02, 0x2c, 0x11, 0x30, 0x11, 0x30,
|
||||||
|
0x02, 0xbc, 0x0f, 0xb8, 0xd2, 0x7f, 0x00, 0xb2, 0x0a, 0x2f, 0x01, 0x2e, 0xfc, 0x00, 0x05, 0x2e, 0xc7, 0x01, 0x10,
|
||||||
|
0x1a, 0x02, 0x2f, 0x21, 0x2e, 0xc7, 0x01, 0x03, 0x2d, 0x02, 0x2c, 0x01, 0x30, 0x01, 0x30, 0xb0, 0x6f, 0x98, 0x2e,
|
||||||
|
0x95, 0xcf, 0xd1, 0x6f, 0xa0, 0x6f, 0x98, 0x2e, 0x95, 0xcf, 0xe2, 0x6f, 0x9f, 0x52, 0x01, 0x2e, 0xce, 0x00, 0x82,
|
||||||
|
0x40, 0x50, 0x42, 0x0c, 0x2c, 0x42, 0x42, 0x11, 0x30, 0x23, 0x2e, 0xd2, 0x00, 0x01, 0x30, 0xb0, 0x6f, 0x98, 0x2e,
|
||||||
|
0x95, 0xcf, 0xa0, 0x6f, 0x01, 0x30, 0x98, 0x2e, 0x95, 0xcf, 0x00, 0x2e, 0xfb, 0x6f, 0x00, 0x5f, 0xb8, 0x2e, 0x83,
|
||||||
|
0x86, 0x01, 0x30, 0x00, 0x30, 0x94, 0x40, 0x24, 0x18, 0x06, 0x00, 0x53, 0x0e, 0x4f, 0x02, 0xf9, 0x2f, 0xb8, 0x2e,
|
||||||
|
0xa9, 0x52, 0x00, 0x2e, 0x60, 0x40, 0x41, 0x40, 0x0d, 0xbc, 0x98, 0xbc, 0xc0, 0x2e, 0x01, 0x0a, 0x0f, 0xb8, 0xab,
|
||||||
|
0x52, 0x53, 0x3c, 0x52, 0x40, 0x40, 0x40, 0x4b, 0x00, 0x82, 0x16, 0x26, 0xb9, 0x01, 0xb8, 0x41, 0x40, 0x10, 0x08,
|
||||||
|
0x97, 0xb8, 0x01, 0x08, 0xc0, 0x2e, 0x11, 0x30, 0x01, 0x08, 0x43, 0x86, 0x25, 0x40, 0x04, 0x40, 0xd8, 0xbe, 0x2c,
|
||||||
|
0x0b, 0x22, 0x11, 0x54, 0x42, 0x03, 0x80, 0x4b, 0x0e, 0xf6, 0x2f, 0xb8, 0x2e, 0x9f, 0x50, 0x10, 0x50, 0xad, 0x52,
|
||||||
|
0x05, 0x2e, 0xd3, 0x00, 0xfb, 0x7f, 0x00, 0x2e, 0x13, 0x40, 0x93, 0x42, 0x41, 0x0e, 0xfb, 0x2f, 0x98, 0x2e, 0xa5,
|
||||||
|
0xb7, 0x98, 0x2e, 0x87, 0xcf, 0x01, 0x2e, 0xd9, 0x00, 0x00, 0xb2, 0xfb, 0x6f, 0x0b, 0x2f, 0x01, 0x2e, 0x69, 0xf7,
|
||||||
|
0xb1, 0x3f, 0x01, 0x08, 0x01, 0x30, 0xf0, 0x5f, 0x23, 0x2e, 0xd9, 0x00, 0x21, 0x2e, 0x69, 0xf7, 0x80, 0x2e, 0x7a,
|
||||||
|
0xb7, 0xf0, 0x5f, 0xb8, 0x2e, 0x01, 0x2e, 0xc0, 0xf8, 0x03, 0x2e, 0xfc, 0xf5, 0x15, 0x54, 0xaf, 0x56, 0x82, 0x08,
|
||||||
|
0x0b, 0x2e, 0x69, 0xf7, 0xcb, 0x0a, 0xb1, 0x58, 0x80, 0x90, 0xdd, 0xbe, 0x4c, 0x08, 0x5f, 0xb9, 0x59, 0x22, 0x80,
|
||||||
|
0x90, 0x07, 0x2f, 0x03, 0x34, 0xc3, 0x08, 0xf2, 0x3a, 0x0a, 0x08, 0x02, 0x35, 0xc0, 0x90, 0x4a, 0x0a, 0x48, 0x22,
|
||||||
|
0xc0, 0x2e, 0x23, 0x2e, 0xfc, 0xf5, 0x10, 0x50, 0xfb, 0x7f, 0x98, 0x2e, 0x56, 0xc7, 0x98, 0x2e, 0x49, 0xc3, 0x10,
|
||||||
|
0x30, 0xfb, 0x6f, 0xf0, 0x5f, 0x21, 0x2e, 0xcc, 0x00, 0x21, 0x2e, 0xca, 0x00, 0xb8, 0x2e, 0x03, 0x2e, 0xd3, 0x00,
|
||||||
|
0x16, 0xb8, 0x02, 0x34, 0x4a, 0x0c, 0x21, 0x2e, 0x2d, 0xf5, 0xc0, 0x2e, 0x23, 0x2e, 0xd3, 0x00, 0x03, 0xbc, 0x21,
|
||||||
|
0x2e, 0xd5, 0x00, 0x03, 0x2e, 0xd5, 0x00, 0x40, 0xb2, 0x10, 0x30, 0x21, 0x2e, 0x77, 0x00, 0x01, 0x30, 0x05, 0x2f,
|
||||||
|
0x05, 0x2e, 0xd8, 0x00, 0x80, 0x90, 0x01, 0x2f, 0x23, 0x2e, 0x6f, 0xf5, 0xc0, 0x2e, 0x21, 0x2e, 0xd9, 0x00, 0x11,
|
||||||
|
0x30, 0x81, 0x08, 0x01, 0x2e, 0x6a, 0xf7, 0x71, 0x3f, 0x23, 0xbd, 0x01, 0x08, 0x02, 0x0a, 0xc0, 0x2e, 0x21, 0x2e,
|
||||||
|
0x6a, 0xf7, 0x30, 0x25, 0x00, 0x30, 0x21, 0x2e, 0x5a, 0xf5, 0x10, 0x50, 0x21, 0x2e, 0x7b, 0x00, 0x21, 0x2e, 0x7c,
|
||||||
|
0x00, 0xfb, 0x7f, 0x98, 0x2e, 0xc3, 0xb7, 0x40, 0x30, 0x21, 0x2e, 0xd4, 0x00, 0xfb, 0x6f, 0xf0, 0x5f, 0x03, 0x25,
|
||||||
|
0x80, 0x2e, 0xaf, 0xb7, 0x80, 0x2e, 0x00, 0xc1, 0x80, 0x2e, 0x00, 0xc1, 0x80, 0x2e, 0x00, 0xc1, 0x80, 0x2e, 0x00,
|
||||||
|
0xc1, 0x80, 0x2e, 0x00, 0xc1, 0x80, 0x2e, 0x00, 0xc1, 0x80, 0x2e, 0x00, 0xc1, 0x80, 0x2e, 0x00, 0xc1, 0x80, 0x2e,
|
||||||
|
0x00, 0xc1, 0x80, 0x2e, 0x00, 0xc1, 0x80, 0x2e, 0x00, 0xc1, 0x80, 0x2e, 0x00, 0xc1, 0x80, 0x2e, 0x00, 0xc1, 0x80,
|
||||||
|
0x2e, 0x00, 0xc1, 0x80, 0x2e, 0x00, 0xc1, 0x01, 0x2e, 0x5d, 0xf7, 0x08, 0xbc, 0x80, 0xac, 0x0e, 0xbb, 0x02, 0x2f,
|
||||||
|
0x00, 0x30, 0x41, 0x04, 0x82, 0x06, 0xc0, 0xa4, 0x00, 0x30, 0x11, 0x2f, 0x40, 0xa9, 0x03, 0x2f, 0x40, 0x91, 0x0d,
|
||||||
|
0x2f, 0x00, 0xa7, 0x0b, 0x2f, 0x80, 0xb3, 0xb3, 0x58, 0x02, 0x2f, 0x90, 0xa1, 0x26, 0x13, 0x20, 0x23, 0x80, 0x90,
|
||||||
|
0x10, 0x30, 0x01, 0x2f, 0xcc, 0x0e, 0x00, 0x2f, 0x00, 0x30, 0xb8, 0x2e, 0xb5, 0x50, 0x18, 0x08, 0x08, 0xbc, 0x88,
|
||||||
|
0xb6, 0x0d, 0x17, 0xc6, 0xbd, 0x56, 0xbc, 0xb7, 0x58, 0xda, 0xba, 0x04, 0x01, 0x1d, 0x0a, 0x10, 0x50, 0x05, 0x30,
|
||||||
|
0x32, 0x25, 0x45, 0x03, 0xfb, 0x7f, 0xf6, 0x30, 0x21, 0x25, 0x98, 0x2e, 0x37, 0xca, 0x16, 0xb5, 0x9a, 0xbc, 0x06,
|
||||||
|
0xb8, 0x80, 0xa8, 0x41, 0x0a, 0x0e, 0x2f, 0x80, 0x90, 0x02, 0x2f, 0x2d, 0x50, 0x48, 0x0f, 0x09, 0x2f, 0xbf, 0xa0,
|
||||||
|
0x04, 0x2f, 0xbf, 0x90, 0x06, 0x2f, 0xb7, 0x54, 0xca, 0x0f, 0x03, 0x2f, 0x00, 0x2e, 0x02, 0x2c, 0xb7, 0x52, 0x2d,
|
||||||
|
0x52, 0xf2, 0x33, 0x98, 0x2e, 0xd9, 0xc0, 0xfb, 0x6f, 0xf1, 0x37, 0xc0, 0x2e, 0x01, 0x08, 0xf0, 0x5f, 0xbf, 0x56,
|
||||||
|
0xb9, 0x54, 0xd0, 0x40, 0xc4, 0x40, 0x0b, 0x2e, 0xfd, 0xf3, 0xbf, 0x52, 0x90, 0x42, 0x94, 0x42, 0x95, 0x42, 0x05,
|
||||||
|
0x30, 0xc1, 0x50, 0x0f, 0x88, 0x06, 0x40, 0x04, 0x41, 0x96, 0x42, 0xc5, 0x42, 0x48, 0xbe, 0x73, 0x30, 0x0d, 0x2e,
|
||||||
|
0xd8, 0x00, 0x4f, 0xba, 0x84, 0x42, 0x03, 0x42, 0x81, 0xb3, 0x02, 0x2f, 0x2b, 0x2e, 0x6f, 0xf5, 0x06, 0x2d, 0x05,
|
||||||
|
0x2e, 0x77, 0xf7, 0xbd, 0x56, 0x93, 0x08, 0x25, 0x2e, 0x77, 0xf7, 0xbb, 0x54, 0x25, 0x2e, 0xc2, 0xf5, 0x07, 0x2e,
|
||||||
|
0xfd, 0xf3, 0x42, 0x30, 0xb4, 0x33, 0xda, 0x0a, 0x4c, 0x00, 0x27, 0x2e, 0xfd, 0xf3, 0x43, 0x40, 0xd4, 0x3f, 0xdc,
|
||||||
|
0x08, 0x43, 0x42, 0x00, 0x2e, 0x00, 0x2e, 0x43, 0x40, 0x24, 0x30, 0xdc, 0x0a, 0x43, 0x42, 0x04, 0x80, 0x03, 0x2e,
|
||||||
|
0xfd, 0xf3, 0x4a, 0x0a, 0x23, 0x2e, 0xfd, 0xf3, 0x61, 0x34, 0xc0, 0x2e, 0x01, 0x42, 0x00, 0x2e, 0x60, 0x50, 0x1a,
|
||||||
|
0x25, 0x7a, 0x86, 0xe0, 0x7f, 0xf3, 0x7f, 0x03, 0x25, 0xc3, 0x52, 0x41, 0x84, 0xdb, 0x7f, 0x33, 0x30, 0x98, 0x2e,
|
||||||
|
0x16, 0xc2, 0x1a, 0x25, 0x7d, 0x82, 0xf0, 0x6f, 0xe2, 0x6f, 0x32, 0x25, 0x16, 0x40, 0x94, 0x40, 0x26, 0x01, 0x85,
|
||||||
|
0x40, 0x8e, 0x17, 0xc4, 0x42, 0x6e, 0x03, 0x95, 0x42, 0x41, 0x0e, 0xf4, 0x2f, 0xdb, 0x6f, 0xa0, 0x5f, 0xb8, 0x2e,
|
||||||
|
0xb0, 0x51, 0xfb, 0x7f, 0x98, 0x2e, 0xe8, 0x0d, 0x5a, 0x25, 0x98, 0x2e, 0x0f, 0x0e, 0xcb, 0x58, 0x32, 0x87, 0xc4,
|
||||||
|
0x7f, 0x65, 0x89, 0x6b, 0x8d, 0xc5, 0x5a, 0x65, 0x7f, 0xe1, 0x7f, 0x83, 0x7f, 0xa6, 0x7f, 0x74, 0x7f, 0xd0, 0x7f,
|
||||||
|
0xb6, 0x7f, 0x94, 0x7f, 0x17, 0x30, 0xc7, 0x52, 0xc9, 0x54, 0x51, 0x7f, 0x00, 0x2e, 0x85, 0x6f, 0x42, 0x7f, 0x00,
|
||||||
|
0x2e, 0x51, 0x41, 0x45, 0x81, 0x42, 0x41, 0x13, 0x40, 0x3b, 0x8a, 0x00, 0x40, 0x4b, 0x04, 0xd0, 0x06, 0xc0, 0xac,
|
||||||
|
0x85, 0x7f, 0x02, 0x2f, 0x02, 0x30, 0x51, 0x04, 0xd3, 0x06, 0x41, 0x84, 0x05, 0x30, 0x5d, 0x02, 0xc9, 0x16, 0xdf,
|
||||||
|
0x08, 0xd3, 0x00, 0x8d, 0x02, 0xaf, 0xbc, 0xb1, 0xb9, 0x59, 0x0a, 0x65, 0x6f, 0x11, 0x43, 0xa1, 0xb4, 0x52, 0x41,
|
||||||
|
0x53, 0x41, 0x01, 0x43, 0x34, 0x7f, 0x65, 0x7f, 0x26, 0x31, 0xe5, 0x6f, 0xd4, 0x6f, 0x98, 0x2e, 0x37, 0xca, 0x32,
|
||||||
|
0x6f, 0x75, 0x6f, 0x83, 0x40, 0x42, 0x41, 0x23, 0x7f, 0x12, 0x7f, 0xf6, 0x30, 0x40, 0x25, 0x51, 0x25, 0x98, 0x2e,
|
||||||
|
0x37, 0xca, 0x14, 0x6f, 0x20, 0x05, 0x70, 0x6f, 0x25, 0x6f, 0x69, 0x07, 0xa2, 0x6f, 0x31, 0x6f, 0x0b, 0x30, 0x04,
|
||||||
|
0x42, 0x9b, 0x42, 0x8b, 0x42, 0x55, 0x42, 0x32, 0x7f, 0x40, 0xa9, 0xc3, 0x6f, 0x71, 0x7f, 0x02, 0x30, 0xd0, 0x40,
|
||||||
|
0xc3, 0x7f, 0x03, 0x2f, 0x40, 0x91, 0x15, 0x2f, 0x00, 0xa7, 0x13, 0x2f, 0x00, 0xa4, 0x11, 0x2f, 0x84, 0xbd, 0x98,
|
||||||
|
0x2e, 0x79, 0xca, 0x55, 0x6f, 0xb7, 0x54, 0x54, 0x41, 0x82, 0x00, 0xf3, 0x3f, 0x45, 0x41, 0xcb, 0x02, 0xf6, 0x30,
|
||||||
|
0x98, 0x2e, 0x37, 0xca, 0x35, 0x6f, 0xa4, 0x6f, 0x41, 0x43, 0x03, 0x2c, 0x00, 0x43, 0xa4, 0x6f, 0x35, 0x6f, 0x17,
|
||||||
|
0x30, 0x42, 0x6f, 0x51, 0x6f, 0x93, 0x40, 0x42, 0x82, 0x00, 0x41, 0xc3, 0x00, 0x03, 0x43, 0x51, 0x7f, 0x00, 0x2e,
|
||||||
|
0x94, 0x40, 0x41, 0x41, 0x4c, 0x02, 0xc4, 0x6f, 0xd1, 0x56, 0x63, 0x0e, 0x74, 0x6f, 0x51, 0x43, 0xa5, 0x7f, 0x8a,
|
||||||
|
0x2f, 0x09, 0x2e, 0xd8, 0x00, 0x01, 0xb3, 0x21, 0x2f, 0xcb, 0x58, 0x90, 0x6f, 0x13, 0x41, 0xb6, 0x6f, 0xe4, 0x7f,
|
||||||
|
0x00, 0x2e, 0x91, 0x41, 0x14, 0x40, 0x92, 0x41, 0x15, 0x40, 0x17, 0x2e, 0x6f, 0xf5, 0xb6, 0x7f, 0xd0, 0x7f, 0xcb,
|
||||||
|
0x7f, 0x98, 0x2e, 0x00, 0x0c, 0x07, 0x15, 0xc2, 0x6f, 0x14, 0x0b, 0x29, 0x2e, 0x6f, 0xf5, 0xc3, 0xa3, 0xc1, 0x8f,
|
||||||
|
0xe4, 0x6f, 0xd0, 0x6f, 0xe6, 0x2f, 0x14, 0x30, 0x05, 0x2e, 0x6f, 0xf5, 0x14, 0x0b, 0x29, 0x2e, 0x6f, 0xf5, 0x18,
|
||||||
|
0x2d, 0xcd, 0x56, 0x04, 0x32, 0xb5, 0x6f, 0x1c, 0x01, 0x51, 0x41, 0x52, 0x41, 0xc3, 0x40, 0xb5, 0x7f, 0xe4, 0x7f,
|
||||||
|
0x98, 0x2e, 0x1f, 0x0c, 0xe4, 0x6f, 0x21, 0x87, 0x00, 0x43, 0x04, 0x32, 0xcf, 0x54, 0x5a, 0x0e, 0xef, 0x2f, 0x15,
|
||||||
|
0x54, 0x09, 0x2e, 0x77, 0xf7, 0x22, 0x0b, 0x29, 0x2e, 0x77, 0xf7, 0xfb, 0x6f, 0x50, 0x5e, 0xb8, 0x2e, 0x10, 0x50,
|
||||||
|
0x01, 0x2e, 0xd4, 0x00, 0x00, 0xb2, 0xfb, 0x7f, 0x51, 0x2f, 0x01, 0xb2, 0x48, 0x2f, 0x02, 0xb2, 0x42, 0x2f, 0x03,
|
||||||
|
0x90, 0x56, 0x2f, 0xd7, 0x52, 0x79, 0x80, 0x42, 0x40, 0x81, 0x84, 0x00, 0x40, 0x42, 0x42, 0x98, 0x2e, 0x93, 0x0c,
|
||||||
|
0xd9, 0x54, 0xd7, 0x50, 0xa1, 0x40, 0x98, 0xbd, 0x82, 0x40, 0x3e, 0x82, 0xda, 0x0a, 0x44, 0x40, 0x8b, 0x16, 0xe3,
|
||||||
|
0x00, 0x53, 0x42, 0x00, 0x2e, 0x43, 0x40, 0x9a, 0x02, 0x52, 0x42, 0x00, 0x2e, 0x41, 0x40, 0x15, 0x54, 0x4a, 0x0e,
|
||||||
|
0x3a, 0x2f, 0x3a, 0x82, 0x00, 0x30, 0x41, 0x40, 0x21, 0x2e, 0x85, 0x0f, 0x40, 0xb2, 0x0a, 0x2f, 0x98, 0x2e, 0xb1,
|
||||||
|
0x0c, 0x98, 0x2e, 0x45, 0x0e, 0x98, 0x2e, 0x5b, 0x0e, 0xfb, 0x6f, 0xf0, 0x5f, 0x00, 0x30, 0x80, 0x2e, 0xce, 0xb7,
|
||||||
|
0xdd, 0x52, 0xd3, 0x54, 0x42, 0x42, 0x4f, 0x84, 0x73, 0x30, 0xdb, 0x52, 0x83, 0x42, 0x1b, 0x30, 0x6b, 0x42, 0x23,
|
||||||
|
0x30, 0x27, 0x2e, 0xd7, 0x00, 0x37, 0x2e, 0xd4, 0x00, 0x21, 0x2e, 0xd6, 0x00, 0x7a, 0x84, 0x17, 0x2c, 0x42, 0x42,
|
||||||
|
0x30, 0x30, 0x21, 0x2e, 0xd4, 0x00, 0x12, 0x2d, 0x21, 0x30, 0x00, 0x30, 0x23, 0x2e, 0xd4, 0x00, 0x21, 0x2e, 0x7b,
|
||||||
|
0xf7, 0x0b, 0x2d, 0x17, 0x30, 0x98, 0x2e, 0x51, 0x0c, 0xd5, 0x50, 0x0c, 0x82, 0x72, 0x30, 0x2f, 0x2e, 0xd4, 0x00,
|
||||||
|
0x25, 0x2e, 0x7b, 0xf7, 0x40, 0x42, 0x00, 0x2e, 0xfb, 0x6f, 0xf0, 0x5f, 0xb8, 0x2e, 0x70, 0x50, 0x0a, 0x25, 0x39,
|
||||||
|
0x86, 0xfb, 0x7f, 0xe1, 0x32, 0x62, 0x30, 0x98, 0x2e, 0xc2, 0xc4, 0xb5, 0x56, 0xa5, 0x6f, 0xab, 0x08, 0x91, 0x6f,
|
||||||
|
0x4b, 0x08, 0xdf, 0x56, 0xc4, 0x6f, 0x23, 0x09, 0x4d, 0xba, 0x93, 0xbc, 0x8c, 0x0b, 0xd1, 0x6f, 0x0b, 0x09, 0xcb,
|
||||||
|
0x52, 0xe1, 0x5e, 0x56, 0x42, 0xaf, 0x09, 0x4d, 0xba, 0x23, 0xbd, 0x94, 0x0a, 0xe5, 0x6f, 0x68, 0xbb, 0xeb, 0x08,
|
||||||
|
0xbd, 0xb9, 0x63, 0xbe, 0xfb, 0x6f, 0x52, 0x42, 0xe3, 0x0a, 0xc0, 0x2e, 0x43, 0x42, 0x90, 0x5f, 0xd1, 0x50, 0x03,
|
||||||
|
0x2e, 0x25, 0xf3, 0x13, 0x40, 0x00, 0x40, 0x9b, 0xbc, 0x9b, 0xb4, 0x08, 0xbd, 0xb8, 0xb9, 0x98, 0xbc, 0xda, 0x0a,
|
||||||
|
0x08, 0xb6, 0x89, 0x16, 0xc0, 0x2e, 0x19, 0x00, 0x62, 0x02, 0x10, 0x50, 0xfb, 0x7f, 0x98, 0x2e, 0x81, 0x0d, 0x01,
|
||||||
|
0x2e, 0xd4, 0x00, 0x31, 0x30, 0x08, 0x04, 0xfb, 0x6f, 0x01, 0x30, 0xf0, 0x5f, 0x23, 0x2e, 0xd6, 0x00, 0x21, 0x2e,
|
||||||
|
0xd7, 0x00, 0xb8, 0x2e, 0x01, 0x2e, 0xd7, 0x00, 0x03, 0x2e, 0xd6, 0x00, 0x48, 0x0e, 0x01, 0x2f, 0x80, 0x2e, 0x1f,
|
||||||
|
0x0e, 0xb8, 0x2e, 0xe3, 0x50, 0x21, 0x34, 0x01, 0x42, 0x82, 0x30, 0xc1, 0x32, 0x25, 0x2e, 0x62, 0xf5, 0x01, 0x00,
|
||||||
|
0x22, 0x30, 0x01, 0x40, 0x4a, 0x0a, 0x01, 0x42, 0xb8, 0x2e, 0xe3, 0x54, 0xf0, 0x3b, 0x83, 0x40, 0xd8, 0x08, 0xe5,
|
||||||
|
0x52, 0x83, 0x42, 0x00, 0x30, 0x83, 0x30, 0x50, 0x42, 0xc4, 0x32, 0x27, 0x2e, 0x64, 0xf5, 0x94, 0x00, 0x50, 0x42,
|
||||||
|
0x40, 0x42, 0xd3, 0x3f, 0x84, 0x40, 0x7d, 0x82, 0xe3, 0x08, 0x40, 0x42, 0x83, 0x42, 0xb8, 0x2e, 0xdd, 0x52, 0x00,
|
||||||
|
0x30, 0x40, 0x42, 0x7c, 0x86, 0xb9, 0x52, 0x09, 0x2e, 0x70, 0x0f, 0xbf, 0x54, 0xc4, 0x42, 0xd3, 0x86, 0x54, 0x40,
|
||||||
|
0x55, 0x40, 0x94, 0x42, 0x85, 0x42, 0x21, 0x2e, 0xd7, 0x00, 0x42, 0x40, 0x25, 0x2e, 0xfd, 0xf3, 0xc0, 0x42, 0x7e,
|
||||||
|
0x82, 0x05, 0x2e, 0x7d, 0x00, 0x80, 0xb2, 0x14, 0x2f, 0x05, 0x2e, 0x89, 0x00, 0x27, 0xbd, 0x2f, 0xb9, 0x80, 0x90,
|
||||||
|
0x02, 0x2f, 0x21, 0x2e, 0x6f, 0xf5, 0x0c, 0x2d, 0x07, 0x2e, 0x71, 0x0f, 0x14, 0x30, 0x1c, 0x09, 0x05, 0x2e, 0x77,
|
||||||
|
0xf7, 0xbd, 0x56, 0x47, 0xbe, 0x93, 0x08, 0x94, 0x0a, 0x25, 0x2e, 0x77, 0xf7, 0xe7, 0x54, 0x50, 0x42, 0x4a, 0x0e,
|
||||||
|
0xfc, 0x2f, 0xb8, 0x2e, 0x50, 0x50, 0x02, 0x30, 0x43, 0x86, 0xe5, 0x50, 0xfb, 0x7f, 0xe3, 0x7f, 0xd2, 0x7f, 0xc0,
|
||||||
|
0x7f, 0xb1, 0x7f, 0x00, 0x2e, 0x41, 0x40, 0x00, 0x40, 0x48, 0x04, 0x98, 0x2e, 0x74, 0xc0, 0x1e, 0xaa, 0xd3, 0x6f,
|
||||||
|
0x14, 0x30, 0xb1, 0x6f, 0xe3, 0x22, 0xc0, 0x6f, 0x52, 0x40, 0xe4, 0x6f, 0x4c, 0x0e, 0x12, 0x42, 0xd3, 0x7f, 0xeb,
|
||||||
|
0x2f, 0x03, 0x2e, 0x86, 0x0f, 0x40, 0x90, 0x11, 0x30, 0x03, 0x2f, 0x23, 0x2e, 0x86, 0x0f, 0x02, 0x2c, 0x00, 0x30,
|
||||||
|
0xd0, 0x6f, 0xfb, 0x6f, 0xb0, 0x5f, 0xb8, 0x2e, 0x40, 0x50, 0xf1, 0x7f, 0x0a, 0x25, 0x3c, 0x86, 0xeb, 0x7f, 0x41,
|
||||||
|
0x33, 0x22, 0x30, 0x98, 0x2e, 0xc2, 0xc4, 0xd3, 0x6f, 0xf4, 0x30, 0xdc, 0x09, 0x47, 0x58, 0xc2, 0x6f, 0x94, 0x09,
|
||||||
|
0xeb, 0x58, 0x6a, 0xbb, 0xdc, 0x08, 0xb4, 0xb9, 0xb1, 0xbd, 0xe9, 0x5a, 0x95, 0x08, 0x21, 0xbd, 0xf6, 0xbf, 0x77,
|
||||||
|
0x0b, 0x51, 0xbe, 0xf1, 0x6f, 0xeb, 0x6f, 0x52, 0x42, 0x54, 0x42, 0xc0, 0x2e, 0x43, 0x42, 0xc0, 0x5f, 0x50, 0x50,
|
||||||
|
0xf5, 0x50, 0x31, 0x30, 0x11, 0x42, 0xfb, 0x7f, 0x7b, 0x30, 0x0b, 0x42, 0x11, 0x30, 0x02, 0x80, 0x23, 0x33, 0x01,
|
||||||
|
0x42, 0x03, 0x00, 0x07, 0x2e, 0x80, 0x03, 0x05, 0x2e, 0xd3, 0x00, 0x23, 0x52, 0xe2, 0x7f, 0xd3, 0x7f, 0xc0, 0x7f,
|
||||||
|
0x98, 0x2e, 0xb6, 0x0e, 0xd1, 0x6f, 0x08, 0x0a, 0x1a, 0x25, 0x7b, 0x86, 0xd0, 0x7f, 0x01, 0x33, 0x12, 0x30, 0x98,
|
||||||
|
0x2e, 0xc2, 0xc4, 0xd1, 0x6f, 0x08, 0x0a, 0x00, 0xb2, 0x0d, 0x2f, 0xe3, 0x6f, 0x01, 0x2e, 0x80, 0x03, 0x51, 0x30,
|
||||||
|
0xc7, 0x86, 0x23, 0x2e, 0x21, 0xf2, 0x08, 0xbc, 0xc0, 0x42, 0x98, 0x2e, 0xa5, 0xb7, 0x00, 0x2e, 0x00, 0x2e, 0xd0,
|
||||||
|
0x2e, 0xb0, 0x6f, 0x0b, 0xb8, 0x03, 0x2e, 0x1b, 0x00, 0x08, 0x1a, 0xb0, 0x7f, 0x70, 0x30, 0x04, 0x2f, 0x21, 0x2e,
|
||||||
|
0x21, 0xf2, 0x00, 0x2e, 0x00, 0x2e, 0xd0, 0x2e, 0x98, 0x2e, 0x6d, 0xc0, 0x98, 0x2e, 0x5d, 0xc0, 0xed, 0x50, 0x98,
|
||||||
|
0x2e, 0x44, 0xcb, 0xef, 0x50, 0x98, 0x2e, 0x46, 0xc3, 0xf1, 0x50, 0x98, 0x2e, 0x53, 0xc7, 0x35, 0x50, 0x98, 0x2e,
|
||||||
|
0x64, 0xcf, 0x10, 0x30, 0x98, 0x2e, 0xdc, 0x03, 0x20, 0x26, 0xc0, 0x6f, 0x02, 0x31, 0x12, 0x42, 0xab, 0x33, 0x0b,
|
||||||
|
0x42, 0x37, 0x80, 0x01, 0x30, 0x01, 0x42, 0xf3, 0x37, 0xf7, 0x52, 0xfb, 0x50, 0x44, 0x40, 0xa2, 0x0a, 0x42, 0x42,
|
||||||
|
0x8b, 0x31, 0x09, 0x2e, 0x5e, 0xf7, 0xf9, 0x54, 0xe3, 0x08, 0x83, 0x42, 0x1b, 0x42, 0x23, 0x33, 0x4b, 0x00, 0xbc,
|
||||||
|
0x84, 0x0b, 0x40, 0x33, 0x30, 0x83, 0x42, 0x0b, 0x42, 0xe0, 0x7f, 0xd1, 0x7f, 0x98, 0x2e, 0x58, 0xb7, 0xd1, 0x6f,
|
||||||
|
0x80, 0x30, 0x40, 0x42, 0x03, 0x30, 0xe0, 0x6f, 0xf3, 0x54, 0x04, 0x30, 0x00, 0x2e, 0x00, 0x2e, 0x01, 0x89, 0x62,
|
||||||
|
0x0e, 0xfa, 0x2f, 0x43, 0x42, 0x11, 0x30, 0xfb, 0x6f, 0xc0, 0x2e, 0x01, 0x42, 0xb0, 0x5f, 0xc1, 0x4a, 0x00, 0x00,
|
||||||
|
0x6d, 0x57, 0x00, 0x00, 0x77, 0x8e, 0x00, 0x00, 0xe0, 0xff, 0xff, 0xff, 0xd3, 0xff, 0xff, 0xff, 0xe5, 0xff, 0xff,
|
||||||
|
0xff, 0xee, 0xe1, 0xff, 0xff, 0x7c, 0x13, 0x00, 0x00, 0x46, 0xe6, 0xff, 0xff, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||||
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||||
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||||
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0x2e, 0x00, 0xc1, 0x80,
|
||||||
|
0x2e, 0x00, 0xc1, 0x80, 0x2e, 0x00, 0xc1, 0x80, 0x2e, 0x00, 0xc1, 0x80, 0x2e, 0x00, 0xc1, 0x80, 0x2e, 0x00, 0xc1,
|
||||||
|
0x80, 0x2e, 0x00, 0xc1, 0x80, 0x2e, 0x00, 0xc1, 0x80, 0x2e, 0x00, 0xc1, 0x80, 0x2e, 0x00, 0xc1, 0x80, 0x2e, 0x00,
|
||||||
|
0xc1, 0x80, 0x2e, 0x00, 0xc1, 0x80, 0x2e, 0x00, 0xc1, 0x80, 0x2e, 0x00, 0xc1, 0x80, 0x2e, 0x00, 0xc1, 0x80, 0x2e,
|
||||||
|
0x00, 0xc1, 0x80, 0x2e, 0x00, 0xc1, 0x80, 0x2e, 0x00, 0xc1, 0x80, 0x2e, 0x00, 0xc1, 0x80, 0x2e, 0x00, 0xc1, 0x80,
|
||||||
|
0x2e, 0x00, 0xc1, 0x80, 0x2e, 0x00, 0xc1, 0x80, 0x2e, 0x00, 0xc1, 0x80, 0x2e, 0x00, 0xc1, 0x80, 0x2e, 0x00, 0xc1,
|
||||||
|
0x80, 0x2e, 0x00, 0xc1, 0x80, 0x2e, 0x00, 0xc1, 0x80, 0x2e, 0x00, 0xc1, 0x80, 0x2e, 0x00, 0xc1, 0x80, 0x2e, 0x00,
|
||||||
|
0xc1, 0x80, 0x2e, 0x00, 0xc1, 0x80, 0x2e, 0x00, 0xc1, 0x80, 0x2e, 0x00, 0xc1, 0x80, 0x2e, 0x00, 0xc1, 0x80, 0x2e,
|
||||||
|
0x00, 0xc1, 0x80, 0x2e, 0x00, 0xc1, 0x80, 0x2e, 0x00, 0xc1, 0x80, 0x2e, 0x00, 0xc1, 0x80, 0x2e, 0x00, 0xc1, 0x80,
|
||||||
|
0x2e, 0x00, 0xc1, 0x80, 0x2e, 0x00, 0xc1, 0x80, 0x2e, 0x00, 0xc1, 0x80, 0x2e, 0x00, 0xc1, 0x80, 0x2e, 0x00, 0xc1,
|
||||||
|
0x80, 0x2e, 0x00, 0xc1, 0x80, 0x2e, 0x00, 0xc1, 0x80, 0x2e, 0x00, 0xc1, 0x80, 0x2e, 0x00, 0xc1, 0x80, 0x2e, 0x00,
|
||||||
|
0xc1, 0x80, 0x2e, 0x00, 0xc1, 0x80, 0x2e, 0x00, 0xc1, 0x80, 0x2e, 0x00, 0xc1, 0x80, 0x2e, 0x00, 0xc1, 0x80, 0x2e,
|
||||||
|
0x00, 0xc1, 0x80, 0x2e, 0x00, 0xc1, 0x80, 0x2e, 0x00, 0xc1, 0x80, 0x2e, 0x00, 0xc1, 0x80, 0x2e, 0x00, 0xc1, 0x80,
|
||||||
|
0x2e, 0x00, 0xc1
|
||||||
|
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace esphome::bmi270
|
||||||
91
esphome/components/bmi270/motion.py
Normal file
91
esphome/components/bmi270/motion.py
Normal file
@@ -0,0 +1,91 @@
|
|||||||
|
import esphome.codegen as cg
|
||||||
|
from esphome.components import i2c
|
||||||
|
from esphome.components.const import (
|
||||||
|
CONF_ACCELEROMETER_ODR,
|
||||||
|
CONF_ACCELEROMETER_RANGE,
|
||||||
|
CONF_GYROSCOPE_ODR,
|
||||||
|
CONF_GYROSCOPE_RANGE,
|
||||||
|
)
|
||||||
|
from esphome.components.motion import motion_schema, new_motion_component
|
||||||
|
import esphome.config_validation as cv
|
||||||
|
|
||||||
|
from . import BMI270Component, bmi270_ns
|
||||||
|
|
||||||
|
DEPENDENCIES = ["i2c"]
|
||||||
|
|
||||||
|
# Enum proxies (must match the C++ enum values exactly)
|
||||||
|
BMI270AccelRange = bmi270_ns.enum("BMI270AccelRange")
|
||||||
|
ACCEL_RANGE_OPTIONS = {
|
||||||
|
"2G": BMI270AccelRange.BMI270_ACCEL_RANGE_2G,
|
||||||
|
"4G": BMI270AccelRange.BMI270_ACCEL_RANGE_4G,
|
||||||
|
"8G": BMI270AccelRange.BMI270_ACCEL_RANGE_8G,
|
||||||
|
"16G": BMI270AccelRange.BMI270_ACCEL_RANGE_16G,
|
||||||
|
}
|
||||||
|
|
||||||
|
BMI270GyroRange = bmi270_ns.enum("BMI270GyroRange")
|
||||||
|
GYRO_RANGE_OPTIONS = {
|
||||||
|
"2000DPS": BMI270GyroRange.BMI270_GYRO_RANGE_2000,
|
||||||
|
"1000DPS": BMI270GyroRange.BMI270_GYRO_RANGE_1000,
|
||||||
|
"500DPS": BMI270GyroRange.BMI270_GYRO_RANGE_500,
|
||||||
|
"250DPS": BMI270GyroRange.BMI270_GYRO_RANGE_250,
|
||||||
|
"125DPS": BMI270GyroRange.BMI270_GYRO_RANGE_125,
|
||||||
|
}
|
||||||
|
|
||||||
|
BMI270AccelODR = bmi270_ns.enum("BMI270AccelODR")
|
||||||
|
ACCEL_ODR_OPTIONS = {
|
||||||
|
"12_5HZ": BMI270AccelODR.BMI270_ACCEL_ODR_12_5,
|
||||||
|
"25HZ": BMI270AccelODR.BMI270_ACCEL_ODR_25,
|
||||||
|
"50HZ": BMI270AccelODR.BMI270_ACCEL_ODR_50,
|
||||||
|
"100HZ": BMI270AccelODR.BMI270_ACCEL_ODR_100,
|
||||||
|
"200HZ": BMI270AccelODR.BMI270_ACCEL_ODR_200,
|
||||||
|
"400HZ": BMI270AccelODR.BMI270_ACCEL_ODR_400,
|
||||||
|
"800HZ": BMI270AccelODR.BMI270_ACCEL_ODR_800,
|
||||||
|
"1600HZ": BMI270AccelODR.BMI270_ACCEL_ODR_1600,
|
||||||
|
}
|
||||||
|
|
||||||
|
BMI270GyroODR = bmi270_ns.enum("BMI270GyroODR")
|
||||||
|
GYRO_ODR_OPTIONS = {
|
||||||
|
"25HZ": BMI270GyroODR.BMI270_GYRO_ODR_25,
|
||||||
|
"50HZ": BMI270GyroODR.BMI270_GYRO_ODR_50,
|
||||||
|
"100HZ": BMI270GyroODR.BMI270_GYRO_ODR_100,
|
||||||
|
"200HZ": BMI270GyroODR.BMI270_GYRO_ODR_200,
|
||||||
|
"400HZ": BMI270GyroODR.BMI270_GYRO_ODR_400,
|
||||||
|
"800HZ": BMI270GyroODR.BMI270_GYRO_ODR_800,
|
||||||
|
"1600HZ": BMI270GyroODR.BMI270_GYRO_ODR_1600,
|
||||||
|
"3200HZ": BMI270GyroODR.BMI270_GYRO_ODR_3200,
|
||||||
|
}
|
||||||
|
|
||||||
|
# Top-level CONFIG_SCHEMA
|
||||||
|
CONFIG_SCHEMA = (
|
||||||
|
motion_schema(BMI270Component, has_accel=True, has_gyro=True)
|
||||||
|
.extend(
|
||||||
|
{
|
||||||
|
cv.Optional(CONF_ACCELEROMETER_RANGE, default="4G"): cv.enum(
|
||||||
|
ACCEL_RANGE_OPTIONS, upper=True
|
||||||
|
),
|
||||||
|
cv.Optional(CONF_ACCELEROMETER_ODR, default="100HZ"): cv.enum(
|
||||||
|
ACCEL_ODR_OPTIONS, upper=True
|
||||||
|
),
|
||||||
|
cv.Optional(CONF_GYROSCOPE_RANGE, default="2000DPS"): cv.enum(
|
||||||
|
GYRO_RANGE_OPTIONS, upper=True
|
||||||
|
),
|
||||||
|
cv.Optional(CONF_GYROSCOPE_ODR, default="200HZ"): cv.enum(
|
||||||
|
GYRO_ODR_OPTIONS, upper=True
|
||||||
|
),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
.extend(i2c.i2c_device_schema(0x68))
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# Code generation
|
||||||
|
async def to_code(config):
|
||||||
|
var = await new_motion_component(config)
|
||||||
|
await i2c.register_i2c_device(var, config)
|
||||||
|
|
||||||
|
# Accelerometer sensors
|
||||||
|
# Hardware configuration
|
||||||
|
cg.add(var.set_accel_range(config[CONF_ACCELEROMETER_RANGE]))
|
||||||
|
cg.add(var.set_accel_odr(config[CONF_ACCELEROMETER_ODR]))
|
||||||
|
cg.add(var.set_gyro_range(config[CONF_GYROSCOPE_RANGE]))
|
||||||
|
cg.add(var.set_gyro_odr(config[CONF_GYROSCOPE_ODR]))
|
||||||
41
esphome/components/bmi270/sensor.py
Normal file
41
esphome/components/bmi270/sensor.py
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
# YAML config keys
|
||||||
|
import esphome.codegen as cg
|
||||||
|
from esphome.components import sensor
|
||||||
|
import esphome.config_validation as cv
|
||||||
|
from esphome.const import (
|
||||||
|
CONF_TEMPERATURE,
|
||||||
|
CONF_TYPE,
|
||||||
|
DEVICE_CLASS_TEMPERATURE,
|
||||||
|
ICON_THERMOMETER,
|
||||||
|
STATE_CLASS_MEASUREMENT,
|
||||||
|
UNIT_CELSIUS,
|
||||||
|
)
|
||||||
|
from esphome.cpp_generator import MockObj
|
||||||
|
|
||||||
|
from . import CONF_BMI270_ID, BMI270Component
|
||||||
|
|
||||||
|
AUTO_LOAD = ["bmi270"]
|
||||||
|
|
||||||
|
CONFIG_SCHEMA = sensor.sensor_schema(
|
||||||
|
unit_of_measurement=UNIT_CELSIUS,
|
||||||
|
icon=ICON_THERMOMETER,
|
||||||
|
accuracy_decimals=2,
|
||||||
|
state_class=STATE_CLASS_MEASUREMENT,
|
||||||
|
device_class=DEVICE_CLASS_TEMPERATURE,
|
||||||
|
).extend(
|
||||||
|
{
|
||||||
|
cv.Optional(CONF_TYPE): cv.one_of(CONF_TEMPERATURE),
|
||||||
|
cv.GenerateID(CONF_BMI270_ID): cv.use_id(BMI270Component),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
async def to_code(config):
|
||||||
|
var = await sensor.new_sensor(config)
|
||||||
|
parent = await cg.get_variable(config[CONF_BMI270_ID])
|
||||||
|
data = MockObj("data")
|
||||||
|
value_lambda = await cg.process_lambda(
|
||||||
|
var.publish_state(data),
|
||||||
|
[(cg.float_, str(data))],
|
||||||
|
)
|
||||||
|
cg.add(parent.add_temperature_listener(value_lambda))
|
||||||
@@ -222,6 +222,7 @@ bool BTHomeMiThermometer::decrypt_bthome_payload_(const std::vector<uint8_t> &da
|
|||||||
}
|
}
|
||||||
|
|
||||||
size_t plaintext_length;
|
size_t plaintext_length;
|
||||||
|
// NOLINTNEXTLINE(readability-suspicious-call-argument) - similarly named size args are not swapped
|
||||||
psa_status_t status = psa_aead_decrypt(key_id, PSA_ALG_AEAD_WITH_SHORTENED_TAG(PSA_ALG_CCM, BTHOME_MIC_SIZE),
|
psa_status_t status = psa_aead_decrypt(key_id, PSA_ALG_AEAD_WITH_SHORTENED_TAG(PSA_ALG_CCM, BTHOME_MIC_SIZE),
|
||||||
nonce.data(), nonce.size(), nullptr, 0, ct_with_tag, ct_with_tag_size,
|
nonce.data(), nonce.size(), nullptr, 0, ct_with_tag, ct_with_tag_size,
|
||||||
payload.data(), ciphertext_size, &plaintext_length);
|
payload.data(), ciphertext_size, &plaintext_length);
|
||||||
|
|||||||
@@ -50,7 +50,7 @@ async def to_code(config: ConfigType) -> None:
|
|||||||
buffer = cg.new_Pvariable(config[CONF_ENCODER_BUFFER_ID])
|
buffer = cg.new_Pvariable(config[CONF_ENCODER_BUFFER_ID])
|
||||||
cg.add(buffer.set_buffer_size(config[CONF_BUFFER_SIZE]))
|
cg.add(buffer.set_buffer_size(config[CONF_BUFFER_SIZE]))
|
||||||
if config[CONF_TYPE] == ESP32_CAMERA_ENCODER:
|
if config[CONF_TYPE] == ESP32_CAMERA_ENCODER:
|
||||||
add_idf_component(name="espressif/esp32-camera", ref="2.1.5")
|
add_idf_component(name="espressif/esp32-camera", ref="2.1.7")
|
||||||
cg.add_define("USE_ESP32_CAMERA_JPEG_ENCODER")
|
cg.add_define("USE_ESP32_CAMERA_JPEG_ENCODER")
|
||||||
var = cg.new_Pvariable(
|
var = cg.new_Pvariable(
|
||||||
config[CONF_ID],
|
config[CONF_ID],
|
||||||
|
|||||||
@@ -5,6 +5,8 @@ CODEOWNERS = ["@esphome/core"]
|
|||||||
BYTE_ORDER_LITTLE = "little_endian"
|
BYTE_ORDER_LITTLE = "little_endian"
|
||||||
BYTE_ORDER_BIG = "big_endian"
|
BYTE_ORDER_BIG = "big_endian"
|
||||||
|
|
||||||
|
CONF_ACCELEROMETER_ODR = "accelerometer_odr"
|
||||||
|
CONF_ACCELEROMETER_RANGE = "accelerometer_range"
|
||||||
CONF_B_CONSTANT = "b_constant"
|
CONF_B_CONSTANT = "b_constant"
|
||||||
CONF_BYTE_ORDER = "byte_order"
|
CONF_BYTE_ORDER = "byte_order"
|
||||||
CONF_CLIMATE_ID = "climate_id"
|
CONF_CLIMATE_ID = "climate_id"
|
||||||
@@ -13,8 +15,11 @@ CONF_CRC_ENABLE = "crc_enable"
|
|||||||
CONF_DATA_BITS = "data_bits"
|
CONF_DATA_BITS = "data_bits"
|
||||||
CONF_DRAW_ROUNDING = "draw_rounding"
|
CONF_DRAW_ROUNDING = "draw_rounding"
|
||||||
CONF_ENABLED = "enabled"
|
CONF_ENABLED = "enabled"
|
||||||
|
CONF_GYROSCOPE_ODR = "gyroscope_odr"
|
||||||
|
CONF_GYROSCOPE_RANGE = "gyroscope_range"
|
||||||
CONF_IGNORE_NOT_FOUND = "ignore_not_found"
|
CONF_IGNORE_NOT_FOUND = "ignore_not_found"
|
||||||
CONF_LIBRETINY = "libretiny"
|
CONF_LIBRETINY = "libretiny"
|
||||||
|
CONF_LOOP = "loop"
|
||||||
CONF_ON_PACKET = "on_packet"
|
CONF_ON_PACKET = "on_packet"
|
||||||
CONF_ON_RECEIVE = "on_receive"
|
CONF_ON_RECEIVE = "on_receive"
|
||||||
CONF_ON_STATE_CHANGE = "on_state_change"
|
CONF_ON_STATE_CHANGE = "on_state_change"
|
||||||
@@ -22,6 +27,7 @@ CONF_PARITY = "parity"
|
|||||||
CONF_RECEIVER_FREQUENCY = "receiver_frequency"
|
CONF_RECEIVER_FREQUENCY = "receiver_frequency"
|
||||||
CONF_REQUEST_HEADERS = "request_headers"
|
CONF_REQUEST_HEADERS = "request_headers"
|
||||||
CONF_ROWS = "rows"
|
CONF_ROWS = "rows"
|
||||||
|
CONF_SHA256 = "sha256"
|
||||||
CONF_STOP_BITS = "stop_bits"
|
CONF_STOP_BITS = "stop_bits"
|
||||||
CONF_USE_PSRAM = "use_psram"
|
CONF_USE_PSRAM = "use_psram"
|
||||||
CONF_VOLUME_INCREMENT = "volume_increment"
|
CONF_VOLUME_INCREMENT = "volume_increment"
|
||||||
|
|||||||
@@ -216,7 +216,7 @@ uint8_t DaikinArcClimate::temperature_() {
|
|||||||
return 0xc0;
|
return 0xc0;
|
||||||
default:
|
default:
|
||||||
float new_temp = clamp<float>(this->target_temperature, DAIKIN_TEMP_MIN, DAIKIN_TEMP_MAX);
|
float new_temp = clamp<float>(this->target_temperature, DAIKIN_TEMP_MIN, DAIKIN_TEMP_MAX);
|
||||||
uint8_t temperature = (uint8_t) floor(new_temp);
|
uint8_t temperature = (uint8_t) std::floor(new_temp);
|
||||||
return temperature << 1 | (new_temp - temperature > 0 ? 0x01 : 0);
|
return temperature << 1 | (new_temp - temperature > 0 ? 0x01 : 0);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
from esphome import automation
|
from esphome import automation
|
||||||
import esphome.codegen as cg
|
import esphome.codegen as cg
|
||||||
from esphome.components import uart
|
from esphome.components import uart
|
||||||
|
from esphome.components.const import CONF_LOOP
|
||||||
import esphome.config_validation as cv
|
import esphome.config_validation as cv
|
||||||
from esphome.const import CONF_DEVICE, CONF_FILE, CONF_ID, CONF_VOLUME
|
from esphome.const import CONF_DEVICE, CONF_FILE, CONF_ID, CONF_VOLUME
|
||||||
|
|
||||||
@@ -15,7 +16,6 @@ DFPlayerIsPlayingCondition = dfplayer_ns.class_(
|
|||||||
|
|
||||||
MULTI_CONF = True
|
MULTI_CONF = True
|
||||||
CONF_FOLDER = "folder"
|
CONF_FOLDER = "folder"
|
||||||
CONF_LOOP = "loop"
|
|
||||||
CONF_EQ_PRESET = "eq_preset"
|
CONF_EQ_PRESET = "eq_preset"
|
||||||
CONF_ON_FINISHED_PLAYBACK = "on_finished_playback"
|
CONF_ON_FINISHED_PLAYBACK = "on_finished_playback"
|
||||||
|
|
||||||
|
|||||||
@@ -3,11 +3,18 @@ from dataclasses import dataclass
|
|||||||
from esphome import automation, core
|
from esphome import automation, core
|
||||||
from esphome.automation import maybe_simple_id
|
from esphome.automation import maybe_simple_id
|
||||||
import esphome.codegen as cg
|
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
|
import esphome.config_validation as cv
|
||||||
from esphome.const import (
|
from esphome.const import (
|
||||||
CONF_AUTO_CLEAR_ENABLED,
|
CONF_AUTO_CLEAR_ENABLED,
|
||||||
|
CONF_DIMENSIONS,
|
||||||
CONF_FROM,
|
CONF_FROM,
|
||||||
|
CONF_HEIGHT,
|
||||||
CONF_ID,
|
CONF_ID,
|
||||||
CONF_LAMBDA,
|
CONF_LAMBDA,
|
||||||
CONF_PAGE_ID,
|
CONF_PAGE_ID,
|
||||||
@@ -16,10 +23,11 @@ from esphome.const import (
|
|||||||
CONF_TO,
|
CONF_TO,
|
||||||
CONF_TRIGGER_ID,
|
CONF_TRIGGER_ID,
|
||||||
CONF_UPDATE_INTERVAL,
|
CONF_UPDATE_INTERVAL,
|
||||||
|
CONF_WIDTH,
|
||||||
SCHEDULER_DONT_RUN,
|
SCHEDULER_DONT_RUN,
|
||||||
)
|
)
|
||||||
from esphome.core import CORE, CoroPriority, coroutine_with_priority
|
from esphome.core import CORE, ID, CoroPriority, coroutine_with_priority
|
||||||
from esphome.cpp_generator import MockObj
|
from esphome.final_validate import full_config
|
||||||
|
|
||||||
DOMAIN = "display"
|
DOMAIN = "display"
|
||||||
IS_PLATFORM_COMPONENT = True
|
IS_PLATFORM_COMPONENT = True
|
||||||
@@ -159,29 +167,97 @@ async def setup_display_core_(var, config):
|
|||||||
class DisplayMetaData:
|
class DisplayMetaData:
|
||||||
width: int = 0
|
width: int = 0
|
||||||
height: int = 0
|
height: int = 0
|
||||||
has_writer: bool = False
|
|
||||||
has_hardware_rotation: 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]:
|
def get_all_display_metadata() -> dict[str, DisplayMetaData]:
|
||||||
"""Get all display metadata."""
|
"""Get all display metadata as a dict keyed by resolved ID strings.
|
||||||
return CORE.data.setdefault(DOMAIN, {}).setdefault(KEY_METADATA, {})
|
|
||||||
|
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:
|
def get_display_metadata(display_id: ID) -> DisplayMetaData:
|
||||||
"""Get display metadata by ID for use by other components."""
|
"""Get display metadata by ID object
|
||||||
return get_all_display_metadata().get(display_id, DisplayMetaData())
|
|
||||||
|
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(
|
def add_metadata(
|
||||||
id: str | MockObj,
|
id: ID,
|
||||||
width: int,
|
width: int,
|
||||||
height: int,
|
height: int,
|
||||||
has_writer: bool,
|
|
||||||
has_hardware_rotation: bool = False,
|
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(
|
entries = _get_metadata_list()
|
||||||
width, height, has_writer, has_hardware_rotation
|
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,
|
||||||
|
),
|
||||||
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
#include "display.h"
|
#include "display.h"
|
||||||
|
#include <cmath>
|
||||||
#include <utility>
|
#include <utility>
|
||||||
#include <numbers>
|
#include <numbers>
|
||||||
#include "display_color_utils.h"
|
#include "display_color_utils.h"
|
||||||
@@ -238,7 +239,7 @@ void Display::filled_gauge(int center_x, int center_y, int radius1, int radius2,
|
|||||||
int lhline_width = -(dxmax - dxmin) + 1;
|
int lhline_width = -(dxmax - dxmin) + 1;
|
||||||
if (progress >= 50) {
|
if (progress >= 50) {
|
||||||
if (float(dymax) < float(-dxmax) * tan_a) {
|
if (float(dymax) < float(-dxmax) * tan_a) {
|
||||||
upd_dxmax = ceil(float(dymax) / tan_a);
|
upd_dxmax = std::ceil(float(dymax) / tan_a);
|
||||||
} else {
|
} else {
|
||||||
upd_dxmax = -dxmax;
|
upd_dxmax = -dxmax;
|
||||||
}
|
}
|
||||||
@@ -253,7 +254,7 @@ void Display::filled_gauge(int center_x, int center_y, int radius1, int radius2,
|
|||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if (float(dymin) > float(-dxmin) * tan_a) {
|
if (float(dymin) > float(-dxmin) * tan_a) {
|
||||||
upd_dxmin = ceil(float(dymin) / tan_a);
|
upd_dxmin = std::ceil(float(dymin) / tan_a);
|
||||||
} else {
|
} else {
|
||||||
upd_dxmin = -dxmin;
|
upd_dxmin = -dxmin;
|
||||||
}
|
}
|
||||||
@@ -268,12 +269,12 @@ void Display::filled_gauge(int center_x, int center_y, int radius1, int radius2,
|
|||||||
int hline_width = 2 * (-dxmax) + 1;
|
int hline_width = 2 * (-dxmax) + 1;
|
||||||
if (progress >= 50) {
|
if (progress >= 50) {
|
||||||
if (dymax < float(-dxmax) * tan_a) {
|
if (dymax < float(-dxmax) * tan_a) {
|
||||||
upd_dxmax = ceil(float(dymax) / tan_a);
|
upd_dxmax = std::ceil(float(dymax) / tan_a);
|
||||||
hline_width = -dxmax + upd_dxmax + 1;
|
hline_width = -dxmax + upd_dxmax + 1;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if (dymax < float(-dxmax) * tan_a) {
|
if (dymax < float(-dxmax) * tan_a) {
|
||||||
upd_dxmax = ceil(float(dymax) / tan_a);
|
upd_dxmax = std::ceil(float(dymax) / tan_a);
|
||||||
hline_width = -dxmax - upd_dxmax + 1;
|
hline_width = -dxmax - upd_dxmax + 1;
|
||||||
} else {
|
} else {
|
||||||
hline_width = 0;
|
hline_width = 0;
|
||||||
@@ -452,8 +453,8 @@ void HOT Display::get_regular_polygon_vertex(int vertex_id, int *vertex_x, int *
|
|||||||
rotation_radians -= (variation == VARIATION_FLAT_TOP) ? std::numbers::pi / edges : 0.0;
|
rotation_radians -= (variation == VARIATION_FLAT_TOP) ? std::numbers::pi / edges : 0.0;
|
||||||
|
|
||||||
float vertex_angle = ((float) vertex_id) / edges * 2 * std::numbers::pi + rotation_radians;
|
float vertex_angle = ((float) vertex_id) / edges * 2 * std::numbers::pi + rotation_radians;
|
||||||
*vertex_x = (int) round(cos(vertex_angle) * radius) + center_x;
|
*vertex_x = (int) std::round(std::cos(vertex_angle) * radius) + center_x;
|
||||||
*vertex_y = (int) round(sin(vertex_angle) * radius) + center_y;
|
*vertex_y = (int) std::round(std::sin(vertex_angle) * radius) + center_y;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,57 +1,258 @@
|
|||||||
import esphome.codegen as cg
|
import logging
|
||||||
from esphome.components import uart
|
import re
|
||||||
import esphome.config_validation as cv
|
|
||||||
from esphome.const import CONF_ID, PLATFORM_ESP32, PLATFORM_ESP8266
|
|
||||||
|
|
||||||
CODEOWNERS = ["@SimonFischer04"]
|
import esphome.codegen as cg
|
||||||
|
from esphome.components import esp32, uart
|
||||||
|
import esphome.config_validation as cv
|
||||||
|
from esphome.const import (
|
||||||
|
CONF_ID,
|
||||||
|
CONF_NAME,
|
||||||
|
CONF_PATTERN,
|
||||||
|
CONF_PRIORITY,
|
||||||
|
CONF_RECEIVE_TIMEOUT,
|
||||||
|
)
|
||||||
|
from esphome.core import CORE
|
||||||
|
|
||||||
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
CODEOWNERS = ["@SimonFischer04", "@Tomer27cz", "@latonita", "@PolarGoose"]
|
||||||
DEPENDENCIES = ["uart"]
|
DEPENDENCIES = ["uart"]
|
||||||
|
|
||||||
CONF_DLMS_METER_ID = "dlms_meter_id"
|
CONF_DLMS_METER_ID = "dlms_meter_id"
|
||||||
CONF_DECRYPTION_KEY = "decryption_key"
|
CONF_DECRYPTION_KEY = "decryption_key"
|
||||||
|
CONF_AUTH_KEY = "auth_key"
|
||||||
|
CONF_OBIS_CODE = "obis_code"
|
||||||
|
CONF_CUSTOM_PATTERNS = "custom_patterns"
|
||||||
|
CONF_SKIP_CRC = "skip_crc"
|
||||||
|
CONF_DEFAULT_OBIS = "default_obis"
|
||||||
CONF_PROVIDER = "provider"
|
CONF_PROVIDER = "provider"
|
||||||
|
|
||||||
PROVIDERS = {"generic": 0, "netznoe": 1}
|
|
||||||
|
|
||||||
dlms_meter_component_ns = cg.esphome_ns.namespace("dlms_meter")
|
dlms_meter_component_ns = cg.esphome_ns.namespace("dlms_meter")
|
||||||
DlmsMeterComponent = dlms_meter_component_ns.class_(
|
DlmsMeterComponent = dlms_meter_component_ns.class_(
|
||||||
"DlmsMeterComponent", cg.Component, uart.UARTDevice
|
"DlmsMeterComponent", cg.Component, uart.UARTDevice
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def validate_key(value):
|
def obis_code(value):
|
||||||
value = cv.string_strict(value)
|
# Normalize the OBIS code to the strict A.B.C.D.E.F format
|
||||||
if len(value) != 32:
|
bytes_list = parse_obis_code_bytes(value)
|
||||||
raise cv.Invalid("Decryption key must be 32 hex characters (16 bytes)")
|
return ".".join(str(b) for b in bytes_list)
|
||||||
try:
|
|
||||||
return [int(value[i : i + 2], 16) for i in range(0, 32, 2)]
|
|
||||||
except ValueError as exc:
|
|
||||||
raise cv.Invalid("Decryption key must be hex values from 00 to FF") from exc
|
|
||||||
|
|
||||||
|
|
||||||
|
def parse_obis_code_bytes(value):
|
||||||
|
value = cv.string(value)
|
||||||
|
normalized = re.sub(r"[\-\:\*]", ".", value)
|
||||||
|
parts = normalized.split(".")
|
||||||
|
if len(parts) < 5 or len(parts) > 6:
|
||||||
|
raise cv.Invalid("OBIS code must have 5 or 6 parts")
|
||||||
|
try:
|
||||||
|
bytes_list = [int(p) for p in parts]
|
||||||
|
except ValueError as exc:
|
||||||
|
raise cv.Invalid("OBIS code parts must be integers") from exc
|
||||||
|
for b in bytes_list:
|
||||||
|
if b < 0 or b > 255:
|
||||||
|
raise cv.Invalid("OBIS code parts must be between 0 and 255")
|
||||||
|
if len(bytes_list) == 5:
|
||||||
|
bytes_list.append(255)
|
||||||
|
return bytes_list
|
||||||
|
|
||||||
|
|
||||||
|
def custom_pattern_dict(value):
|
||||||
|
if isinstance(value, str):
|
||||||
|
return {CONF_PATTERN: value}
|
||||||
|
return value
|
||||||
|
|
||||||
|
|
||||||
|
def validate_custom_pattern(value):
|
||||||
|
if CONF_DEFAULT_OBIS in value and CONF_NAME not in value:
|
||||||
|
raise cv.Invalid(f"'{CONF_DEFAULT_OBIS}' requires '{CONF_NAME}' to be set")
|
||||||
|
return value
|
||||||
|
|
||||||
|
|
||||||
|
def validate_provider_deprecation(config):
|
||||||
|
if CONF_PROVIDER in config:
|
||||||
|
provider = str(config[CONF_PROVIDER]).lower()
|
||||||
|
if provider == "netznoe":
|
||||||
|
_LOGGER.warning(
|
||||||
|
"The 'provider: netznoe' option is deprecated and will be removed in 2026.11.0. "
|
||||||
|
"The required custom patterns have been added automatically for this release, but you must update your configuration.\n"
|
||||||
|
"Please remove the 'provider' key and explicitly replace it with the following:\n\n"
|
||||||
|
"custom_patterns:\n"
|
||||||
|
' - pattern: "L, TSTR"\n'
|
||||||
|
' name: "MeterID"\n'
|
||||||
|
' default_obis: "0.0.96.1.0.255"\n'
|
||||||
|
' - pattern: "F, TDTM"\n'
|
||||||
|
' name: "DateTime"\n'
|
||||||
|
' default_obis: "0.0.1.0.0.255"\n'
|
||||||
|
)
|
||||||
|
patterns = config.get(CONF_CUSTOM_PATTERNS, [])
|
||||||
|
|
||||||
|
# Ensure "L, TSTR" for MeterID is present
|
||||||
|
if not any(p.get(CONF_PATTERN) == "L, TSTR" for p in patterns):
|
||||||
|
patterns.append(
|
||||||
|
{
|
||||||
|
CONF_PATTERN: "L, TSTR",
|
||||||
|
CONF_NAME: "MeterID",
|
||||||
|
CONF_DEFAULT_OBIS: [0, 0, 96, 1, 0, 255],
|
||||||
|
CONF_PRIORITY: 0,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
# Ensure "F, TDTM" for DateTime is present
|
||||||
|
if not any(p.get(CONF_PATTERN) == "F, TDTM" for p in patterns):
|
||||||
|
patterns.append(
|
||||||
|
{
|
||||||
|
CONF_PATTERN: "F, TDTM",
|
||||||
|
CONF_NAME: "DateTime",
|
||||||
|
CONF_DEFAULT_OBIS: [0, 0, 1, 0, 0, 255],
|
||||||
|
CONF_PRIORITY: 0,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
config[CONF_CUSTOM_PATTERNS] = patterns
|
||||||
|
else:
|
||||||
|
_LOGGER.warning(
|
||||||
|
"The 'provider' option is deprecated and will be removed in 2026.11.0. "
|
||||||
|
"The dlms_parser library now handles quirks dynamically. "
|
||||||
|
"Please remove this option from your configuration."
|
||||||
|
)
|
||||||
|
return config
|
||||||
|
|
||||||
|
|
||||||
|
CUSTOM_PATTERN_SCHEMA = cv.All(
|
||||||
|
custom_pattern_dict,
|
||||||
|
cv.Schema(
|
||||||
|
{
|
||||||
|
cv.Required(CONF_PATTERN): cv.string,
|
||||||
|
cv.Optional(CONF_NAME): cv.string,
|
||||||
|
cv.Optional(CONF_PRIORITY, default=0): cv.int_,
|
||||||
|
cv.Optional(CONF_DEFAULT_OBIS): parse_obis_code_bytes,
|
||||||
|
}
|
||||||
|
),
|
||||||
|
validate_custom_pattern,
|
||||||
|
)
|
||||||
|
|
||||||
CONFIG_SCHEMA = cv.All(
|
CONFIG_SCHEMA = cv.All(
|
||||||
cv.Schema(
|
cv.Schema(
|
||||||
{
|
{
|
||||||
cv.GenerateID(): cv.declare_id(DlmsMeterComponent),
|
cv.GenerateID(): cv.declare_id(DlmsMeterComponent),
|
||||||
cv.Required(CONF_DECRYPTION_KEY): validate_key,
|
cv.Optional(CONF_DECRYPTION_KEY): lambda value: cv.bind_key(
|
||||||
cv.Optional(CONF_PROVIDER, default="generic"): cv.enum(
|
value, name="Decryption key"
|
||||||
PROVIDERS, lower=True
|
|
||||||
),
|
),
|
||||||
|
cv.Optional(CONF_AUTH_KEY): lambda value: cv.bind_key(
|
||||||
|
value, name="Authentication key"
|
||||||
|
),
|
||||||
|
cv.Optional(CONF_CUSTOM_PATTERNS): cv.ensure_list(CUSTOM_PATTERN_SCHEMA),
|
||||||
|
cv.Optional(CONF_SKIP_CRC, default=False): cv.boolean,
|
||||||
|
cv.Optional(CONF_PROVIDER): cv.string,
|
||||||
|
cv.Optional(
|
||||||
|
CONF_RECEIVE_TIMEOUT, default="1000ms"
|
||||||
|
): cv.positive_time_period_milliseconds,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
.extend(uart.UART_DEVICE_SCHEMA)
|
.extend(uart.UART_DEVICE_SCHEMA)
|
||||||
.extend(cv.COMPONENT_SCHEMA),
|
.extend(cv.COMPONENT_SCHEMA),
|
||||||
cv.only_on([PLATFORM_ESP8266, PLATFORM_ESP32]),
|
validate_provider_deprecation,
|
||||||
)
|
)
|
||||||
|
|
||||||
FINAL_VALIDATE_SCHEMA = uart.final_validate_device_schema(
|
FINAL_VALIDATE_SCHEMA = uart.final_validate_device_schema("dlms_meter", require_rx=True)
|
||||||
"dlms_meter", baud_rate=2400, require_rx=True
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
async def to_code(config):
|
async def to_code(config):
|
||||||
var = cg.new_Pvariable(config[CONF_ID])
|
dec_key_expr = cg.RawExpression("std::nullopt")
|
||||||
|
if dec_key := config.get(CONF_DECRYPTION_KEY):
|
||||||
|
key_bytes = [str(int(dec_key[i : i + 2], 16)) for i in range(0, 32, 2)]
|
||||||
|
dec_key_expr = cg.RawExpression(
|
||||||
|
f"std::array<uint8_t, 16>{{{', '.join(key_bytes)}}}"
|
||||||
|
)
|
||||||
|
|
||||||
|
auth_key_expr = cg.RawExpression("std::nullopt")
|
||||||
|
if auth_key := config.get(CONF_AUTH_KEY):
|
||||||
|
key_bytes = [str(int(auth_key[i : i + 2], 16)) for i in range(0, 32, 2)]
|
||||||
|
auth_key_expr = cg.RawExpression(
|
||||||
|
f"std::array<uint8_t, 16>{{{', '.join(key_bytes)}}}"
|
||||||
|
)
|
||||||
|
|
||||||
|
patterns = []
|
||||||
|
if custom_patterns := config.get(CONF_CUSTOM_PATTERNS):
|
||||||
|
for p in custom_patterns:
|
||||||
|
name_expr = cg.RawExpression("std::nullopt")
|
||||||
|
if name_val := p.get(CONF_NAME):
|
||||||
|
name_expr = name_val
|
||||||
|
|
||||||
|
if obis_vals := p.get(CONF_DEFAULT_OBIS):
|
||||||
|
obis_expr = cg.RawExpression(
|
||||||
|
f"std::array<uint8_t, 6>{{{obis_vals[0]}, {obis_vals[1]}, {obis_vals[2]}, {obis_vals[3]}, {obis_vals[4]}, {obis_vals[5]}}}"
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
obis_expr = cg.RawExpression("std::nullopt")
|
||||||
|
|
||||||
|
patterns.append(
|
||||||
|
cg.ArrayInitializer(
|
||||||
|
p[CONF_PATTERN],
|
||||||
|
name_expr,
|
||||||
|
p.get(CONF_PRIORITY, 0),
|
||||||
|
obis_expr,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
patterns_expr = (
|
||||||
|
cg.ArrayInitializer(*patterns) if patterns else cg.RawExpression("{}")
|
||||||
|
)
|
||||||
|
|
||||||
|
var = cg.new_Pvariable(
|
||||||
|
config[CONF_ID],
|
||||||
|
config[CONF_RECEIVE_TIMEOUT],
|
||||||
|
config[CONF_SKIP_CRC],
|
||||||
|
dec_key_expr,
|
||||||
|
auth_key_expr,
|
||||||
|
patterns_expr,
|
||||||
|
)
|
||||||
|
|
||||||
|
hub_id = config[CONF_ID].id
|
||||||
|
|
||||||
|
sensor_count = 0
|
||||||
|
for sens_conf in CORE.config.get("sensor", []):
|
||||||
|
if (
|
||||||
|
sens_conf.get("platform") == "dlms_meter"
|
||||||
|
and sens_conf.get(CONF_DLMS_METER_ID).id == hub_id
|
||||||
|
):
|
||||||
|
if CONF_OBIS_CODE in sens_conf:
|
||||||
|
sensor_count += 1
|
||||||
|
else:
|
||||||
|
from .sensor import NUMERIC_KEYS
|
||||||
|
|
||||||
|
sensor_count += sum(1 for key in NUMERIC_KEYS if key in sens_conf)
|
||||||
|
|
||||||
|
text_sensor_count = 0
|
||||||
|
for sens_conf in CORE.config.get("text_sensor", []):
|
||||||
|
if (
|
||||||
|
sens_conf.get("platform") == "dlms_meter"
|
||||||
|
and sens_conf.get(CONF_DLMS_METER_ID).id == hub_id
|
||||||
|
):
|
||||||
|
if CONF_OBIS_CODE in sens_conf:
|
||||||
|
text_sensor_count += 1
|
||||||
|
else:
|
||||||
|
from .text_sensor import TEXT_KEYS
|
||||||
|
|
||||||
|
text_sensor_count += sum(1 for key in TEXT_KEYS if key in sens_conf)
|
||||||
|
|
||||||
|
binary_sensor_count = 0
|
||||||
|
for sens_conf in CORE.config.get("binary_sensor", []):
|
||||||
|
if (
|
||||||
|
sens_conf.get("platform") == "dlms_meter"
|
||||||
|
and sens_conf.get(CONF_DLMS_METER_ID).id == hub_id
|
||||||
|
):
|
||||||
|
binary_sensor_count += 1
|
||||||
|
|
||||||
|
cg.add_define("DLMS_MAX_SENSORS", sensor_count)
|
||||||
|
cg.add_define("DLMS_MAX_TEXT_SENSORS", text_sensor_count)
|
||||||
|
cg.add_define("DLMS_MAX_BINARY_SENSORS", binary_sensor_count)
|
||||||
|
|
||||||
await cg.register_component(var, config)
|
await cg.register_component(var, config)
|
||||||
await uart.register_uart_device(var, config)
|
await uart.register_uart_device(var, config)
|
||||||
key = ", ".join(str(b) for b in config[CONF_DECRYPTION_KEY])
|
|
||||||
cg.add(var.set_decryption_key(cg.RawExpression(f"{{{key}}}")))
|
if CORE.is_esp32:
|
||||||
cg.add(var.set_provider(PROVIDERS[config[CONF_PROVIDER]]))
|
esp32.add_idf_component(name="esphome/dlms_parser", ref="1.1.0")
|
||||||
|
else:
|
||||||
|
cg.add_library("esphome/dlms_parser", "1.1.0")
|
||||||
|
|||||||
20
esphome/components/dlms_meter/binary_sensor/__init__.py
Normal file
20
esphome/components/dlms_meter/binary_sensor/__init__.py
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
import esphome.codegen as cg
|
||||||
|
from esphome.components import binary_sensor
|
||||||
|
import esphome.config_validation as cv
|
||||||
|
|
||||||
|
from .. import CONF_DLMS_METER_ID, CONF_OBIS_CODE, DlmsMeterComponent, obis_code
|
||||||
|
|
||||||
|
DEPENDENCIES = ["dlms_meter"]
|
||||||
|
|
||||||
|
CONFIG_SCHEMA = binary_sensor.binary_sensor_schema().extend(
|
||||||
|
{
|
||||||
|
cv.GenerateID(CONF_DLMS_METER_ID): cv.use_id(DlmsMeterComponent),
|
||||||
|
cv.Required(CONF_OBIS_CODE): obis_code,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
async def to_code(config):
|
||||||
|
hub = await cg.get_variable(config[CONF_DLMS_METER_ID])
|
||||||
|
var = await binary_sensor.new_binary_sensor(config)
|
||||||
|
cg.add(hub.register_binary_sensor(config[CONF_OBIS_CODE], var))
|
||||||
@@ -1,71 +0,0 @@
|
|||||||
#pragma once
|
|
||||||
|
|
||||||
#include <cstdint>
|
|
||||||
|
|
||||||
namespace esphome::dlms_meter {
|
|
||||||
|
|
||||||
/*
|
|
||||||
+-------------------------------+
|
|
||||||
| Ciphering Service |
|
|
||||||
+-------------------------------+
|
|
||||||
| System Title Length |
|
|
||||||
+-------------------------------+
|
|
||||||
| |
|
|
||||||
| |
|
|
||||||
| |
|
|
||||||
| System |
|
|
||||||
| Title |
|
|
||||||
| |
|
|
||||||
| |
|
|
||||||
| |
|
|
||||||
+-------------------------------+
|
|
||||||
| Length | (1 or 3 Bytes)
|
|
||||||
+-------------------------------+
|
|
||||||
| Security Control Byte |
|
|
||||||
+-------------------------------+
|
|
||||||
| |
|
|
||||||
| Frame |
|
|
||||||
| Counter |
|
|
||||||
| |
|
|
||||||
+-------------------------------+
|
|
||||||
| |
|
|
||||||
~ ~
|
|
||||||
Encrypted Payload
|
|
||||||
~ ~
|
|
||||||
| |
|
|
||||||
+-------------------------------+
|
|
||||||
|
|
||||||
Ciphering Service: 0xDB (General-Glo-Ciphering)
|
|
||||||
System Title Length: 0x08
|
|
||||||
System Title: Unique ID of meter
|
|
||||||
Length: 1 Byte=Length <= 127, 3 Bytes=Length > 127 (0x82 & 2 Bytes length)
|
|
||||||
Security Control Byte:
|
|
||||||
- Bit 3…0: Security_Suite_Id
|
|
||||||
- Bit 4: "A" subfield: indicates that authentication is applied
|
|
||||||
- Bit 5: "E" subfield: indicates that encryption is applied
|
|
||||||
- Bit 6: Key_Set subfield: 0 = Unicast, 1 = Broadcast
|
|
||||||
- Bit 7: Indicates the use of compression.
|
|
||||||
*/
|
|
||||||
|
|
||||||
static constexpr uint8_t DLMS_HEADER_LENGTH = 16;
|
|
||||||
static constexpr uint8_t DLMS_HEADER_EXT_OFFSET = 2; // Extra offset for extended length header
|
|
||||||
static constexpr uint8_t DLMS_CIPHER_OFFSET = 0;
|
|
||||||
static constexpr uint8_t DLMS_SYST_OFFSET = 1;
|
|
||||||
static constexpr uint8_t DLMS_LENGTH_OFFSET = 10;
|
|
||||||
static constexpr uint8_t TWO_BYTE_LENGTH = 0x82;
|
|
||||||
static constexpr uint8_t DLMS_LENGTH_CORRECTION = 5; // Header bytes included in length field
|
|
||||||
static constexpr uint8_t DLMS_SECBYTE_OFFSET = 11;
|
|
||||||
static constexpr uint8_t DLMS_FRAMECOUNTER_OFFSET = 12;
|
|
||||||
static constexpr uint8_t DLMS_FRAMECOUNTER_LENGTH = 4;
|
|
||||||
static constexpr uint8_t DLMS_PAYLOAD_OFFSET = 16;
|
|
||||||
static constexpr uint8_t GLO_CIPHERING = 0xDB;
|
|
||||||
static constexpr uint8_t DATA_NOTIFICATION = 0x0F;
|
|
||||||
static constexpr uint8_t TIMESTAMP_DATETIME = 0x0C;
|
|
||||||
static constexpr uint16_t MAX_MESSAGE_LENGTH = 512; // Maximum size of message (when having 2 bytes length in header).
|
|
||||||
|
|
||||||
// Provider specific quirks
|
|
||||||
static constexpr uint8_t NETZ_NOE_MAGIC_BYTE = 0x81; // Magic length byte used by Netz NOE
|
|
||||||
static constexpr uint8_t NETZ_NOE_EXPECTED_MESSAGE_LENGTH = 0xF8;
|
|
||||||
static constexpr uint8_t NETZ_NOE_EXPECTED_SECURITY_CONTROL_BYTE = 0x20;
|
|
||||||
|
|
||||||
} // namespace esphome::dlms_meter
|
|
||||||
@@ -1,516 +1,236 @@
|
|||||||
#include "dlms_meter.h"
|
#include "dlms_meter.h"
|
||||||
|
#include "esphome/core/log.h"
|
||||||
|
|
||||||
#include <cinttypes>
|
#include <cstdio>
|
||||||
|
|
||||||
#if defined(USE_ESP8266_FRAMEWORK_ARDUINO)
|
|
||||||
#include <bearssl/bearssl.h>
|
|
||||||
#elif defined(USE_ESP32)
|
|
||||||
#include <esp_idf_version.h>
|
|
||||||
#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(6, 0, 0)
|
|
||||||
#include <psa/crypto.h>
|
|
||||||
#else
|
|
||||||
#include "mbedtls/esp_config.h"
|
|
||||||
#include "mbedtls/gcm.h"
|
|
||||||
#endif
|
|
||||||
#endif
|
|
||||||
|
|
||||||
namespace esphome::dlms_meter {
|
namespace esphome::dlms_meter {
|
||||||
|
|
||||||
static constexpr const char *TAG = "dlms_meter";
|
static const char *const TAG = "dlms_meter";
|
||||||
|
static void log_callback(dlms_parser::LogLevel level, const char *fmt, va_list args) {
|
||||||
|
std::array<char, 256> buf;
|
||||||
|
vsnprintf(buf.data(), buf.size(), fmt, args);
|
||||||
|
switch (level) {
|
||||||
|
case dlms_parser::LogLevel::ERROR:
|
||||||
|
ESP_LOGE(TAG, "%s", buf.data());
|
||||||
|
break;
|
||||||
|
case dlms_parser::LogLevel::WARNING:
|
||||||
|
ESP_LOGW(TAG, "%s", buf.data());
|
||||||
|
break;
|
||||||
|
case dlms_parser::LogLevel::INFO:
|
||||||
|
ESP_LOGI(TAG, "%s", buf.data());
|
||||||
|
break;
|
||||||
|
case dlms_parser::LogLevel::VERBOSE:
|
||||||
|
ESP_LOGV(TAG, "%s", buf.data());
|
||||||
|
break;
|
||||||
|
case dlms_parser::LogLevel::VERY_VERBOSE:
|
||||||
|
ESP_LOGVV(TAG, "%s", buf.data());
|
||||||
|
break;
|
||||||
|
case dlms_parser::LogLevel::DEBUG:
|
||||||
|
ESP_LOGD(TAG, "%s", buf.data());
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
DlmsMeterComponent::DlmsMeterComponent(uint32_t receive_timeout_ms, bool skip_crc_check,
|
||||||
|
std::optional<std::array<uint8_t, 16>> decryption_key,
|
||||||
|
std::optional<std::array<uint8_t, 16>> authentication_key,
|
||||||
|
std::vector<CustomPattern> custom_patterns)
|
||||||
|
: receive_timeout_ms_(receive_timeout_ms),
|
||||||
|
skip_crc_check_(skip_crc_check),
|
||||||
|
custom_patterns_(std::move(custom_patterns)),
|
||||||
|
parser_(&decryptor_) {
|
||||||
|
dlms_parser::Logger::set_log_function(log_callback);
|
||||||
|
|
||||||
|
if (decryption_key.has_value()) {
|
||||||
|
#ifdef DLMS_METER_NO_CRYPTO
|
||||||
|
ESP_LOGE(TAG, "Decryption is not supported on this platform (no compatible crypto library found)");
|
||||||
|
#else
|
||||||
|
auto opt_key = dlms_parser::Aes128GcmDecryptionKey::from_bytes(decryption_key.value());
|
||||||
|
if (opt_key) {
|
||||||
|
this->parser_.set_decryption_key(*opt_key);
|
||||||
|
} else {
|
||||||
|
ESP_LOGE(TAG, "Failed to set decryption key: invalid key format");
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
if (authentication_key.has_value()) {
|
||||||
|
#ifdef DLMS_METER_NO_CRYPTO
|
||||||
|
ESP_LOGE(TAG, "Authentication is not supported on this platform (no compatible crypto library found)");
|
||||||
|
#else
|
||||||
|
auto opt_key = dlms_parser::Aes128GcmAuthenticationKey::from_bytes(authentication_key.value());
|
||||||
|
if (opt_key) {
|
||||||
|
this->parser_.set_authentication_key(*opt_key);
|
||||||
|
} else {
|
||||||
|
ESP_LOGE(TAG, "Failed to set authentication key: invalid key format");
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
this->parser_.set_skip_crc_check(this->skip_crc_check_);
|
||||||
|
|
||||||
|
this->parser_.load_default_patterns();
|
||||||
|
for (const auto &pattern : this->custom_patterns_) {
|
||||||
|
if (pattern.default_obis.has_value() && pattern.name.has_value()) {
|
||||||
|
this->parser_.register_pattern(pattern.name->c_str(), pattern.pattern.c_str(), pattern.priority,
|
||||||
|
pattern.default_obis.value());
|
||||||
|
} else if (pattern.name.has_value()) {
|
||||||
|
this->parser_.register_pattern(pattern.name->c_str(), pattern.pattern.c_str(), pattern.priority);
|
||||||
|
} else {
|
||||||
|
this->parser_.register_pattern(pattern.pattern.c_str());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void DlmsMeterComponent::setup() { this->flush_rx_buffer_(); }
|
||||||
|
|
||||||
void DlmsMeterComponent::dump_config() {
|
void DlmsMeterComponent::dump_config() {
|
||||||
const char *provider_name = this->provider_ == PROVIDER_NETZNOE ? "Netz NOE" : "Generic";
|
ESP_LOGCONFIG(TAG, "DLMS Meter:");
|
||||||
ESP_LOGCONFIG(TAG,
|
ESP_LOGCONFIG(TAG, " Receive Timeout: %u ms", this->receive_timeout_ms_);
|
||||||
"DLMS Meter:\n"
|
ESP_LOGCONFIG(TAG, " Skip CRC Check: %s", YESNO(this->skip_crc_check_));
|
||||||
" Provider: %s\n"
|
|
||||||
" Read Timeout: %" PRIu32 " ms",
|
for (const auto &pattern : this->custom_patterns_) {
|
||||||
provider_name, this->read_timeout_);
|
if (pattern.default_obis.has_value() && pattern.name.has_value()) {
|
||||||
#define DLMS_METER_LOG_SENSOR(s) LOG_SENSOR(" ", #s, this->s##_sensor_);
|
const auto &obis = pattern.default_obis.value();
|
||||||
DLMS_METER_SENSOR_LIST(DLMS_METER_LOG_SENSOR, )
|
ESP_LOGCONFIG(TAG, " Custom Pattern: '%s' (name: %s, priority: %d, default_obis: %d.%d.%d.%d.%d.%d)",
|
||||||
#define DLMS_METER_LOG_TEXT_SENSOR(s) LOG_TEXT_SENSOR(" ", #s, this->s##_text_sensor_);
|
pattern.pattern.c_str(), pattern.name->c_str(), pattern.priority, obis[0], obis[1], obis[2],
|
||||||
DLMS_METER_TEXT_SENSOR_LIST(DLMS_METER_LOG_TEXT_SENSOR, )
|
obis[3], obis[4], obis[5]);
|
||||||
|
} else if (pattern.name.has_value()) {
|
||||||
|
ESP_LOGCONFIG(TAG, " Custom Pattern: '%s' (name: %s, priority: %d)", pattern.pattern.c_str(),
|
||||||
|
pattern.name->c_str(), pattern.priority);
|
||||||
|
} else {
|
||||||
|
ESP_LOGCONFIG(TAG, " Custom Pattern: '%s'", pattern.pattern.c_str());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#ifdef USE_SENSOR
|
||||||
|
for (const auto &entry : this->sensors_) {
|
||||||
|
LOG_SENSOR(" ", "Numeric Sensor (OBIS)", entry.sensor);
|
||||||
|
ESP_LOGCONFIG(TAG, " OBIS: %s", entry.obis_code.c_str());
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
#ifdef USE_TEXT_SENSOR
|
||||||
|
for (const auto &entry : this->text_sensors_) {
|
||||||
|
LOG_TEXT_SENSOR(" ", "Text Sensor (OBIS)", entry.sensor);
|
||||||
|
ESP_LOGCONFIG(TAG, " OBIS: %s", entry.obis_code.c_str());
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
#ifdef USE_BINARY_SENSOR
|
||||||
|
for (const auto &entry : this->binary_sensors_) {
|
||||||
|
LOG_BINARY_SENSOR(" ", "Binary Sensor (OBIS)", entry.sensor);
|
||||||
|
ESP_LOGCONFIG(TAG, " OBIS: %s", entry.obis_code.c_str());
|
||||||
|
}
|
||||||
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
void DlmsMeterComponent::loop() {
|
void DlmsMeterComponent::loop() {
|
||||||
// Read while data is available, netznoe uses two frames so allow 2x max frame length
|
this->read_rx_buffer_();
|
||||||
size_t avail = this->available();
|
if (this->bytes_accumulated_ > 0 &&
|
||||||
if (avail > 0) {
|
App.get_loop_component_start_time() - this->last_rx_char_time_ > this->receive_timeout_ms_) {
|
||||||
size_t remaining = MBUS_MAX_FRAME_LENGTH * 2 - this->receive_buffer_.size();
|
this->process_frame_();
|
||||||
if (remaining == 0) {
|
|
||||||
ESP_LOGW(TAG, "Receive buffer full, dropping remaining bytes");
|
|
||||||
} else {
|
|
||||||
// Read all available bytes in batches to reduce UART call overhead.
|
|
||||||
// Cap reads to remaining buffer capacity.
|
|
||||||
if (avail > remaining) {
|
|
||||||
avail = remaining;
|
|
||||||
}
|
|
||||||
uint8_t buf[64];
|
|
||||||
while (avail > 0) {
|
|
||||||
size_t to_read = std::min(avail, sizeof(buf));
|
|
||||||
if (!this->read_array(buf, to_read)) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
avail -= to_read;
|
|
||||||
this->receive_buffer_.insert(this->receive_buffer_.end(), buf, buf + to_read);
|
|
||||||
this->last_read_ = millis();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!this->receive_buffer_.empty() && millis() - this->last_read_ > this->read_timeout_) {
|
|
||||||
this->mbus_payload_.clear();
|
|
||||||
if (!this->parse_mbus_(this->mbus_payload_))
|
|
||||||
return;
|
|
||||||
|
|
||||||
uint16_t message_length;
|
|
||||||
uint8_t systitle_length;
|
|
||||||
uint16_t header_offset;
|
|
||||||
if (!this->parse_dlms_(this->mbus_payload_, message_length, systitle_length, header_offset))
|
|
||||||
return;
|
|
||||||
|
|
||||||
if (message_length < DECODER_START_OFFSET || message_length > MAX_MESSAGE_LENGTH) {
|
|
||||||
ESP_LOGE(TAG, "DLMS: Message length invalid: %u", message_length);
|
|
||||||
this->receive_buffer_.clear();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Decrypt in place and then decode the OBIS codes
|
|
||||||
if (!this->decrypt_(this->mbus_payload_, message_length, systitle_length, header_offset))
|
|
||||||
return;
|
|
||||||
this->decode_obis_(&this->mbus_payload_[header_offset + DLMS_PAYLOAD_OFFSET], message_length);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
bool DlmsMeterComponent::parse_mbus_(std::vector<uint8_t> &mbus_payload) {
|
void DlmsMeterComponent::flush_rx_buffer_() {
|
||||||
ESP_LOGV(TAG, "Parsing M-Bus frames");
|
while (this->available()) {
|
||||||
uint16_t frame_offset = 0; // Offset is used if the M-Bus message is split into multiple frames
|
this->read();
|
||||||
|
|
||||||
while (frame_offset < this->receive_buffer_.size()) {
|
|
||||||
// Ensure enough bytes remain for the minimal intro header before accessing indices
|
|
||||||
if (this->receive_buffer_.size() - frame_offset < MBUS_HEADER_INTRO_LENGTH) {
|
|
||||||
ESP_LOGE(TAG, "MBUS: Not enough data for frame header (need %d, have %d)", MBUS_HEADER_INTRO_LENGTH,
|
|
||||||
(this->receive_buffer_.size() - frame_offset));
|
|
||||||
this->receive_buffer_.clear();
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check start bytes
|
|
||||||
if (this->receive_buffer_[frame_offset + MBUS_START1_OFFSET] != START_BYTE_LONG_FRAME ||
|
|
||||||
this->receive_buffer_[frame_offset + MBUS_START2_OFFSET] != START_BYTE_LONG_FRAME) {
|
|
||||||
ESP_LOGE(TAG, "MBUS: Start bytes do not match");
|
|
||||||
this->receive_buffer_.clear();
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Both length bytes must be identical
|
|
||||||
if (this->receive_buffer_[frame_offset + MBUS_LENGTH1_OFFSET] !=
|
|
||||||
this->receive_buffer_[frame_offset + MBUS_LENGTH2_OFFSET]) {
|
|
||||||
ESP_LOGE(TAG, "MBUS: Length bytes do not match");
|
|
||||||
this->receive_buffer_.clear();
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
uint8_t frame_length = this->receive_buffer_[frame_offset + MBUS_LENGTH1_OFFSET]; // Get length of this frame
|
|
||||||
|
|
||||||
// Check if received data is enough for the given frame length
|
|
||||||
if (this->receive_buffer_.size() - frame_offset <
|
|
||||||
frame_length + 3) { // length field inside packet does not account for second start- + checksum- + stop- byte
|
|
||||||
ESP_LOGE(TAG, "MBUS: Frame too big for received data");
|
|
||||||
this->receive_buffer_.clear();
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Ensure we have full frame (header + payload + checksum + stop byte) before accessing stop byte
|
|
||||||
size_t required_total =
|
|
||||||
frame_length + MBUS_HEADER_INTRO_LENGTH + MBUS_FOOTER_LENGTH; // payload + header + 2 footer bytes
|
|
||||||
if (this->receive_buffer_.size() - frame_offset < required_total) {
|
|
||||||
ESP_LOGE(TAG, "MBUS: Incomplete frame (need %d, have %d)", (unsigned int) required_total,
|
|
||||||
this->receive_buffer_.size() - frame_offset);
|
|
||||||
this->receive_buffer_.clear();
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this->receive_buffer_[frame_offset + frame_length + MBUS_HEADER_INTRO_LENGTH + MBUS_FOOTER_LENGTH - 1] !=
|
|
||||||
STOP_BYTE) {
|
|
||||||
ESP_LOGE(TAG, "MBUS: Invalid stop byte");
|
|
||||||
this->receive_buffer_.clear();
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Verify checksum: sum of all bytes starting at MBUS_HEADER_INTRO_LENGTH, take last byte
|
|
||||||
uint8_t checksum = 0; // use uint8_t so only the 8 least significant bits are stored
|
|
||||||
for (uint16_t i = 0; i < frame_length; i++) {
|
|
||||||
checksum += this->receive_buffer_[frame_offset + MBUS_HEADER_INTRO_LENGTH + i];
|
|
||||||
}
|
|
||||||
if (checksum != this->receive_buffer_[frame_offset + frame_length + MBUS_HEADER_INTRO_LENGTH]) {
|
|
||||||
ESP_LOGE(TAG, "MBUS: Invalid checksum: %x != %x", checksum,
|
|
||||||
this->receive_buffer_[frame_offset + frame_length + MBUS_HEADER_INTRO_LENGTH]);
|
|
||||||
this->receive_buffer_.clear();
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
mbus_payload.insert(mbus_payload.end(), &this->receive_buffer_[frame_offset + MBUS_FULL_HEADER_LENGTH],
|
|
||||||
&this->receive_buffer_[frame_offset + MBUS_HEADER_INTRO_LENGTH + frame_length]);
|
|
||||||
|
|
||||||
frame_offset += MBUS_HEADER_INTRO_LENGTH + frame_length + MBUS_FOOTER_LENGTH;
|
|
||||||
}
|
}
|
||||||
return true;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
bool DlmsMeterComponent::parse_dlms_(const std::vector<uint8_t> &mbus_payload, uint16_t &message_length,
|
void DlmsMeterComponent::read_rx_buffer_() {
|
||||||
uint8_t &systitle_length, uint16_t &header_offset) {
|
int available = this->available();
|
||||||
ESP_LOGV(TAG, "Parsing DLMS header");
|
if (available == 0)
|
||||||
if (mbus_payload.size() < DLMS_HEADER_LENGTH + DLMS_HEADER_EXT_OFFSET) {
|
return;
|
||||||
ESP_LOGE(TAG, "DLMS: Payload too short");
|
|
||||||
this->receive_buffer_.clear();
|
if (this->bytes_accumulated_ + available > this->rx_buffer_.size()) {
|
||||||
return false;
|
ESP_LOGW(TAG, "RX Buffer overflow. Frame too large! Dropping frame.");
|
||||||
|
this->bytes_accumulated_ = 0;
|
||||||
|
|
||||||
|
this->flush_rx_buffer_();
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (mbus_payload[DLMS_CIPHER_OFFSET] != GLO_CIPHERING) { // Only general-glo-ciphering is supported (0xDB)
|
bool success = this->read_array(this->rx_buffer_.data() + this->bytes_accumulated_, available);
|
||||||
ESP_LOGE(TAG, "DLMS: Unsupported cipher");
|
if (!success) {
|
||||||
this->receive_buffer_.clear();
|
ESP_LOGW(TAG, "UART read failed. Dropping frame.");
|
||||||
return false;
|
this->bytes_accumulated_ = 0;
|
||||||
|
this->flush_rx_buffer_();
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
systitle_length = mbus_payload[DLMS_SYST_OFFSET];
|
this->bytes_accumulated_ += available;
|
||||||
|
|
||||||
if (systitle_length != 0x08) { // Only system titles with length of 8 are supported
|
this->last_rx_char_time_ = App.get_loop_component_start_time();
|
||||||
ESP_LOGE(TAG, "DLMS: Unsupported system title length");
|
|
||||||
this->receive_buffer_.clear();
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
message_length = mbus_payload[DLMS_LENGTH_OFFSET];
|
|
||||||
header_offset = 0;
|
|
||||||
|
|
||||||
if (this->provider_ == PROVIDER_NETZNOE) {
|
|
||||||
// for some reason EVN seems to set the standard "length" field to 0x81 and then the actual length is in the next
|
|
||||||
// byte. Check some bytes to see if received data still matches expectation
|
|
||||||
if (message_length == NETZ_NOE_MAGIC_BYTE &&
|
|
||||||
mbus_payload[DLMS_LENGTH_OFFSET + 1] == NETZ_NOE_EXPECTED_MESSAGE_LENGTH &&
|
|
||||||
mbus_payload[DLMS_LENGTH_OFFSET + 2] == NETZ_NOE_EXPECTED_SECURITY_CONTROL_BYTE) {
|
|
||||||
message_length = mbus_payload[DLMS_LENGTH_OFFSET + 1];
|
|
||||||
header_offset = 1;
|
|
||||||
} else {
|
|
||||||
ESP_LOGE(TAG, "Wrong Length - Security Control Byte sequence detected for provider EVN");
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if (message_length == TWO_BYTE_LENGTH) {
|
|
||||||
message_length = encode_uint16(mbus_payload[DLMS_LENGTH_OFFSET + 1], mbus_payload[DLMS_LENGTH_OFFSET + 2]);
|
|
||||||
header_offset = DLMS_HEADER_EXT_OFFSET;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (message_length < DLMS_LENGTH_CORRECTION) {
|
|
||||||
ESP_LOGE(TAG, "DLMS: Message length too short: %u", message_length);
|
|
||||||
this->receive_buffer_.clear();
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
message_length -= DLMS_LENGTH_CORRECTION; // Correct message length due to part of header being included in length
|
|
||||||
|
|
||||||
if (mbus_payload.size() - DLMS_HEADER_LENGTH - header_offset != message_length) {
|
|
||||||
ESP_LOGV(TAG, "DLMS: Length mismatch - payload=%d, header=%d, offset=%d, message=%d", mbus_payload.size(),
|
|
||||||
DLMS_HEADER_LENGTH, header_offset, message_length);
|
|
||||||
ESP_LOGE(TAG, "DLMS: Message has invalid length");
|
|
||||||
this->receive_buffer_.clear();
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (mbus_payload[header_offset + DLMS_SECBYTE_OFFSET] != 0x21 &&
|
|
||||||
mbus_payload[header_offset + DLMS_SECBYTE_OFFSET] !=
|
|
||||||
0x20) { // Only certain security suite is supported (0x21 || 0x20)
|
|
||||||
ESP_LOGE(TAG, "DLMS: Unsupported security control byte");
|
|
||||||
this->receive_buffer_.clear();
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
bool DlmsMeterComponent::decrypt_(std::vector<uint8_t> &mbus_payload, uint16_t message_length, uint8_t systitle_length,
|
void DlmsMeterComponent::process_frame_() {
|
||||||
uint16_t header_offset) {
|
ESP_LOGV(TAG, "Processing frame of size: %zu bytes", this->bytes_accumulated_);
|
||||||
ESP_LOGV(TAG, "Decrypting payload");
|
|
||||||
uint8_t iv[12]; // Reserve space for the IV, always 12 bytes
|
|
||||||
// Copy system title to IV (System title is before length; no header offset needed!)
|
|
||||||
// Add 1 to the offset in order to skip the system title length byte
|
|
||||||
memcpy(&iv[0], &mbus_payload[DLMS_SYST_OFFSET + 1], systitle_length);
|
|
||||||
memcpy(&iv[8], &mbus_payload[header_offset + DLMS_FRAMECOUNTER_OFFSET],
|
|
||||||
DLMS_FRAMECOUNTER_LENGTH); // Copy frame counter to IV
|
|
||||||
|
|
||||||
uint8_t *payload_ptr = &mbus_payload[header_offset + DLMS_PAYLOAD_OFFSET];
|
auto callback = [this](const char *obis_code, float float_val, const char *str_val, bool is_numeric) {
|
||||||
|
this->on_data_(obis_code, float_val, str_val, is_numeric);
|
||||||
|
};
|
||||||
|
|
||||||
#if defined(USE_ESP8266_FRAMEWORK_ARDUINO)
|
this->parser_.parse({this->rx_buffer_.data(), this->bytes_accumulated_}, callback);
|
||||||
br_gcm_context gcm_ctx;
|
|
||||||
br_aes_ct_ctr_keys bc;
|
|
||||||
br_aes_ct_ctr_init(&bc, this->decryption_key_.data(), this->decryption_key_.size());
|
|
||||||
br_gcm_init(&gcm_ctx, &bc.vtable, br_ghash_ctmul32);
|
|
||||||
br_gcm_reset(&gcm_ctx, iv, sizeof(iv));
|
|
||||||
br_gcm_flip(&gcm_ctx);
|
|
||||||
br_gcm_run(&gcm_ctx, 0, payload_ptr, message_length);
|
|
||||||
#elif defined(USE_ESP32)
|
|
||||||
#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(6, 0, 0)
|
|
||||||
// PSA Crypto multipart AEAD (no tag verification, matching legacy behavior)
|
|
||||||
psa_key_attributes_t attributes = PSA_KEY_ATTRIBUTES_INIT;
|
|
||||||
psa_set_key_type(&attributes, PSA_KEY_TYPE_AES);
|
|
||||||
psa_set_key_bits(&attributes, this->decryption_key_.size() * 8);
|
|
||||||
psa_set_key_usage_flags(&attributes, PSA_KEY_USAGE_DECRYPT);
|
|
||||||
psa_set_key_algorithm(&attributes, PSA_ALG_GCM);
|
|
||||||
|
|
||||||
mbedtls_svc_key_id_t key_id;
|
this->bytes_accumulated_ = 0;
|
||||||
bool decrypt_failed = true;
|
}
|
||||||
if (psa_import_key(&attributes, this->decryption_key_.data(), this->decryption_key_.size(), &key_id) == PSA_SUCCESS) {
|
|
||||||
psa_aead_operation_t op = PSA_AEAD_OPERATION_INIT;
|
void DlmsMeterComponent::on_data_(const char *obis_code, float float_val, const char *str_val, bool is_numeric) {
|
||||||
if (psa_aead_decrypt_setup(&op, key_id, PSA_ALG_GCM) == PSA_SUCCESS &&
|
int updated_count = 0;
|
||||||
psa_aead_set_nonce(&op, iv, sizeof(iv)) == PSA_SUCCESS) {
|
|
||||||
size_t outlen = 0;
|
#ifdef USE_SENSOR
|
||||||
if (psa_aead_update(&op, payload_ptr, message_length, payload_ptr, message_length, &outlen) == PSA_SUCCESS &&
|
if (is_numeric) {
|
||||||
outlen == message_length) {
|
for (auto &item : this->sensors_) {
|
||||||
decrypt_failed = false;
|
if (item.obis_code == obis_code) {
|
||||||
|
item.sensor->publish_state(float_val);
|
||||||
|
updated_count++;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
psa_aead_abort(&op);
|
|
||||||
psa_destroy_key(key_id);
|
|
||||||
}
|
|
||||||
if (decrypt_failed) {
|
|
||||||
ESP_LOGE(TAG, "Decryption failed");
|
|
||||||
this->receive_buffer_.clear();
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
#else
|
|
||||||
size_t outlen = 0;
|
|
||||||
mbedtls_gcm_context gcm_ctx;
|
|
||||||
mbedtls_gcm_init(&gcm_ctx);
|
|
||||||
mbedtls_gcm_setkey(&gcm_ctx, MBEDTLS_CIPHER_ID_AES, this->decryption_key_.data(), this->decryption_key_.size() * 8);
|
|
||||||
mbedtls_gcm_starts(&gcm_ctx, MBEDTLS_GCM_DECRYPT, iv, sizeof(iv));
|
|
||||||
auto ret = mbedtls_gcm_update(&gcm_ctx, payload_ptr, message_length, payload_ptr, message_length, &outlen);
|
|
||||||
mbedtls_gcm_free(&gcm_ctx);
|
|
||||||
if (ret != 0) {
|
|
||||||
ESP_LOGE(TAG, "Decryption failed with error: %d", ret);
|
|
||||||
this->receive_buffer_.clear();
|
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
#else
|
|
||||||
#error "Invalid Platform"
|
#ifdef USE_TEXT_SENSOR
|
||||||
|
if (!is_numeric && str_val != nullptr) {
|
||||||
|
for (auto &item : this->text_sensors_) {
|
||||||
|
if (item.obis_code == obis_code) {
|
||||||
|
item.sensor->publish_state(str_val);
|
||||||
|
updated_count++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
if (payload_ptr[0] != DATA_NOTIFICATION || payload_ptr[5] != TIMESTAMP_DATETIME) {
|
#ifdef USE_BINARY_SENSOR
|
||||||
ESP_LOGE(TAG, "OBIS: Packet was decrypted but data is invalid");
|
if (is_numeric) {
|
||||||
this->receive_buffer_.clear();
|
bool state = float_val != 0.0f;
|
||||||
return false;
|
for (auto &item : this->binary_sensors_) {
|
||||||
}
|
if (item.obis_code == obis_code) {
|
||||||
ESP_LOGV(TAG, "Decrypted payload: %d bytes", message_length);
|
item.sensor->publish_state(state);
|
||||||
return true;
|
updated_count++;
|
||||||
}
|
|
||||||
|
|
||||||
void DlmsMeterComponent::decode_obis_(uint8_t *plaintext, uint16_t message_length) {
|
|
||||||
ESP_LOGV(TAG, "Decoding payload");
|
|
||||||
MeterData data{};
|
|
||||||
uint16_t current_position = DECODER_START_OFFSET;
|
|
||||||
bool power_factor_found = false;
|
|
||||||
|
|
||||||
while (current_position + OBIS_CODE_OFFSET <= message_length) {
|
|
||||||
if (plaintext[current_position + OBIS_TYPE_OFFSET] != DataType::OCTET_STRING) {
|
|
||||||
ESP_LOGE(TAG, "OBIS: Unsupported OBIS header type: %x", plaintext[current_position + OBIS_TYPE_OFFSET]);
|
|
||||||
this->receive_buffer_.clear();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
uint8_t obis_code_length = plaintext[current_position + OBIS_LENGTH_OFFSET];
|
|
||||||
if (obis_code_length != OBIS_CODE_LENGTH_STANDARD && obis_code_length != OBIS_CODE_LENGTH_EXTENDED) {
|
|
||||||
ESP_LOGE(TAG, "OBIS: Unsupported OBIS header length: %x", obis_code_length);
|
|
||||||
this->receive_buffer_.clear();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (current_position + OBIS_CODE_OFFSET + obis_code_length > message_length) {
|
|
||||||
ESP_LOGE(TAG, "OBIS: Buffer too short for OBIS code");
|
|
||||||
this->receive_buffer_.clear();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
uint8_t *obis_code = &plaintext[current_position + OBIS_CODE_OFFSET];
|
|
||||||
uint8_t obis_medium = obis_code[OBIS_A];
|
|
||||||
uint16_t obis_cd = encode_uint16(obis_code[OBIS_C], obis_code[OBIS_D]);
|
|
||||||
|
|
||||||
bool timestamp_found = false;
|
|
||||||
bool meter_number_found = false;
|
|
||||||
if (this->provider_ == PROVIDER_NETZNOE) {
|
|
||||||
// Do not advance Position when reading the Timestamp at DECODER_START_OFFSET
|
|
||||||
if ((obis_code_length == OBIS_CODE_LENGTH_EXTENDED) && (current_position == DECODER_START_OFFSET)) {
|
|
||||||
timestamp_found = true;
|
|
||||||
} else if (power_factor_found) {
|
|
||||||
meter_number_found = true;
|
|
||||||
power_factor_found = false;
|
|
||||||
} else {
|
|
||||||
current_position += obis_code_length + OBIS_CODE_OFFSET; // Advance past code and position
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
current_position += obis_code_length + OBIS_CODE_OFFSET; // Advance past code, position and type
|
|
||||||
}
|
|
||||||
if (!timestamp_found && !meter_number_found && obis_medium != Medium::ELECTRICITY &&
|
|
||||||
obis_medium != Medium::ABSTRACT) {
|
|
||||||
ESP_LOGE(TAG, "OBIS: Unsupported OBIS medium: %x", obis_medium);
|
|
||||||
this->receive_buffer_.clear();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (current_position >= message_length) {
|
|
||||||
ESP_LOGE(TAG, "OBIS: Buffer too short for data type");
|
|
||||||
this->receive_buffer_.clear();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
float value = 0.0f;
|
|
||||||
uint8_t value_size = 0;
|
|
||||||
uint8_t data_type = plaintext[current_position];
|
|
||||||
current_position++;
|
|
||||||
|
|
||||||
switch (data_type) {
|
|
||||||
case DataType::DOUBLE_LONG_UNSIGNED: {
|
|
||||||
value_size = 4;
|
|
||||||
if (current_position + value_size > message_length) {
|
|
||||||
ESP_LOGE(TAG, "OBIS: Buffer too short for DOUBLE_LONG_UNSIGNED");
|
|
||||||
this->receive_buffer_.clear();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
value = encode_uint32(plaintext[current_position + 0], plaintext[current_position + 1],
|
|
||||||
plaintext[current_position + 2], plaintext[current_position + 3]);
|
|
||||||
current_position += value_size;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case DataType::LONG_UNSIGNED: {
|
|
||||||
value_size = 2;
|
|
||||||
if (current_position + value_size > message_length) {
|
|
||||||
ESP_LOGE(TAG, "OBIS: Buffer too short for LONG_UNSIGNED");
|
|
||||||
this->receive_buffer_.clear();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
value = encode_uint16(plaintext[current_position + 0], plaintext[current_position + 1]);
|
|
||||||
current_position += value_size;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case DataType::OCTET_STRING: {
|
|
||||||
uint8_t data_length = plaintext[current_position];
|
|
||||||
current_position++; // Advance past string length
|
|
||||||
if (current_position + data_length > message_length) {
|
|
||||||
ESP_LOGE(TAG, "OBIS: Buffer too short for OCTET_STRING");
|
|
||||||
this->receive_buffer_.clear();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
// Handle timestamp (normal OBIS code or NETZNOE special case)
|
|
||||||
if (obis_cd == OBIS_TIMESTAMP || timestamp_found) {
|
|
||||||
if (data_length < 8) {
|
|
||||||
ESP_LOGE(TAG, "OBIS: Timestamp data too short: %u", data_length);
|
|
||||||
this->receive_buffer_.clear();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
uint16_t year = encode_uint16(plaintext[current_position + 0], plaintext[current_position + 1]);
|
|
||||||
uint8_t month = plaintext[current_position + 2];
|
|
||||||
uint8_t day = plaintext[current_position + 3];
|
|
||||||
uint8_t hour = plaintext[current_position + 5];
|
|
||||||
uint8_t minute = plaintext[current_position + 6];
|
|
||||||
uint8_t second = plaintext[current_position + 7];
|
|
||||||
if (year > 9999 || month > 12 || day > 31 || hour > 23 || minute > 59 || second > 59) {
|
|
||||||
ESP_LOGE(TAG, "Invalid timestamp values: %04u-%02u-%02uT%02u:%02u:%02uZ", year, month, day, hour, minute,
|
|
||||||
second);
|
|
||||||
this->receive_buffer_.clear();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
snprintf(data.timestamp, sizeof(data.timestamp), "%04u-%02u-%02uT%02u:%02u:%02uZ", year, month, day, hour,
|
|
||||||
minute, second);
|
|
||||||
} else if (meter_number_found) {
|
|
||||||
snprintf(data.meternumber, sizeof(data.meternumber), "%.*s", data_length, &plaintext[current_position]);
|
|
||||||
}
|
|
||||||
current_position += data_length;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
default:
|
|
||||||
ESP_LOGE(TAG, "OBIS: Unsupported OBIS data type: %x", data_type);
|
|
||||||
this->receive_buffer_.clear();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Skip break after data
|
|
||||||
if (this->provider_ == PROVIDER_NETZNOE) {
|
|
||||||
// Don't skip the break on the first timestamp, as there's none
|
|
||||||
if (!timestamp_found) {
|
|
||||||
current_position += 2;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
current_position += 2;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check for additional data (scaler-unit structure)
|
|
||||||
if (current_position < message_length && plaintext[current_position] == DataType::INTEGER) {
|
|
||||||
// Apply scaler: real_value = raw_value × 10^scaler
|
|
||||||
if (current_position + 1 < message_length) {
|
|
||||||
int8_t scaler = static_cast<int8_t>(plaintext[current_position + 1]);
|
|
||||||
if (scaler != 0) {
|
|
||||||
value *= pow10_int(scaler);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// on EVN Meters there is no additional break
|
|
||||||
if (this->provider_ == PROVIDER_NETZNOE) {
|
|
||||||
current_position += 4;
|
|
||||||
} else {
|
|
||||||
current_position += 6;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Handle numeric values (LONG_UNSIGNED and DOUBLE_LONG_UNSIGNED)
|
|
||||||
if (value_size > 0) {
|
|
||||||
switch (obis_cd) {
|
|
||||||
case OBIS_VOLTAGE_L1:
|
|
||||||
data.voltage_l1 = value;
|
|
||||||
break;
|
|
||||||
case OBIS_VOLTAGE_L2:
|
|
||||||
data.voltage_l2 = value;
|
|
||||||
break;
|
|
||||||
case OBIS_VOLTAGE_L3:
|
|
||||||
data.voltage_l3 = value;
|
|
||||||
break;
|
|
||||||
case OBIS_CURRENT_L1:
|
|
||||||
data.current_l1 = value;
|
|
||||||
break;
|
|
||||||
case OBIS_CURRENT_L2:
|
|
||||||
data.current_l2 = value;
|
|
||||||
break;
|
|
||||||
case OBIS_CURRENT_L3:
|
|
||||||
data.current_l3 = value;
|
|
||||||
break;
|
|
||||||
case OBIS_ACTIVE_POWER_PLUS:
|
|
||||||
data.active_power_plus = value;
|
|
||||||
break;
|
|
||||||
case OBIS_ACTIVE_POWER_MINUS:
|
|
||||||
data.active_power_minus = value;
|
|
||||||
break;
|
|
||||||
case OBIS_ACTIVE_ENERGY_PLUS:
|
|
||||||
data.active_energy_plus = value;
|
|
||||||
break;
|
|
||||||
case OBIS_ACTIVE_ENERGY_MINUS:
|
|
||||||
data.active_energy_minus = value;
|
|
||||||
break;
|
|
||||||
case OBIS_REACTIVE_ENERGY_PLUS:
|
|
||||||
data.reactive_energy_plus = value;
|
|
||||||
break;
|
|
||||||
case OBIS_REACTIVE_ENERGY_MINUS:
|
|
||||||
data.reactive_energy_minus = value;
|
|
||||||
break;
|
|
||||||
case OBIS_POWER_FACTOR:
|
|
||||||
data.power_factor = value;
|
|
||||||
power_factor_found = true;
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
ESP_LOGW(TAG, "Unsupported OBIS code 0x%04X", obis_cd);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
this->receive_buffer_.clear();
|
if (updated_count == 0) {
|
||||||
|
ESP_LOGV(TAG, "Received OBIS %s, but no sensors are registered for it.", obis_code);
|
||||||
ESP_LOGI(TAG, "Received valid data");
|
}
|
||||||
this->publish_sensors(data);
|
|
||||||
this->status_clear_warning();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#ifdef USE_SENSOR
|
||||||
|
void DlmsMeterComponent::register_sensor(const std::string &obis_code, sensor::Sensor *sensor) {
|
||||||
|
this->sensors_.push_back({obis_code, sensor});
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
#ifdef USE_TEXT_SENSOR
|
||||||
|
void DlmsMeterComponent::register_text_sensor(const std::string &obis_code, text_sensor::TextSensor *sensor) {
|
||||||
|
this->text_sensors_.push_back({obis_code, sensor});
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
#ifdef USE_BINARY_SENSOR
|
||||||
|
void DlmsMeterComponent::register_binary_sensor(const std::string &obis_code, binary_sensor::BinarySensor *sensor) {
|
||||||
|
this->binary_sensors_.push_back({obis_code, sensor});
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
} // namespace esphome::dlms_meter
|
} // namespace esphome::dlms_meter
|
||||||
|
|||||||
@@ -2,95 +2,150 @@
|
|||||||
|
|
||||||
#include "esphome/core/component.h"
|
#include "esphome/core/component.h"
|
||||||
#include "esphome/core/defines.h"
|
#include "esphome/core/defines.h"
|
||||||
|
#include "esphome/core/application.h"
|
||||||
#include "esphome/core/log.h"
|
#include "esphome/core/log.h"
|
||||||
|
#include "esphome/core/helpers.h"
|
||||||
|
#include "esphome/components/uart/uart.h"
|
||||||
|
|
||||||
#ifdef USE_SENSOR
|
#ifdef USE_SENSOR
|
||||||
#include "esphome/components/sensor/sensor.h"
|
#include "esphome/components/sensor/sensor.h"
|
||||||
#endif
|
#endif
|
||||||
#ifdef USE_TEXT_SENSOR
|
#ifdef USE_TEXT_SENSOR
|
||||||
#include "esphome/components/text_sensor/text_sensor.h"
|
#include "esphome/components/text_sensor/text_sensor.h"
|
||||||
#endif
|
#endif
|
||||||
#include "esphome/components/uart/uart.h"
|
#ifdef USE_BINARY_SENSOR
|
||||||
|
#include "esphome/components/binary_sensor/binary_sensor.h"
|
||||||
|
#endif
|
||||||
|
|
||||||
#include "mbus.h"
|
#include <dlms_parser/dlms_parser.h>
|
||||||
#include "dlms.h"
|
|
||||||
#include "obis.h"
|
|
||||||
|
|
||||||
#include <array>
|
|
||||||
#include <vector>
|
#include <vector>
|
||||||
|
#include <string>
|
||||||
|
#include <array>
|
||||||
|
#include <optional>
|
||||||
|
#include <span>
|
||||||
|
|
||||||
|
#if __has_include(<psa/crypto.h>)
|
||||||
|
#include <dlms_parser/decryption/aes_128_gcm_decryptor_tfpsa.h>
|
||||||
|
#elif !defined(USE_ESP8266) && __has_include(<mbedtls/gcm.h>)
|
||||||
|
#if __has_include(<mbedtls/esp_config.h>)
|
||||||
|
#include <mbedtls/esp_config.h>
|
||||||
|
#endif
|
||||||
|
#include <dlms_parser/decryption/aes_128_gcm_decryptor_mbedtls.h>
|
||||||
|
#elif __has_include(<bearssl/bearssl.h>)
|
||||||
|
#include <dlms_parser/decryption/aes_128_gcm_decryptor_bearssl.h>
|
||||||
|
#else
|
||||||
|
#define DLMS_METER_NO_CRYPTO
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#ifndef DLMS_MAX_SENSORS
|
||||||
|
static constexpr uint8_t DLMS_MAX_SENSORS = 0;
|
||||||
|
#endif
|
||||||
|
#ifndef DLMS_MAX_TEXT_SENSORS
|
||||||
|
static constexpr uint8_t DLMS_MAX_TEXT_SENSORS = 0;
|
||||||
|
#endif
|
||||||
|
#ifndef DLMS_MAX_BINARY_SENSORS
|
||||||
|
static constexpr uint8_t DLMS_MAX_BINARY_SENSORS = 0;
|
||||||
|
#endif
|
||||||
|
|
||||||
namespace esphome::dlms_meter {
|
namespace esphome::dlms_meter {
|
||||||
|
|
||||||
#ifndef DLMS_METER_SENSOR_LIST
|
#ifdef DLMS_METER_NO_CRYPTO
|
||||||
#define DLMS_METER_SENSOR_LIST(F, SEP)
|
// Fallback dummy decryptor for platforms without supported crypto (e.g., Zephyr during clang-tidy)
|
||||||
#endif
|
class Aes128GcmDecryptorDummy : public dlms_parser::Aes128GcmDecryptor {
|
||||||
|
public:
|
||||||
#ifndef DLMS_METER_TEXT_SENSOR_LIST
|
void set_decryption_key(const dlms_parser::Aes128GcmDecryptionKey &key) override {}
|
||||||
#define DLMS_METER_TEXT_SENSOR_LIST(F, SEP)
|
bool decrypt_in_place(std::span<const uint8_t> iv, std::span<uint8_t> ciphertext_and_plaintext,
|
||||||
#endif
|
std::span<const uint8_t> aad, std::span<const uint8_t> tag) override {
|
||||||
|
return false;
|
||||||
struct MeterData {
|
}
|
||||||
float voltage_l1 = 0.0f; // Voltage L1
|
|
||||||
float voltage_l2 = 0.0f; // Voltage L2
|
|
||||||
float voltage_l3 = 0.0f; // Voltage L3
|
|
||||||
float current_l1 = 0.0f; // Current L1
|
|
||||||
float current_l2 = 0.0f; // Current L2
|
|
||||||
float current_l3 = 0.0f; // Current L3
|
|
||||||
float active_power_plus = 0.0f; // Active power taken from grid
|
|
||||||
float active_power_minus = 0.0f; // Active power put into grid
|
|
||||||
float active_energy_plus = 0.0f; // Active energy taken from grid
|
|
||||||
float active_energy_minus = 0.0f; // Active energy put into grid
|
|
||||||
float reactive_energy_plus = 0.0f; // Reactive energy taken from grid
|
|
||||||
float reactive_energy_minus = 0.0f; // Reactive energy put into grid
|
|
||||||
char timestamp[27]{}; // Text sensor for the timestamp value
|
|
||||||
|
|
||||||
// Netz NOE
|
|
||||||
float power_factor = 0.0f; // Power Factor
|
|
||||||
char meternumber[13]{}; // Text sensor for the meterNumber value
|
|
||||||
};
|
};
|
||||||
|
#endif
|
||||||
|
|
||||||
// Provider constants
|
#if __has_include(<psa/crypto.h>)
|
||||||
enum Providers : uint32_t { PROVIDER_GENERIC = 0x00, PROVIDER_NETZNOE = 0x01 };
|
using Aes128GcmDecryptorImpl = dlms_parser::Aes128GcmDecryptorTfPsa;
|
||||||
|
#elif !defined(USE_ESP8266) && __has_include(<mbedtls/gcm.h>)
|
||||||
|
using Aes128GcmDecryptorImpl = dlms_parser::Aes128GcmDecryptorMbedTls;
|
||||||
|
#elif __has_include(<bearssl/bearssl.h>)
|
||||||
|
using Aes128GcmDecryptorImpl = dlms_parser::Aes128GcmDecryptorBearSsl;
|
||||||
|
#else
|
||||||
|
using Aes128GcmDecryptorImpl = Aes128GcmDecryptorDummy;
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#ifdef USE_SENSOR
|
||||||
|
struct SensorItem {
|
||||||
|
std::string obis_code;
|
||||||
|
sensor::Sensor *sensor;
|
||||||
|
};
|
||||||
|
#endif
|
||||||
|
#ifdef USE_TEXT_SENSOR
|
||||||
|
struct TextSensorItem {
|
||||||
|
std::string obis_code;
|
||||||
|
text_sensor::TextSensor *sensor;
|
||||||
|
};
|
||||||
|
#endif
|
||||||
|
#ifdef USE_BINARY_SENSOR
|
||||||
|
struct BinarySensorItem {
|
||||||
|
std::string obis_code;
|
||||||
|
binary_sensor::BinarySensor *sensor;
|
||||||
|
};
|
||||||
|
#endif
|
||||||
|
|
||||||
|
struct CustomPattern {
|
||||||
|
std::string pattern;
|
||||||
|
std::optional<std::string> name;
|
||||||
|
int priority{0};
|
||||||
|
std::optional<std::array<uint8_t, 6>> default_obis;
|
||||||
|
};
|
||||||
|
|
||||||
class DlmsMeterComponent : public Component, public uart::UARTDevice {
|
class DlmsMeterComponent : public Component, public uart::UARTDevice {
|
||||||
public:
|
public:
|
||||||
DlmsMeterComponent() = default;
|
DlmsMeterComponent(uint32_t receive_timeout_ms, bool skip_crc_check,
|
||||||
|
std::optional<std::array<uint8_t, 16>> decryption_key,
|
||||||
|
std::optional<std::array<uint8_t, 16>> authentication_key,
|
||||||
|
std::vector<CustomPattern> custom_patterns);
|
||||||
|
|
||||||
|
void setup() override;
|
||||||
void dump_config() override;
|
void dump_config() override;
|
||||||
void loop() override;
|
void loop() override;
|
||||||
|
|
||||||
void set_decryption_key(const std::array<uint8_t, 16> &key) { this->decryption_key_ = key; }
|
#ifdef USE_SENSOR
|
||||||
void set_provider(uint32_t provider) { this->provider_ = provider; }
|
void register_sensor(const std::string &obis_code, sensor::Sensor *sensor);
|
||||||
|
#endif
|
||||||
void publish_sensors(MeterData &data) {
|
#ifdef USE_TEXT_SENSOR
|
||||||
#define DLMS_METER_PUBLISH_SENSOR(s) \
|
void register_text_sensor(const std::string &obis_code, text_sensor::TextSensor *sensor);
|
||||||
if (this->s##_sensor_ != nullptr) \
|
#endif
|
||||||
s##_sensor_->publish_state(data.s);
|
#ifdef USE_BINARY_SENSOR
|
||||||
DLMS_METER_SENSOR_LIST(DLMS_METER_PUBLISH_SENSOR, )
|
void register_binary_sensor(const std::string &obis_code, binary_sensor::BinarySensor *sensor);
|
||||||
|
#endif
|
||||||
#define DLMS_METER_PUBLISH_TEXT_SENSOR(s) \
|
|
||||||
if (this->s##_text_sensor_ != nullptr) \
|
|
||||||
s##_text_sensor_->publish_state(data.s);
|
|
||||||
DLMS_METER_TEXT_SENSOR_LIST(DLMS_METER_PUBLISH_TEXT_SENSOR, )
|
|
||||||
}
|
|
||||||
|
|
||||||
DLMS_METER_SENSOR_LIST(SUB_SENSOR, )
|
|
||||||
DLMS_METER_TEXT_SENSOR_LIST(SUB_TEXT_SENSOR, )
|
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
bool parse_mbus_(std::vector<uint8_t> &mbus_payload);
|
void read_rx_buffer_();
|
||||||
bool parse_dlms_(const std::vector<uint8_t> &mbus_payload, uint16_t &message_length, uint8_t &systitle_length,
|
void flush_rx_buffer_();
|
||||||
uint16_t &header_offset);
|
void process_frame_();
|
||||||
bool decrypt_(std::vector<uint8_t> &mbus_payload, uint16_t message_length, uint8_t systitle_length,
|
void on_data_(const char *obis_code, float float_val, const char *str_val, bool is_numeric);
|
||||||
uint16_t header_offset);
|
|
||||||
void decode_obis_(uint8_t *plaintext, uint16_t message_length);
|
|
||||||
|
|
||||||
std::vector<uint8_t> receive_buffer_; // Stores the packet currently being received
|
std::array<uint8_t, 2048> rx_buffer_;
|
||||||
std::vector<uint8_t> mbus_payload_; // Parsed M-Bus payload, reused to avoid heap churn
|
size_t bytes_accumulated_{0};
|
||||||
uint32_t last_read_ = 0; // Timestamp when data was last read
|
uint32_t last_rx_char_time_{0};
|
||||||
uint32_t read_timeout_ = 1000; // Time to wait after last byte before considering data complete
|
|
||||||
|
|
||||||
uint32_t provider_ = PROVIDER_GENERIC; // Provider of the meter / your grid operator
|
uint32_t receive_timeout_ms_{1000};
|
||||||
std::array<uint8_t, 16> decryption_key_;
|
bool skip_crc_check_{false};
|
||||||
|
|
||||||
|
std::vector<CustomPattern> custom_patterns_;
|
||||||
|
|
||||||
|
Aes128GcmDecryptorImpl decryptor_;
|
||||||
|
dlms_parser::DlmsParser parser_;
|
||||||
|
|
||||||
|
#ifdef USE_SENSOR
|
||||||
|
StaticVector<SensorItem, DLMS_MAX_SENSORS> sensors_;
|
||||||
|
#endif
|
||||||
|
#ifdef USE_TEXT_SENSOR
|
||||||
|
StaticVector<TextSensorItem, DLMS_MAX_TEXT_SENSORS> text_sensors_;
|
||||||
|
#endif
|
||||||
|
#ifdef USE_BINARY_SENSOR
|
||||||
|
StaticVector<BinarySensorItem, DLMS_MAX_BINARY_SENSORS> binary_sensors_;
|
||||||
|
#endif
|
||||||
};
|
};
|
||||||
|
|
||||||
} // namespace esphome::dlms_meter
|
} // namespace esphome::dlms_meter
|
||||||
|
|||||||
@@ -1,69 +0,0 @@
|
|||||||
#pragma once
|
|
||||||
|
|
||||||
#include <cstdint>
|
|
||||||
|
|
||||||
namespace esphome::dlms_meter {
|
|
||||||
|
|
||||||
/*
|
|
||||||
+----------------------------------------------------+ -
|
|
||||||
| Start Character [0x68] | \
|
|
||||||
+----------------------------------------------------+ |
|
|
||||||
| Data Length (L) | |
|
|
||||||
+----------------------------------------------------+ |
|
|
||||||
| Data Length Repeat (L) | |
|
|
||||||
+----------------------------------------------------+ > M-Bus Data link layer
|
|
||||||
| Start Character Repeat [0x68] | |
|
|
||||||
+----------------------------------------------------+ |
|
|
||||||
| Control/Function Field (C) | |
|
|
||||||
+----------------------------------------------------+ |
|
|
||||||
| Address Field (A) | /
|
|
||||||
+----------------------------------------------------+ -
|
|
||||||
| Control Information Field (CI) | \
|
|
||||||
+----------------------------------------------------+ |
|
|
||||||
| Source Transport Service Access Point (STSAP) | > DLMS/COSEM M-Bus transport layer
|
|
||||||
+----------------------------------------------------+ |
|
|
||||||
| Destination Transport Service Access Point (DTSAP) | /
|
|
||||||
+----------------------------------------------------+ -
|
|
||||||
| | \
|
|
||||||
~ ~ |
|
|
||||||
Data > DLMS/COSEM Application Layer
|
|
||||||
~ ~ |
|
|
||||||
| | /
|
|
||||||
+----------------------------------------------------+ -
|
|
||||||
| Checksum | \
|
|
||||||
+----------------------------------------------------+ > M-Bus Data link layer
|
|
||||||
| Stop Character [0x16] | /
|
|
||||||
+----------------------------------------------------+ -
|
|
||||||
|
|
||||||
Data_Length = L - C - A - CI
|
|
||||||
Each line (except Data) is one Byte
|
|
||||||
|
|
||||||
Possible Values found in publicly available docs:
|
|
||||||
- C: 0x53/0x73 (SND_UD)
|
|
||||||
- A: FF (Broadcast)
|
|
||||||
- CI: 0x00-0x1F/0x60/0x61/0x7C/0x7D
|
|
||||||
- STSAP: 0x01 (Management Logical Device ID 1 of the meter)
|
|
||||||
- DTSAP: 0x67 (Consumer Information Push Client ID 103)
|
|
||||||
*/
|
|
||||||
|
|
||||||
// MBUS start bytes for different telegram formats:
|
|
||||||
// - Single Character: 0xE5 (length=1)
|
|
||||||
// - Short Frame: 0x10 (length=5)
|
|
||||||
// - Control Frame: 0x68 (length=9)
|
|
||||||
// - Long Frame: 0x68 (length=9+data_length)
|
|
||||||
// This component currently only uses Long Frame.
|
|
||||||
static constexpr uint8_t START_BYTE_SINGLE_CHARACTER = 0xE5;
|
|
||||||
static constexpr uint8_t START_BYTE_SHORT_FRAME = 0x10;
|
|
||||||
static constexpr uint8_t START_BYTE_CONTROL_FRAME = 0x68;
|
|
||||||
static constexpr uint8_t START_BYTE_LONG_FRAME = 0x68;
|
|
||||||
static constexpr uint8_t MBUS_HEADER_INTRO_LENGTH = 4; // Header length for the intro (0x68, length, length, 0x68)
|
|
||||||
static constexpr uint8_t MBUS_FULL_HEADER_LENGTH = 9; // Total header length
|
|
||||||
static constexpr uint8_t MBUS_FOOTER_LENGTH = 2; // Footer after frame
|
|
||||||
static constexpr uint8_t MBUS_MAX_FRAME_LENGTH = 250; // Maximum size of frame
|
|
||||||
static constexpr uint8_t MBUS_START1_OFFSET = 0; // Offset of first start byte
|
|
||||||
static constexpr uint8_t MBUS_LENGTH1_OFFSET = 1; // Offset of first length byte
|
|
||||||
static constexpr uint8_t MBUS_LENGTH2_OFFSET = 2; // Offset of (duplicated) second length byte
|
|
||||||
static constexpr uint8_t MBUS_START2_OFFSET = 3; // Offset of (duplicated) second start byte
|
|
||||||
static constexpr uint8_t STOP_BYTE = 0x16;
|
|
||||||
|
|
||||||
} // namespace esphome::dlms_meter
|
|
||||||
@@ -1,94 +0,0 @@
|
|||||||
#pragma once
|
|
||||||
|
|
||||||
#include <cstdint>
|
|
||||||
|
|
||||||
namespace esphome::dlms_meter {
|
|
||||||
|
|
||||||
// Data types as per specification
|
|
||||||
enum DataType {
|
|
||||||
NULL_DATA = 0x00,
|
|
||||||
BOOLEAN = 0x03,
|
|
||||||
BIT_STRING = 0x04,
|
|
||||||
DOUBLE_LONG = 0x05,
|
|
||||||
DOUBLE_LONG_UNSIGNED = 0x06,
|
|
||||||
OCTET_STRING = 0x09,
|
|
||||||
VISIBLE_STRING = 0x0A,
|
|
||||||
UTF8_STRING = 0x0C,
|
|
||||||
BINARY_CODED_DECIMAL = 0x0D,
|
|
||||||
INTEGER = 0x0F,
|
|
||||||
LONG = 0x10,
|
|
||||||
UNSIGNED = 0x11,
|
|
||||||
LONG_UNSIGNED = 0x12,
|
|
||||||
LONG64 = 0x14,
|
|
||||||
LONG64_UNSIGNED = 0x15,
|
|
||||||
ENUM = 0x16,
|
|
||||||
FLOAT32 = 0x17,
|
|
||||||
FLOAT64 = 0x18,
|
|
||||||
DATE_TIME = 0x19,
|
|
||||||
DATE = 0x1A,
|
|
||||||
TIME = 0x1B,
|
|
||||||
|
|
||||||
ARRAY = 0x01,
|
|
||||||
STRUCTURE = 0x02,
|
|
||||||
COMPACT_ARRAY = 0x13
|
|
||||||
};
|
|
||||||
|
|
||||||
enum Medium {
|
|
||||||
ABSTRACT = 0x00,
|
|
||||||
ELECTRICITY = 0x01,
|
|
||||||
HEAT_COST_ALLOCATOR = 0x04,
|
|
||||||
COOLING = 0x05,
|
|
||||||
HEAT = 0x06,
|
|
||||||
GAS = 0x07,
|
|
||||||
COLD_WATER = 0x08,
|
|
||||||
HOT_WATER = 0x09,
|
|
||||||
OIL = 0x10,
|
|
||||||
COMPRESSED_AIR = 0x11,
|
|
||||||
NITROGEN = 0x12
|
|
||||||
};
|
|
||||||
|
|
||||||
// Data structure
|
|
||||||
static constexpr uint8_t DECODER_START_OFFSET = 20; // Skip header, timestamp and break block
|
|
||||||
static constexpr uint8_t OBIS_TYPE_OFFSET = 0;
|
|
||||||
static constexpr uint8_t OBIS_LENGTH_OFFSET = 1;
|
|
||||||
static constexpr uint8_t OBIS_CODE_OFFSET = 2;
|
|
||||||
static constexpr uint8_t OBIS_CODE_LENGTH_STANDARD = 0x06; // 6-byte OBIS code (A.B.C.D.E.F)
|
|
||||||
static constexpr uint8_t OBIS_CODE_LENGTH_EXTENDED = 0x0C; // 12-byte extended OBIS code
|
|
||||||
static constexpr uint8_t OBIS_A = 0;
|
|
||||||
static constexpr uint8_t OBIS_B = 1;
|
|
||||||
static constexpr uint8_t OBIS_C = 2;
|
|
||||||
static constexpr uint8_t OBIS_D = 3;
|
|
||||||
static constexpr uint8_t OBIS_E = 4;
|
|
||||||
static constexpr uint8_t OBIS_F = 5;
|
|
||||||
|
|
||||||
// Metadata
|
|
||||||
static constexpr uint16_t OBIS_TIMESTAMP = 0x0100;
|
|
||||||
static constexpr uint16_t OBIS_SERIAL_NUMBER = 0x6001;
|
|
||||||
static constexpr uint16_t OBIS_DEVICE_NAME = 0x2A00;
|
|
||||||
|
|
||||||
// Voltage
|
|
||||||
static constexpr uint16_t OBIS_VOLTAGE_L1 = 0x2007;
|
|
||||||
static constexpr uint16_t OBIS_VOLTAGE_L2 = 0x3407;
|
|
||||||
static constexpr uint16_t OBIS_VOLTAGE_L3 = 0x4807;
|
|
||||||
|
|
||||||
// Current
|
|
||||||
static constexpr uint16_t OBIS_CURRENT_L1 = 0x1F07;
|
|
||||||
static constexpr uint16_t OBIS_CURRENT_L2 = 0x3307;
|
|
||||||
static constexpr uint16_t OBIS_CURRENT_L3 = 0x4707;
|
|
||||||
|
|
||||||
// Power
|
|
||||||
static constexpr uint16_t OBIS_ACTIVE_POWER_PLUS = 0x0107;
|
|
||||||
static constexpr uint16_t OBIS_ACTIVE_POWER_MINUS = 0x0207;
|
|
||||||
|
|
||||||
// Active energy
|
|
||||||
static constexpr uint16_t OBIS_ACTIVE_ENERGY_PLUS = 0x0108;
|
|
||||||
static constexpr uint16_t OBIS_ACTIVE_ENERGY_MINUS = 0x0208;
|
|
||||||
|
|
||||||
// Reactive energy
|
|
||||||
static constexpr uint16_t OBIS_REACTIVE_ENERGY_PLUS = 0x0308;
|
|
||||||
static constexpr uint16_t OBIS_REACTIVE_ENERGY_MINUS = 0x0408;
|
|
||||||
|
|
||||||
// Netz NOE specific
|
|
||||||
static constexpr uint16_t OBIS_POWER_FACTOR = 0x0D07;
|
|
||||||
|
|
||||||
} // namespace esphome::dlms_meter
|
|
||||||
@@ -1,8 +1,9 @@
|
|||||||
|
import logging
|
||||||
|
|
||||||
import esphome.codegen as cg
|
import esphome.codegen as cg
|
||||||
from esphome.components import sensor
|
from esphome.components import sensor
|
||||||
import esphome.config_validation as cv
|
import esphome.config_validation as cv
|
||||||
from esphome.const import (
|
from esphome.const import (
|
||||||
CONF_ID,
|
|
||||||
DEVICE_CLASS_CURRENT,
|
DEVICE_CLASS_CURRENT,
|
||||||
DEVICE_CLASS_ENERGY,
|
DEVICE_CLASS_ENERGY,
|
||||||
DEVICE_CLASS_POWER,
|
DEVICE_CLASS_POWER,
|
||||||
@@ -16,109 +17,142 @@ from esphome.const import (
|
|||||||
UNIT_WATT_HOURS,
|
UNIT_WATT_HOURS,
|
||||||
)
|
)
|
||||||
|
|
||||||
from .. import CONF_DLMS_METER_ID, DlmsMeterComponent
|
from .. import CONF_DLMS_METER_ID, CONF_OBIS_CODE, DlmsMeterComponent, obis_code
|
||||||
|
|
||||||
AUTO_LOAD = ["dlms_meter"]
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
CONFIG_SCHEMA = cv.Schema(
|
DEPENDENCIES = ["dlms_meter"]
|
||||||
|
|
||||||
|
NUMERIC_KEYS = {
|
||||||
|
"voltage_l1": "1.0.32.7.0.255",
|
||||||
|
"voltage_l2": "1.0.52.7.0.255",
|
||||||
|
"voltage_l3": "1.0.72.7.0.255",
|
||||||
|
"current_l1": "1.0.31.7.0.255",
|
||||||
|
"current_l2": "1.0.51.7.0.255",
|
||||||
|
"current_l3": "1.0.71.7.0.255",
|
||||||
|
"active_power_plus": "1.0.1.7.0.255",
|
||||||
|
"active_power_minus": "1.0.2.7.0.255",
|
||||||
|
"active_energy_plus": "1.0.1.8.0.255",
|
||||||
|
"active_energy_minus": "1.0.2.8.0.255",
|
||||||
|
"reactive_energy_plus": "1.0.3.8.0.255",
|
||||||
|
"reactive_energy_minus": "1.0.4.8.0.255",
|
||||||
|
"power_factor": "1.0.13.7.0.255",
|
||||||
|
}
|
||||||
|
|
||||||
|
DYNAMIC_SCHEMA = sensor.sensor_schema().extend(
|
||||||
{
|
{
|
||||||
cv.GenerateID(CONF_DLMS_METER_ID): cv.use_id(DlmsMeterComponent),
|
cv.GenerateID(CONF_DLMS_METER_ID): cv.use_id(DlmsMeterComponent),
|
||||||
cv.Optional("voltage_l1"): sensor.sensor_schema(
|
cv.Required(CONF_OBIS_CODE): obis_code,
|
||||||
unit_of_measurement=UNIT_VOLT,
|
|
||||||
accuracy_decimals=1,
|
|
||||||
device_class=DEVICE_CLASS_VOLTAGE,
|
|
||||||
state_class=STATE_CLASS_MEASUREMENT,
|
|
||||||
),
|
|
||||||
cv.Optional("voltage_l2"): sensor.sensor_schema(
|
|
||||||
unit_of_measurement=UNIT_VOLT,
|
|
||||||
accuracy_decimals=1,
|
|
||||||
device_class=DEVICE_CLASS_VOLTAGE,
|
|
||||||
state_class=STATE_CLASS_MEASUREMENT,
|
|
||||||
),
|
|
||||||
cv.Optional("voltage_l3"): sensor.sensor_schema(
|
|
||||||
unit_of_measurement=UNIT_VOLT,
|
|
||||||
accuracy_decimals=1,
|
|
||||||
device_class=DEVICE_CLASS_VOLTAGE,
|
|
||||||
state_class=STATE_CLASS_MEASUREMENT,
|
|
||||||
),
|
|
||||||
cv.Optional("current_l1"): sensor.sensor_schema(
|
|
||||||
unit_of_measurement=UNIT_AMPERE,
|
|
||||||
accuracy_decimals=2,
|
|
||||||
device_class=DEVICE_CLASS_CURRENT,
|
|
||||||
state_class=STATE_CLASS_MEASUREMENT,
|
|
||||||
),
|
|
||||||
cv.Optional("current_l2"): sensor.sensor_schema(
|
|
||||||
unit_of_measurement=UNIT_AMPERE,
|
|
||||||
accuracy_decimals=2,
|
|
||||||
device_class=DEVICE_CLASS_CURRENT,
|
|
||||||
state_class=STATE_CLASS_MEASUREMENT,
|
|
||||||
),
|
|
||||||
cv.Optional("current_l3"): sensor.sensor_schema(
|
|
||||||
unit_of_measurement=UNIT_AMPERE,
|
|
||||||
accuracy_decimals=2,
|
|
||||||
device_class=DEVICE_CLASS_CURRENT,
|
|
||||||
state_class=STATE_CLASS_MEASUREMENT,
|
|
||||||
),
|
|
||||||
cv.Optional("active_power_plus"): sensor.sensor_schema(
|
|
||||||
unit_of_measurement=UNIT_WATT,
|
|
||||||
accuracy_decimals=0,
|
|
||||||
device_class=DEVICE_CLASS_POWER,
|
|
||||||
state_class=STATE_CLASS_MEASUREMENT,
|
|
||||||
),
|
|
||||||
cv.Optional("active_power_minus"): sensor.sensor_schema(
|
|
||||||
unit_of_measurement=UNIT_WATT,
|
|
||||||
accuracy_decimals=0,
|
|
||||||
device_class=DEVICE_CLASS_POWER,
|
|
||||||
state_class=STATE_CLASS_MEASUREMENT,
|
|
||||||
),
|
|
||||||
cv.Optional("active_energy_plus"): sensor.sensor_schema(
|
|
||||||
unit_of_measurement=UNIT_WATT_HOURS,
|
|
||||||
accuracy_decimals=0,
|
|
||||||
device_class=DEVICE_CLASS_ENERGY,
|
|
||||||
state_class=STATE_CLASS_TOTAL_INCREASING,
|
|
||||||
),
|
|
||||||
cv.Optional("active_energy_minus"): sensor.sensor_schema(
|
|
||||||
unit_of_measurement=UNIT_WATT_HOURS,
|
|
||||||
accuracy_decimals=0,
|
|
||||||
device_class=DEVICE_CLASS_ENERGY,
|
|
||||||
state_class=STATE_CLASS_TOTAL_INCREASING,
|
|
||||||
),
|
|
||||||
cv.Optional("reactive_energy_plus"): sensor.sensor_schema(
|
|
||||||
unit_of_measurement=UNIT_WATT_HOURS,
|
|
||||||
accuracy_decimals=0,
|
|
||||||
device_class=DEVICE_CLASS_ENERGY,
|
|
||||||
state_class=STATE_CLASS_TOTAL_INCREASING,
|
|
||||||
),
|
|
||||||
cv.Optional("reactive_energy_minus"): sensor.sensor_schema(
|
|
||||||
unit_of_measurement=UNIT_WATT_HOURS,
|
|
||||||
accuracy_decimals=0,
|
|
||||||
device_class=DEVICE_CLASS_ENERGY,
|
|
||||||
state_class=STATE_CLASS_TOTAL_INCREASING,
|
|
||||||
),
|
|
||||||
# Netz NOE
|
|
||||||
cv.Optional("power_factor"): sensor.sensor_schema(
|
|
||||||
accuracy_decimals=3,
|
|
||||||
device_class=DEVICE_CLASS_POWER_FACTOR,
|
|
||||||
state_class=STATE_CLASS_MEASUREMENT,
|
|
||||||
),
|
|
||||||
}
|
}
|
||||||
).extend(cv.COMPONENT_SCHEMA)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def deprecation_warning(config):
|
||||||
|
_LOGGER.warning(
|
||||||
|
"The dlms_meter sensor schema using predefined keys (e.g., 'voltage_l1') is deprecated and will be removed in 2026.11.0. "
|
||||||
|
"Please update your configuration to use the new schema with 'obis_code'."
|
||||||
|
)
|
||||||
|
return config
|
||||||
|
|
||||||
|
|
||||||
|
OLD_SCHEMA = cv.All(
|
||||||
|
cv.Schema(
|
||||||
|
{
|
||||||
|
cv.GenerateID(CONF_DLMS_METER_ID): cv.use_id(DlmsMeterComponent),
|
||||||
|
cv.Optional("voltage_l1"): sensor.sensor_schema(
|
||||||
|
unit_of_measurement=UNIT_VOLT,
|
||||||
|
accuracy_decimals=1,
|
||||||
|
device_class=DEVICE_CLASS_VOLTAGE,
|
||||||
|
state_class=STATE_CLASS_MEASUREMENT,
|
||||||
|
),
|
||||||
|
cv.Optional("voltage_l2"): sensor.sensor_schema(
|
||||||
|
unit_of_measurement=UNIT_VOLT,
|
||||||
|
accuracy_decimals=1,
|
||||||
|
device_class=DEVICE_CLASS_VOLTAGE,
|
||||||
|
state_class=STATE_CLASS_MEASUREMENT,
|
||||||
|
),
|
||||||
|
cv.Optional("voltage_l3"): sensor.sensor_schema(
|
||||||
|
unit_of_measurement=UNIT_VOLT,
|
||||||
|
accuracy_decimals=1,
|
||||||
|
device_class=DEVICE_CLASS_VOLTAGE,
|
||||||
|
state_class=STATE_CLASS_MEASUREMENT,
|
||||||
|
),
|
||||||
|
cv.Optional("current_l1"): sensor.sensor_schema(
|
||||||
|
unit_of_measurement=UNIT_AMPERE,
|
||||||
|
accuracy_decimals=2,
|
||||||
|
device_class=DEVICE_CLASS_CURRENT,
|
||||||
|
state_class=STATE_CLASS_MEASUREMENT,
|
||||||
|
),
|
||||||
|
cv.Optional("current_l2"): sensor.sensor_schema(
|
||||||
|
unit_of_measurement=UNIT_AMPERE,
|
||||||
|
accuracy_decimals=2,
|
||||||
|
device_class=DEVICE_CLASS_CURRENT,
|
||||||
|
state_class=STATE_CLASS_MEASUREMENT,
|
||||||
|
),
|
||||||
|
cv.Optional("current_l3"): sensor.sensor_schema(
|
||||||
|
unit_of_measurement=UNIT_AMPERE,
|
||||||
|
accuracy_decimals=2,
|
||||||
|
device_class=DEVICE_CLASS_CURRENT,
|
||||||
|
state_class=STATE_CLASS_MEASUREMENT,
|
||||||
|
),
|
||||||
|
cv.Optional("active_power_plus"): sensor.sensor_schema(
|
||||||
|
unit_of_measurement=UNIT_WATT,
|
||||||
|
accuracy_decimals=0,
|
||||||
|
device_class=DEVICE_CLASS_POWER,
|
||||||
|
state_class=STATE_CLASS_MEASUREMENT,
|
||||||
|
),
|
||||||
|
cv.Optional("active_power_minus"): sensor.sensor_schema(
|
||||||
|
unit_of_measurement=UNIT_WATT,
|
||||||
|
accuracy_decimals=0,
|
||||||
|
device_class=DEVICE_CLASS_POWER,
|
||||||
|
state_class=STATE_CLASS_MEASUREMENT,
|
||||||
|
),
|
||||||
|
cv.Optional("active_energy_plus"): sensor.sensor_schema(
|
||||||
|
unit_of_measurement=UNIT_WATT_HOURS,
|
||||||
|
accuracy_decimals=0,
|
||||||
|
device_class=DEVICE_CLASS_ENERGY,
|
||||||
|
state_class=STATE_CLASS_TOTAL_INCREASING,
|
||||||
|
),
|
||||||
|
cv.Optional("active_energy_minus"): sensor.sensor_schema(
|
||||||
|
unit_of_measurement=UNIT_WATT_HOURS,
|
||||||
|
accuracy_decimals=0,
|
||||||
|
device_class=DEVICE_CLASS_ENERGY,
|
||||||
|
state_class=STATE_CLASS_TOTAL_INCREASING,
|
||||||
|
),
|
||||||
|
cv.Optional("reactive_energy_plus"): sensor.sensor_schema(
|
||||||
|
unit_of_measurement=UNIT_WATT_HOURS,
|
||||||
|
accuracy_decimals=0,
|
||||||
|
device_class=DEVICE_CLASS_ENERGY,
|
||||||
|
state_class=STATE_CLASS_TOTAL_INCREASING,
|
||||||
|
),
|
||||||
|
cv.Optional("reactive_energy_minus"): sensor.sensor_schema(
|
||||||
|
unit_of_measurement=UNIT_WATT_HOURS,
|
||||||
|
accuracy_decimals=0,
|
||||||
|
device_class=DEVICE_CLASS_ENERGY,
|
||||||
|
state_class=STATE_CLASS_TOTAL_INCREASING,
|
||||||
|
),
|
||||||
|
cv.Optional("power_factor"): sensor.sensor_schema(
|
||||||
|
accuracy_decimals=3,
|
||||||
|
device_class=DEVICE_CLASS_POWER_FACTOR,
|
||||||
|
state_class=STATE_CLASS_MEASUREMENT,
|
||||||
|
),
|
||||||
|
}
|
||||||
|
).extend(cv.COMPONENT_SCHEMA),
|
||||||
|
deprecation_warning,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
CONFIG_SCHEMA = cv.Any(DYNAMIC_SCHEMA, OLD_SCHEMA)
|
||||||
|
|
||||||
|
|
||||||
async def to_code(config):
|
async def to_code(config):
|
||||||
hub = await cg.get_variable(config[CONF_DLMS_METER_ID])
|
hub = await cg.get_variable(config[CONF_DLMS_METER_ID])
|
||||||
|
|
||||||
sensors = []
|
if obis := config.get(CONF_OBIS_CODE):
|
||||||
for key, conf in config.items():
|
var = await sensor.new_sensor(config)
|
||||||
if not isinstance(conf, dict):
|
cg.add(hub.register_sensor(obis, var))
|
||||||
continue
|
else:
|
||||||
id = conf[CONF_ID]
|
for key, obis_val in NUMERIC_KEYS.items():
|
||||||
if id and id.type == sensor.Sensor:
|
if sensor_config := config.get(key):
|
||||||
sens = await sensor.new_sensor(conf)
|
sens = await sensor.new_sensor(sensor_config)
|
||||||
cg.add(getattr(hub, f"set_{key}_sensor")(sens))
|
cg.add(hub.register_sensor(obis_val, sens))
|
||||||
sensors.append(f"F({key})")
|
|
||||||
|
|
||||||
if sensors:
|
|
||||||
cg.add_define(
|
|
||||||
"DLMS_METER_SENSOR_LIST(F, sep)", cg.RawExpression(" sep ".join(sensors))
|
|
||||||
)
|
|
||||||
|
|||||||
@@ -1,37 +1,59 @@
|
|||||||
|
import logging
|
||||||
|
|
||||||
import esphome.codegen as cg
|
import esphome.codegen as cg
|
||||||
from esphome.components import text_sensor
|
from esphome.components import text_sensor
|
||||||
import esphome.config_validation as cv
|
import esphome.config_validation as cv
|
||||||
from esphome.const import CONF_ID
|
|
||||||
|
|
||||||
from .. import CONF_DLMS_METER_ID, DlmsMeterComponent
|
from .. import CONF_DLMS_METER_ID, CONF_OBIS_CODE, DlmsMeterComponent, obis_code
|
||||||
|
|
||||||
AUTO_LOAD = ["dlms_meter"]
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
CONFIG_SCHEMA = cv.Schema(
|
DEPENDENCIES = ["dlms_meter"]
|
||||||
|
|
||||||
|
TEXT_KEYS = {
|
||||||
|
"timestamp": "0.0.1.0.0.255",
|
||||||
|
"meternumber": "0.0.96.1.0.255",
|
||||||
|
}
|
||||||
|
|
||||||
|
DYNAMIC_SCHEMA = text_sensor.text_sensor_schema().extend(
|
||||||
{
|
{
|
||||||
cv.GenerateID(CONF_DLMS_METER_ID): cv.use_id(DlmsMeterComponent),
|
cv.GenerateID(CONF_DLMS_METER_ID): cv.use_id(DlmsMeterComponent),
|
||||||
cv.Optional("timestamp"): text_sensor.text_sensor_schema(),
|
cv.Required(CONF_OBIS_CODE): obis_code,
|
||||||
# Netz NOE
|
|
||||||
cv.Optional("meternumber"): text_sensor.text_sensor_schema(),
|
|
||||||
}
|
}
|
||||||
).extend(cv.COMPONENT_SCHEMA)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def deprecation_warning(config):
|
||||||
|
_LOGGER.warning(
|
||||||
|
"The dlms_meter text_sensor schema using predefined keys (e.g., 'timestamp') is deprecated and will be removed in 2026.11.0. "
|
||||||
|
"Please update your configuration to use the new schema with 'obis_code'."
|
||||||
|
)
|
||||||
|
return config
|
||||||
|
|
||||||
|
|
||||||
|
OLD_SCHEMA = cv.All(
|
||||||
|
cv.Schema(
|
||||||
|
{
|
||||||
|
cv.GenerateID(CONF_DLMS_METER_ID): cv.use_id(DlmsMeterComponent),
|
||||||
|
cv.Optional("timestamp"): text_sensor.text_sensor_schema(),
|
||||||
|
cv.Optional("meternumber"): text_sensor.text_sensor_schema(),
|
||||||
|
}
|
||||||
|
).extend(cv.COMPONENT_SCHEMA),
|
||||||
|
deprecation_warning,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
CONFIG_SCHEMA = cv.Any(DYNAMIC_SCHEMA, OLD_SCHEMA)
|
||||||
|
|
||||||
|
|
||||||
async def to_code(config):
|
async def to_code(config):
|
||||||
hub = await cg.get_variable(config[CONF_DLMS_METER_ID])
|
hub = await cg.get_variable(config[CONF_DLMS_METER_ID])
|
||||||
|
|
||||||
text_sensors = []
|
if obis := config.get(CONF_OBIS_CODE):
|
||||||
for key, conf in config.items():
|
var = await text_sensor.new_text_sensor(config)
|
||||||
if not isinstance(conf, dict):
|
cg.add(hub.register_text_sensor(obis, var))
|
||||||
continue
|
else:
|
||||||
id = conf[CONF_ID]
|
for key, obis_val in TEXT_KEYS.items():
|
||||||
if id and id.type == text_sensor.TextSensor:
|
if text_sensor_config := config.get(key):
|
||||||
sens = await text_sensor.new_text_sensor(conf)
|
sens = await text_sensor.new_text_sensor(text_sensor_config)
|
||||||
cg.add(getattr(hub, f"set_{key}_text_sensor")(sens))
|
cg.add(hub.register_text_sensor(obis_val, sens))
|
||||||
text_sensors.append(f"F({key})")
|
|
||||||
|
|
||||||
if text_sensors:
|
|
||||||
cg.add_define(
|
|
||||||
"DLMS_METER_TEXT_SENSOR_LIST(F, sep)",
|
|
||||||
cg.RawExpression(" sep ".join(text_sensors)),
|
|
||||||
)
|
|
||||||
|
|||||||
@@ -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_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_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.9.0")
|
||||||
|
|
||||||
|
|
||||||
def final_validate(config: ConfigType) -> ConfigType:
|
def final_validate(config: ConfigType) -> ConfigType:
|
||||||
|
|||||||
@@ -153,8 +153,9 @@ void Dsmr::receive_encrypted_telegram_() {
|
|||||||
bool Dsmr::parse_telegram_(const dsmr_parser::DsmrUnencryptedTelegram &telegram) {
|
bool Dsmr::parse_telegram_(const dsmr_parser::DsmrUnencryptedTelegram &telegram) {
|
||||||
this->stop_requesting_data_();
|
this->stop_requesting_data_();
|
||||||
|
|
||||||
ESP_LOGV(TAG, "Trying to parse telegram (%zu bytes)", telegram.content().size());
|
ESP_LOGV(TAG, "Trying to parse telegram (%zu bytes)", telegram.full_content().size());
|
||||||
ESP_LOGVV(TAG, "Telegram content:\n %.*s", static_cast<int>(telegram.content().size()), telegram.content().data());
|
ESP_LOGVV(TAG, "Telegram content:\n %.*s", static_cast<int>(telegram.full_content().size()),
|
||||||
|
telegram.full_content().data());
|
||||||
|
|
||||||
MyData data;
|
MyData data;
|
||||||
if (const bool res = dsmr_parser::DsmrParser::parse(data, telegram); !res) {
|
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
|
// Publish the telegram, after publishing the sensors so it can also trigger action based on latest values
|
||||||
if (this->s_telegram_ != nullptr) {
|
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;
|
return true;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,9 +16,14 @@
|
|||||||
#include <span>
|
#include <span>
|
||||||
#include <vector>
|
#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>)
|
#if __has_include(<psa/crypto.h>)
|
||||||
#include <dsmr_parser/decryption/aes128gcm_tfpsa.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>)
|
#if __has_include(<mbedtls/esp_config.h>)
|
||||||
#include <mbedtls/esp_config.h>
|
#include <mbedtls/esp_config.h>
|
||||||
#endif
|
#endif
|
||||||
@@ -33,7 +38,7 @@ namespace esphome::dsmr {
|
|||||||
|
|
||||||
#if __has_include(<psa/crypto.h>)
|
#if __has_include(<psa/crypto.h>)
|
||||||
using Aes128GcmDecryptorImpl = dsmr_parser::Aes128GcmTfPsa;
|
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;
|
using Aes128GcmDecryptorImpl = dsmr_parser::Aes128GcmMbedTls;
|
||||||
#else
|
#else
|
||||||
using Aes128GcmDecryptorImpl = dsmr_parser::Aes128GcmBearSsl;
|
using Aes128GcmDecryptorImpl = dsmr_parser::Aes128GcmBearSsl;
|
||||||
@@ -69,7 +74,8 @@ class Dsmr : public Component, public uart::UARTDevice {
|
|||||||
receive_timeout_(receive_timeout),
|
receive_timeout_(receive_timeout),
|
||||||
request_pin_(request_pin),
|
request_pin_(request_pin),
|
||||||
buffer_(max_telegram_length),
|
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);
|
this->set_decryption_key_(decryption_key);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -92,7 +98,11 @@ class Dsmr : public Component, public uart::UARTDevice {
|
|||||||
|
|
||||||
// Remove before 2026.8.0
|
// Remove before 2026.8.0
|
||||||
ESPDEPRECATED("Use 'decryption_key' configuration parameter. This method will be removed in 2026.8.0", "2026.2.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
|
// Sensor setters
|
||||||
#define DSMR_SET_SENSOR(s) \
|
#define DSMR_SET_SENSOR(s) \
|
||||||
@@ -138,7 +148,7 @@ class Dsmr : public Component, public uart::UARTDevice {
|
|||||||
std::vector<uint8_t> buffer_;
|
std::vector<uint8_t> buffer_;
|
||||||
dsmr_parser::PacketAccumulator packet_accumulator_;
|
dsmr_parser::PacketAccumulator packet_accumulator_;
|
||||||
Aes128GcmDecryptorImpl gcm_decryptor_;
|
Aes128GcmDecryptorImpl gcm_decryptor_;
|
||||||
dsmr_parser::DlmsPacketDecryptor dlms_decryptor_{gcm_decryptor_};
|
dsmr_parser::DlmsPacketDecryptor dlms_decryptor_;
|
||||||
std::array<uint8_t, 256> uart_chunk_reading_buf_;
|
std::array<uint8_t, 256> uart_chunk_reading_buf_;
|
||||||
};
|
};
|
||||||
} // namespace esphome::dsmr
|
} // namespace esphome::dsmr
|
||||||
|
|||||||
@@ -248,10 +248,6 @@ CONFIG_SCHEMA = cv.Schema(
|
|||||||
device_class=DEVICE_CLASS_POWER,
|
device_class=DEVICE_CLASS_POWER,
|
||||||
state_class=STATE_CLASS_MEASUREMENT,
|
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(
|
cv.Optional("electricity_failures"): sensor.sensor_schema(
|
||||||
accuracy_decimals=0,
|
accuracy_decimals=0,
|
||||||
state_class=STATE_CLASS_MEASUREMENT,
|
state_class=STATE_CLASS_MEASUREMENT,
|
||||||
@@ -808,6 +804,10 @@ CONFIG_SCHEMA = cv.Schema(
|
|||||||
device_class=DEVICE_CLASS_DURATION,
|
device_class=DEVICE_CLASS_DURATION,
|
||||||
state_class=STATE_CLASS_MEASUREMENT,
|
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)
|
).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"): text_sensor.text_sensor_schema(),
|
||||||
cv.Optional("p1_version_be"): 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("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"): text_sensor.text_sensor_schema(),
|
||||||
cv.Optional("electricity_tariff_il"): 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(),
|
cv.Optional("electricity_failure_log"): text_sensor.text_sensor_schema(),
|
||||||
|
|||||||
@@ -52,6 +52,8 @@ class E131Component : public esphome::Component {
|
|||||||
if (!this->udp_.parsePacket())
|
if (!this->udp_.parsePacket())
|
||||||
return -1;
|
return -1;
|
||||||
return this->udp_.read(buf, len);
|
return this->udp_.read(buf, len);
|
||||||
|
#else
|
||||||
|
return -1;
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
bool packet_(const uint8_t *data, size_t len, int &universe, E131Packet &packet);
|
bool packet_(const uint8_t *data, size_t len, int &universe, E131Packet &packet);
|
||||||
|
|||||||
@@ -5,10 +5,15 @@ from esphome import core, pins
|
|||||||
import esphome.codegen as cg
|
import esphome.codegen as cg
|
||||||
from esphome.components import display, spi
|
from esphome.components import display, spi
|
||||||
from esphome.components.display import CONF_SHOW_TEST_CARD, validate_rotation
|
from esphome.components.display import CONF_SHOW_TEST_CARD, validate_rotation
|
||||||
from esphome.components.mipi import flatten_sequence, map_sequence
|
from esphome.components.mipi import (
|
||||||
|
flatten_sequence,
|
||||||
|
map_sequence,
|
||||||
|
model_schema_extractor,
|
||||||
|
)
|
||||||
import esphome.config_validation as cv
|
import esphome.config_validation as cv
|
||||||
from esphome.config_validation import update_interval
|
from esphome.config_validation import update_interval
|
||||||
from esphome.const import (
|
from esphome.const import (
|
||||||
|
CONF_AUTO_CLEAR_ENABLED,
|
||||||
CONF_BUSY_PIN,
|
CONF_BUSY_PIN,
|
||||||
CONF_CS_PIN,
|
CONF_CS_PIN,
|
||||||
CONF_DATA_RATE,
|
CONF_DATA_RATE,
|
||||||
@@ -111,6 +116,7 @@ def model_schema(config):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@model_schema_extractor(MODELS, model_schema)
|
||||||
def customise_schema(config):
|
def customise_schema(config):
|
||||||
"""
|
"""
|
||||||
Create a customised config schema for a specific model and validate the configuration.
|
Create a customised config schema for a specific model and validate the configuration.
|
||||||
@@ -124,7 +130,23 @@ def customise_schema(config):
|
|||||||
},
|
},
|
||||||
extra=cv.ALLOW_EXTRA,
|
extra=cv.ALLOW_EXTRA,
|
||||||
)(config)
|
)(config)
|
||||||
return model_schema(config)(config)
|
model = MODELS[config[CONF_MODEL]]
|
||||||
|
config = model_schema(config)(config)
|
||||||
|
width, height = model.get_dimensions(config)
|
||||||
|
display.add_metadata(
|
||||||
|
config[CONF_ID],
|
||||||
|
width,
|
||||||
|
height,
|
||||||
|
has_hardware_rotation=True,
|
||||||
|
byte_order=cv.UNDEFINED,
|
||||||
|
has_writer=config.get(CONF_AUTO_CLEAR_ENABLED) is True
|
||||||
|
or config.get(CONF_PAGES) is not None
|
||||||
|
or config.get(CONF_LAMBDA) is not None
|
||||||
|
or config.get(CONF_SHOW_TEST_CARD) is True,
|
||||||
|
rotation=config.get(CONF_ROTATION, 0),
|
||||||
|
draw_rounding=0,
|
||||||
|
)
|
||||||
|
return config
|
||||||
|
|
||||||
|
|
||||||
CONFIG_SCHEMA = customise_schema
|
CONFIG_SCHEMA = customise_schema
|
||||||
@@ -192,6 +214,9 @@ async def to_code(config):
|
|||||||
if busy_pin := config.get(CONF_BUSY_PIN):
|
if busy_pin := config.get(CONF_BUSY_PIN):
|
||||||
busy = await cg.gpio_pin_expression(busy_pin)
|
busy = await cg.gpio_pin_expression(busy_pin)
|
||||||
cg.add(var.set_busy_pin(busy))
|
cg.add(var.set_busy_pin(busy))
|
||||||
|
if enable_pin := config.get(CONF_ENABLE_PIN):
|
||||||
|
enable = [await cg.gpio_pin_expression(pin) for pin in enable_pin]
|
||||||
|
cg.add(var.set_enable_pins(enable))
|
||||||
cg.add(var.set_full_update_every(config[CONF_FULL_UPDATE_EVERY]))
|
cg.add(var.set_full_update_every(config[CONF_FULL_UPDATE_EVERY]))
|
||||||
if CONF_RESET_DURATION in config:
|
if CONF_RESET_DURATION in config:
|
||||||
cg.add(var.set_reset_duration(config[CONF_RESET_DURATION]))
|
cg.add(var.set_reset_duration(config[CONF_RESET_DURATION]))
|
||||||
|
|||||||
@@ -38,6 +38,10 @@ bool EPaperBase::init_buffer_(size_t buffer_length) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void EPaperBase::setup_pins_() const {
|
void EPaperBase::setup_pins_() const {
|
||||||
|
for (auto *pin : this->enable_pins_) {
|
||||||
|
pin->setup();
|
||||||
|
pin->digital_write(true);
|
||||||
|
}
|
||||||
this->dc_pin_->setup(); // OUTPUT
|
this->dc_pin_->setup(); // OUTPUT
|
||||||
this->dc_pin_->digital_write(false);
|
this->dc_pin_->digital_write(false);
|
||||||
|
|
||||||
|
|||||||
@@ -50,6 +50,7 @@ class EPaperBase : public Display,
|
|||||||
float get_setup_priority() const override;
|
float get_setup_priority() const override;
|
||||||
void set_reset_pin(GPIOPin *reset) { this->reset_pin_ = reset; }
|
void set_reset_pin(GPIOPin *reset) { this->reset_pin_ = reset; }
|
||||||
void set_busy_pin(GPIOPin *busy) { this->busy_pin_ = busy; }
|
void set_busy_pin(GPIOPin *busy) { this->busy_pin_ = busy; }
|
||||||
|
void set_enable_pins(std::vector<GPIOPin *> enable_pins) { this->enable_pins_ = std::move(enable_pins); }
|
||||||
void set_reset_duration(uint32_t reset_duration) { this->reset_duration_ = reset_duration; }
|
void set_reset_duration(uint32_t reset_duration) { this->reset_duration_ = reset_duration; }
|
||||||
void set_transform(uint8_t transform) {
|
void set_transform(uint8_t transform) {
|
||||||
this->transform_ = transform;
|
this->transform_ = transform;
|
||||||
@@ -177,6 +178,7 @@ class EPaperBase : public Display,
|
|||||||
GPIOPin *dc_pin_{};
|
GPIOPin *dc_pin_{};
|
||||||
GPIOPin *busy_pin_{};
|
GPIOPin *busy_pin_{};
|
||||||
GPIOPin *reset_pin_{};
|
GPIOPin *reset_pin_{};
|
||||||
|
std::vector<GPIOPin *> enable_pins_{};
|
||||||
bool waiting_for_idle_{};
|
bool waiting_for_idle_{};
|
||||||
uint32_t delay_until_{}; // timestamp until which to delay processing
|
uint32_t delay_until_{}; // timestamp until which to delay processing
|
||||||
uint16_t next_delay_{}; // milliseconds to delay before next state
|
uint16_t next_delay_{}; // milliseconds to delay before next state
|
||||||
|
|||||||
@@ -10,11 +10,11 @@ class SSD1677(EpaperModel):
|
|||||||
|
|
||||||
# fmt: off
|
# fmt: off
|
||||||
def get_init_sequence(self, config: dict):
|
def get_init_sequence(self, config: dict):
|
||||||
width, _height = self.get_dimensions(config)
|
_width, height = self.get_dimensions(config)
|
||||||
return (
|
return (
|
||||||
(0x18, 0x80), # Select internal Temp sensor
|
(0x18, 0x80), # Select internal Temp sensor
|
||||||
(0x0C, 0xAE, 0xC7, 0xC3, 0xC0, 0x80), # inrush current level 2
|
(0x0C, 0xAE, 0xC7, 0xC3, 0xC0, 0x80), # inrush current level 2
|
||||||
(0x01, (width - 1) % 256, (width - 1) // 256, 0x02), # Set column gate limit
|
(0x01, (height - 1) % 256, (height - 1) // 256, 0x02), # Set gate limit (number of rows-1)
|
||||||
(0x3C, 0x01), # Set border waveform
|
(0x3C, 0x01), # Set border waveform
|
||||||
(0x11, 3), # Set transform
|
(0x11, 3), # Set transform
|
||||||
)
|
)
|
||||||
@@ -51,3 +51,16 @@ ssd1677.extend(
|
|||||||
height=480,
|
height=480,
|
||||||
mirror_x=True,
|
mirror_x=True,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
ssd1677.extend(
|
||||||
|
"seeed-reterminal-sticky",
|
||||||
|
width=800,
|
||||||
|
height=480,
|
||||||
|
mirror_x=True,
|
||||||
|
enable_pin=47,
|
||||||
|
cs_pin=15,
|
||||||
|
dc_pin=16,
|
||||||
|
reset_pin=17,
|
||||||
|
busy_pin=18,
|
||||||
|
data_rate="10MHz",
|
||||||
|
)
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
from collections.abc import Callable, Iterable
|
||||||
import contextlib
|
import contextlib
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
import itertools
|
import itertools
|
||||||
@@ -6,6 +7,7 @@ import os
|
|||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
import re
|
import re
|
||||||
import subprocess
|
import subprocess
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
from esphome import yaml_util
|
from esphome import yaml_util
|
||||||
import esphome.codegen as cg
|
import esphome.codegen as cg
|
||||||
@@ -46,17 +48,18 @@ from esphome.const import (
|
|||||||
Toolchain,
|
Toolchain,
|
||||||
__version__,
|
__version__,
|
||||||
)
|
)
|
||||||
from esphome.core import CORE, HexInt, Library
|
from esphome.core import CORE, EsphomeError, HexInt
|
||||||
from esphome.core.config import BOARD_MAX_LENGTH
|
from esphome.core.config import BOARD_MAX_LENGTH
|
||||||
from esphome.coroutine import CoroPriority, coroutine_with_priority
|
from esphome.coroutine import CoroPriority, coroutine_with_priority
|
||||||
from esphome.espidf.component import generate_idf_component
|
from esphome.espidf.component import generate_idf_components
|
||||||
import esphome.final_validate as fv
|
import esphome.final_validate as fv
|
||||||
from esphome.helpers import copy_file_if_changed, rmtree, write_file_if_changed
|
from esphome.helpers import copy_file_if_changed, rmtree, write_file_if_changed
|
||||||
|
from esphome.schema_extractors import SCHEMA_EXTRACT, schema_extractor
|
||||||
from esphome.types import ConfigType
|
from esphome.types import ConfigType
|
||||||
from esphome.writer import clean_build, clean_cmake_cache
|
from esphome.writer import clean_build, clean_cmake_cache
|
||||||
|
|
||||||
from .boards import BOARDS, STANDARD_BOARDS
|
from .boards import BOARDS, STANDARD_BOARDS
|
||||||
from .const import ( # noqa
|
from .const import (
|
||||||
KEY_ARDUINO_LIBRARIES,
|
KEY_ARDUINO_LIBRARIES,
|
||||||
KEY_BOARD,
|
KEY_BOARD,
|
||||||
KEY_COMPONENTS,
|
KEY_COMPONENTS,
|
||||||
@@ -78,15 +81,18 @@ from .const import ( # noqa
|
|||||||
VARIANT_ESP32C6,
|
VARIANT_ESP32C6,
|
||||||
VARIANT_ESP32C61,
|
VARIANT_ESP32C61,
|
||||||
VARIANT_ESP32H2,
|
VARIANT_ESP32H2,
|
||||||
|
VARIANT_ESP32H4,
|
||||||
|
VARIANT_ESP32H21,
|
||||||
VARIANT_ESP32P4,
|
VARIANT_ESP32P4,
|
||||||
VARIANT_ESP32S2,
|
VARIANT_ESP32S2,
|
||||||
VARIANT_ESP32S3,
|
VARIANT_ESP32S3,
|
||||||
|
VARIANT_ESP32S31,
|
||||||
VARIANT_FRIENDLY,
|
VARIANT_FRIENDLY,
|
||||||
VARIANTS,
|
VARIANTS,
|
||||||
)
|
)
|
||||||
|
|
||||||
# force import gpio to register pin schema
|
# 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__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
AUTO_LOAD = ["preferences"]
|
AUTO_LOAD = ["preferences"]
|
||||||
@@ -403,9 +409,12 @@ CPU_FREQUENCIES = {
|
|||||||
VARIANT_ESP32C6: get_cpu_frequencies(80, 120, 160),
|
VARIANT_ESP32C6: get_cpu_frequencies(80, 120, 160),
|
||||||
VARIANT_ESP32C61: 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_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_ESP32P4: get_cpu_frequencies(40, 360, 400),
|
||||||
VARIANT_ESP32S2: get_cpu_frequencies(80, 160, 240),
|
VARIANT_ESP32S2: get_cpu_frequencies(80, 160, 240),
|
||||||
VARIANT_ESP32S3: 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.
|
# Make sure not missed here if a new variant added.
|
||||||
@@ -464,21 +473,20 @@ def set_core_data(config):
|
|||||||
framework_ver = cv.Version.parse(config[CONF_FRAMEWORK][CONF_VERSION])
|
framework_ver = cv.Version.parse(config[CONF_FRAMEWORK][CONF_VERSION])
|
||||||
CORE.data[KEY_CORE][KEY_FRAMEWORK_VERSION] = framework_ver
|
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:
|
if conf[CONF_TYPE] == FRAMEWORK_ESP_IDF:
|
||||||
CORE.data[KEY_ESP32][KEY_IDF_VERSION] = framework_ver
|
idf_ver = framework_ver
|
||||||
elif (idf_ver := ARDUINO_IDF_VERSION_LOOKUP.get(framework_ver)) is not None:
|
elif (idf_ver := ARDUINO_IDF_VERSION_LOOKUP.get(framework_ver)) is 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:
|
|
||||||
raise cv.Invalid(
|
raise cv.Invalid(
|
||||||
f"Arduino version {framework_ver} has no known ESP-IDF version mapping. "
|
f"Arduino version {framework_ver} has no known ESP-IDF version mapping. "
|
||||||
"Please update ARDUINO_IDF_VERSION_LOOKUP.",
|
"Please update ARDUINO_IDF_VERSION_LOOKUP.",
|
||||||
path=[CONF_FRAMEWORK, CONF_VERSION],
|
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_BOARD] = config[CONF_BOARD]
|
||||||
CORE.data[KEY_ESP32][KEY_FLASH_SIZE] = config[CONF_FLASH_SIZE]
|
CORE.data[KEY_ESP32][KEY_FLASH_SIZE] = config[CONF_FLASH_SIZE]
|
||||||
CORE.data[KEY_ESP32][KEY_VARIANT] = variant
|
CORE.data[KEY_ESP32][KEY_VARIANT] = variant
|
||||||
@@ -491,6 +499,32 @@ def get_esp32_variant(core_obj=None):
|
|||||||
return (core_obj or CORE).data[KEY_ESP32][KEY_VARIANT]
|
return (core_obj or CORE).data[KEY_ESP32][KEY_VARIANT]
|
||||||
|
|
||||||
|
|
||||||
|
def variant_filtered_enum(
|
||||||
|
by_variant: dict[str, Iterable[Any]], **kwargs: Any
|
||||||
|
) -> Callable[[Any], Any]:
|
||||||
|
"""Build a ``one_of`` validator whose valid set depends on the active variant.
|
||||||
|
|
||||||
|
``by_variant`` maps each ESP32 variant constant to the iterable of values that
|
||||||
|
are valid on that variant. At validation time the value is checked against the
|
||||||
|
set allowed for the current target variant. For schema extraction the inverted
|
||||||
|
``{value: [variants, ...]}`` map is returned instead, so the language-schema
|
||||||
|
dump can tag every option with the variants that accept it and frontends can
|
||||||
|
filter to the user's selected variant.
|
||||||
|
"""
|
||||||
|
by_value: dict[str, list[str]] = {}
|
||||||
|
for variant, values in by_variant.items():
|
||||||
|
for value in values:
|
||||||
|
by_value.setdefault(str(value), []).append(variant)
|
||||||
|
|
||||||
|
@schema_extractor("variant_enum")
|
||||||
|
def validator(value: Any) -> Any:
|
||||||
|
if value is SCHEMA_EXTRACT:
|
||||||
|
return by_value
|
||||||
|
return cv.one_of(*by_variant.get(get_esp32_variant(), ()), **kwargs)(value)
|
||||||
|
|
||||||
|
return validator
|
||||||
|
|
||||||
|
|
||||||
def get_board(core_obj=None):
|
def get_board(core_obj=None):
|
||||||
return (core_obj or CORE).data[KEY_ESP32][KEY_BOARD]
|
return (core_obj or CORE).data[KEY_ESP32][KEY_BOARD]
|
||||||
|
|
||||||
@@ -710,11 +744,15 @@ def _is_framework_url(source: str) -> bool:
|
|||||||
# The default/recommended arduino framework version
|
# The default/recommended arduino framework version
|
||||||
# - https://github.com/espressif/arduino-esp32/releases
|
# - https://github.com/espressif/arduino-esp32/releases
|
||||||
ARDUINO_FRAMEWORK_VERSION_LOOKUP = {
|
ARDUINO_FRAMEWORK_VERSION_LOOKUP = {
|
||||||
"recommended": cv.Version(3, 3, 8),
|
"recommended": cv.Version(3, 3, 9),
|
||||||
"latest": cv.Version(3, 3, 8),
|
"latest": cv.Version(3, 3, 9),
|
||||||
"dev": cv.Version(3, 3, 8),
|
"dev": cv.Version(3, 3, 9),
|
||||||
}
|
}
|
||||||
ARDUINO_PLATFORM_VERSION_LOOKUP = {
|
ARDUINO_PLATFORM_VERSION_LOOKUP = {
|
||||||
|
cv.Version(
|
||||||
|
4, 0, 0, "alpha1"
|
||||||
|
): "https://github.com/pioarduino/platform-espressif32.git#prep_IDF6",
|
||||||
|
cv.Version(3, 3, 9): cv.Version(55, 3, 39),
|
||||||
cv.Version(3, 3, 8): cv.Version(55, 3, 38, "1"),
|
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, 7): cv.Version(55, 3, 37),
|
||||||
cv.Version(3, 3, 6): cv.Version(55, 3, 36),
|
cv.Version(3, 3, 6): cv.Version(55, 3, 36),
|
||||||
@@ -735,6 +773,8 @@ ARDUINO_PLATFORM_VERSION_LOOKUP = {
|
|||||||
# These versions correspond to pioarduino/esp-idf releases
|
# These versions correspond to pioarduino/esp-idf releases
|
||||||
# See: https://github.com/pioarduino/esp-idf/releases
|
# See: https://github.com/pioarduino/esp-idf/releases
|
||||||
ARDUINO_IDF_VERSION_LOOKUP = {
|
ARDUINO_IDF_VERSION_LOOKUP = {
|
||||||
|
cv.Version(4, 0, 0, "alpha1"): cv.Version(6, 0, 1),
|
||||||
|
cv.Version(3, 3, 9): cv.Version(5, 5, 4),
|
||||||
cv.Version(3, 3, 8): cv.Version(5, 5, 4),
|
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, 7): cv.Version(5, 5, 3, "1"),
|
||||||
cv.Version(3, 3, 6): cv.Version(5, 5, 2),
|
cv.Version(3, 3, 6): cv.Version(5, 5, 2),
|
||||||
@@ -767,7 +807,7 @@ ESP_IDF_PLATFORM_VERSION_LOOKUP = {
|
|||||||
cv.Version(
|
cv.Version(
|
||||||
6, 0, 0
|
6, 0, 0
|
||||||
): "https://github.com/pioarduino/platform-espressif32.git#prep_IDF6",
|
): "https://github.com/pioarduino/platform-espressif32.git#prep_IDF6",
|
||||||
cv.Version(5, 5, 4): cv.Version(55, 3, 38, "1"),
|
cv.Version(5, 5, 4): cv.Version(55, 3, 39),
|
||||||
cv.Version(5, 5, 3, "1"): cv.Version(55, 3, 37),
|
cv.Version(5, 5, 3, "1"): cv.Version(55, 3, 37),
|
||||||
cv.Version(5, 5, 3): cv.Version(55, 3, 37),
|
cv.Version(5, 5, 3): cv.Version(55, 3, 37),
|
||||||
cv.Version(5, 5, 2): cv.Version(55, 3, 37),
|
cv.Version(5, 5, 2): cv.Version(55, 3, 37),
|
||||||
@@ -787,8 +827,8 @@ ESP_IDF_PLATFORM_VERSION_LOOKUP = {
|
|||||||
# The platform-espressif32 version
|
# The platform-espressif32 version
|
||||||
# - https://github.com/pioarduino/platform-espressif32/releases
|
# - https://github.com/pioarduino/platform-espressif32/releases
|
||||||
PLATFORM_VERSION_LOOKUP = {
|
PLATFORM_VERSION_LOOKUP = {
|
||||||
"recommended": cv.Version(55, 3, 38, "1"),
|
"recommended": cv.Version(55, 3, 39),
|
||||||
"latest": cv.Version(55, 3, 38, "1"),
|
"latest": cv.Version(55, 3, 39),
|
||||||
"dev": "https://github.com/pioarduino/platform-espressif32.git#develop",
|
"dev": "https://github.com/pioarduino/platform-espressif32.git#develop",
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -829,6 +869,16 @@ def _resolve_framework_version(value: ConfigType) -> cv.Version:
|
|||||||
return 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:
|
def _check_pio_versions(config: ConfigType) -> ConfigType:
|
||||||
config = config.copy()
|
config = config.copy()
|
||||||
value = config[CONF_FRAMEWORK]
|
value = config[CONF_FRAMEWORK]
|
||||||
@@ -897,8 +947,10 @@ def _check_esp_idf_versions(config: ConfigType) -> ConfigType:
|
|||||||
"If there are connectivity or build issues please remove the manual source."
|
"If there are connectivity or build issues please remove the manual source."
|
||||||
)
|
)
|
||||||
|
|
||||||
# Official ESP-IDF frameworks don't use the 'extra' semver component.
|
# esp-idf framework only: drop pioarduino's packaging revision (config + download).
|
||||||
value[CONF_VERSION] = str(cv.Version(version.major, version.minor, version.patch))
|
# 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
|
return config
|
||||||
|
|
||||||
@@ -907,11 +959,16 @@ def _validate_toolchain(value) -> Toolchain:
|
|||||||
return Toolchain(cv.one_of(*(t.value for t in Toolchain), lower=True)(value))
|
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.
|
# 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:
|
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:
|
if CORE.using_toolchain_esp_idf:
|
||||||
return _check_esp_idf_versions(config)
|
return _check_esp_idf_versions(config)
|
||||||
return _check_pio_versions(config)
|
return _check_pio_versions(config)
|
||||||
@@ -933,7 +990,21 @@ def _detect_variant(value):
|
|||||||
variant = value.get(CONF_VARIANT)
|
variant = value.get(CONF_VARIANT)
|
||||||
if variant and board is None:
|
if variant and board is None:
|
||||||
# If variant is set, we can derive the board from it
|
# 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 = value.copy()
|
||||||
value[CONF_BOARD] = STANDARD_BOARDS[variant]
|
value[CONF_BOARD] = STANDARD_BOARDS[variant]
|
||||||
if variant == VARIANT_ESP32P4:
|
if variant == VARIANT_ESP32P4:
|
||||||
@@ -1220,6 +1291,7 @@ KEY_MBEDTLS_PKCS7_REQUIRED = "mbedtls_pkcs7_required"
|
|||||||
KEY_FATFS_REQUIRED = "fatfs_required"
|
KEY_FATFS_REQUIRED = "fatfs_required"
|
||||||
KEY_MBEDTLS_SHA512_REQUIRED = "mbedtls_sha512_required"
|
KEY_MBEDTLS_SHA512_REQUIRED = "mbedtls_sha512_required"
|
||||||
KEY_ADC_ONESHOT_IRAM_REQUIRED = "adc_oneshot_iram_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:
|
def require_vfs_select() -> None:
|
||||||
@@ -1328,6 +1400,18 @@ def require_adc_oneshot_iram() -> None:
|
|||||||
CORE.data[KEY_ESP32][KEY_ADC_ONESHOT_IRAM_REQUIRED] = True
|
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. zigbee). No-op on
|
||||||
|
IDF < 6.0.0.
|
||||||
|
"""
|
||||||
|
if idf_version() < cv.Version(6, 0, 0):
|
||||||
|
return
|
||||||
|
CORE.data[KEY_ESP32][KEY_LIBC_PICOLIBC_NEWLIB_COMPAT_REQUIRED] = True
|
||||||
|
|
||||||
|
|
||||||
def _parse_idf_component(value: str) -> ConfigType:
|
def _parse_idf_component(value: str) -> ConfigType:
|
||||||
"""Parse IDF component shorthand syntax like 'owner/component^version'"""
|
"""Parse IDF component shorthand syntax like 'owner/component^version'"""
|
||||||
# Match operator followed by version-like string (digit or *)
|
# Match operator followed by version-like string (digit or *)
|
||||||
@@ -1560,8 +1644,14 @@ FLASH_SIZES = [
|
|||||||
]
|
]
|
||||||
|
|
||||||
CONF_FLASH_SIZE = "flash_size"
|
CONF_FLASH_SIZE = "flash_size"
|
||||||
|
CONF_FLASH_MODE = "flash_mode"
|
||||||
|
CONF_FLASH_FREQUENCY = "flash_frequency"
|
||||||
CONF_CPU_FREQUENCY = "cpu_frequency"
|
CONF_CPU_FREQUENCY = "cpu_frequency"
|
||||||
CONF_PARTITIONS = "partitions"
|
CONF_PARTITIONS = "partitions"
|
||||||
|
FLASH_MODES = ["qio", "qout", "dio", "dout", "opi"]
|
||||||
|
FLASH_FREQUENCIES = [
|
||||||
|
f"{freq}MHZ" for freq in (120, 80, 64, 60, 48, 40, 32, 30, 26, 24, 20, 16)
|
||||||
|
]
|
||||||
CONFIG_SCHEMA = cv.All(
|
CONFIG_SCHEMA = cv.All(
|
||||||
cv.Schema(
|
cv.Schema(
|
||||||
{
|
{
|
||||||
@@ -1575,6 +1665,10 @@ CONFIG_SCHEMA = cv.All(
|
|||||||
cv.Optional(CONF_FLASH_SIZE, default="4MB"): cv.one_of(
|
cv.Optional(CONF_FLASH_SIZE, default="4MB"): cv.one_of(
|
||||||
*FLASH_SIZES, upper=True
|
*FLASH_SIZES, upper=True
|
||||||
),
|
),
|
||||||
|
cv.Optional(CONF_FLASH_MODE): cv.one_of(*FLASH_MODES, lower=True),
|
||||||
|
cv.Optional(CONF_FLASH_FREQUENCY): cv.one_of(
|
||||||
|
*FLASH_FREQUENCIES, upper=True
|
||||||
|
),
|
||||||
cv.Optional(CONF_PARTITIONS): cv.Any(
|
cv.Optional(CONF_PARTITIONS): cv.Any(
|
||||||
cv.file_,
|
cv.file_,
|
||||||
cv.ensure_list(
|
cv.ensure_list(
|
||||||
@@ -1606,6 +1700,7 @@ CONFIG_SCHEMA = cv.All(
|
|||||||
),
|
),
|
||||||
}
|
}
|
||||||
),
|
),
|
||||||
|
_resolve_toolchain,
|
||||||
_detect_variant,
|
_detect_variant,
|
||||||
_set_default_framework,
|
_set_default_framework,
|
||||||
_check_versions,
|
_check_versions,
|
||||||
@@ -1732,6 +1827,26 @@ async def _write_arduino_libraries_sdkconfig() -> None:
|
|||||||
add_idf_sdkconfig_option(f"CONFIG_ARDUINO_SELECTIVE_{lib}", lib in enabled_libs)
|
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)
|
@coroutine_with_priority(CoroPriority.FINAL)
|
||||||
async def _add_yaml_idf_components(components: list[ConfigType]):
|
async def _add_yaml_idf_components(components: list[ConfigType]):
|
||||||
"""Add IDF components from YAML config with final priority to override code-added components."""
|
"""Add IDF components from YAML config with final priority to override code-added components."""
|
||||||
@@ -1790,6 +1905,12 @@ async def to_code(config):
|
|||||||
"board_upload.maximum_size",
|
"board_upload.maximum_size",
|
||||||
int(config[CONF_FLASH_SIZE].removesuffix("MB")) * 1024 * 1024,
|
int(config[CONF_FLASH_SIZE].removesuffix("MB")) * 1024 * 1024,
|
||||||
)
|
)
|
||||||
|
if flash_mode := config.get(CONF_FLASH_MODE):
|
||||||
|
cg.add_platformio_option("board_build.flash_mode", flash_mode)
|
||||||
|
if flash_frequency := config.get(CONF_FLASH_FREQUENCY):
|
||||||
|
cg.add_platformio_option(
|
||||||
|
"board_build.f_flash", f"{flash_frequency[:-3]}000000L"
|
||||||
|
)
|
||||||
|
|
||||||
if CONF_SOURCE in conf:
|
if CONF_SOURCE in conf:
|
||||||
cg.add_platformio_option("platform_packages", [conf[CONF_SOURCE]])
|
cg.add_platformio_option("platform_packages", [conf[CONF_SOURCE]])
|
||||||
@@ -1940,6 +2061,14 @@ async def to_code(config):
|
|||||||
add_idf_sdkconfig_option(
|
add_idf_sdkconfig_option(
|
||||||
f"CONFIG_ESPTOOLPY_FLASHSIZE_{config[CONF_FLASH_SIZE]}", True
|
f"CONFIG_ESPTOOLPY_FLASHSIZE_{config[CONF_FLASH_SIZE]}", True
|
||||||
)
|
)
|
||||||
|
if flash_mode := config.get(CONF_FLASH_MODE):
|
||||||
|
add_idf_sdkconfig_option(
|
||||||
|
f"CONFIG_ESPTOOLPY_FLASHMODE_{flash_mode.upper()}", True
|
||||||
|
)
|
||||||
|
if flash_frequency := config.get(CONF_FLASH_FREQUENCY):
|
||||||
|
add_idf_sdkconfig_option(
|
||||||
|
f"CONFIG_ESPTOOLPY_FLASHFREQ_{flash_frequency[:-3]}M", True
|
||||||
|
)
|
||||||
|
|
||||||
# ESP32-P4: ESP-IDF 5.5.3 changed the default of ESP32P4_SELECTS_REV_LESS_V3
|
# ESP32-P4: ESP-IDF 5.5.3 changed the default of ESP32P4_SELECTS_REV_LESS_V3
|
||||||
# from y to n. PlatformIO uses sections.ld.in (for rev <3) or
|
# from y to n. PlatformIO uses sections.ld.in (for rev <3) or
|
||||||
@@ -2265,17 +2394,8 @@ async def to_code(config):
|
|||||||
add_idf_sdkconfig_option("CONFIG_MBEDTLS_SHA384_C", False)
|
add_idf_sdkconfig_option("CONFIG_MBEDTLS_SHA384_C", False)
|
||||||
add_idf_sdkconfig_option("CONFIG_MBEDTLS_SHA512_C", False)
|
add_idf_sdkconfig_option("CONFIG_MBEDTLS_SHA512_C", False)
|
||||||
|
|
||||||
# Disable PicolibC Newlib compatibility shim on IDF 6.0+
|
# FINAL priority: runs after every require_libc_picolibc_newlib_compat() call
|
||||||
# IDF 6.0 switched from Newlib to PicolibC. The shim provides thread-local
|
CORE.add_job(_set_libc_picolibc_newlib_compat)
|
||||||
# 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)
|
|
||||||
|
|
||||||
# Disable regi2c control functions in IRAM
|
# Disable regi2c control functions in IRAM
|
||||||
# Only needed if using analog peripherals (ADC, DAC, etc.) from ISRs while cache is disabled
|
# Only needed if using analog peripherals (ADC, DAC, etc.) from ISRs while cache is disabled
|
||||||
@@ -2536,13 +2656,6 @@ def _write_sdkconfig():
|
|||||||
clean_build(clear_pio_cache=False)
|
clean_build(clear_pio_cache=False)
|
||||||
|
|
||||||
|
|
||||||
def _platformio_library_to_dependency(library: Library) -> tuple[str, dict[str, str]]:
|
|
||||||
dependency: dict[str, str] = {}
|
|
||||||
name, _version, path = generate_idf_component(library)
|
|
||||||
dependency["override_path"] = str(path)
|
|
||||||
return name, dependency
|
|
||||||
|
|
||||||
|
|
||||||
def _write_idf_component_yml():
|
def _write_idf_component_yml():
|
||||||
yml_path = CORE.relative_build_path("src/idf_component.yml")
|
yml_path = CORE.relative_build_path("src/idf_component.yml")
|
||||||
dependencies: dict[str, dict] = {}
|
dependencies: dict[str, dict] = {}
|
||||||
@@ -2583,6 +2696,26 @@ def _write_idf_component_yml():
|
|||||||
"override_path": str(stub_path),
|
"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
|
# Remove stubs for components that are now required by enabled libraries
|
||||||
for component_name in required_idf_components:
|
for component_name in required_idf_components:
|
||||||
stub_path = stubs_dir / _idf_component_stub_name(component_name)
|
stub_path = stubs_dir / _idf_component_stub_name(component_name)
|
||||||
@@ -2596,13 +2729,21 @@ def _write_idf_component_yml():
|
|||||||
)
|
)
|
||||||
|
|
||||||
if CORE.using_toolchain_esp_idf:
|
if CORE.using_toolchain_esp_idf:
|
||||||
# Try to convert PlatformIO library to ESP-IDF components
|
# Convert the PlatformIO libraries to ESP-IDF components as a batch so
|
||||||
for name, library in CORE.platformio_libraries.items():
|
# PlatformIO resolves the whole dependency tree at once -- deduplicating
|
||||||
|
# shared transitive deps (e.g. esphome/libsodium pulled by both noise-c
|
||||||
|
# and esp_wireguard) to a single version instead of clashing
|
||||||
|
# override_path entries.
|
||||||
|
libraries = [
|
||||||
|
library
|
||||||
|
for name, library in CORE.platformio_libraries.items()
|
||||||
# Don't process arduino libraries
|
# Don't process arduino libraries
|
||||||
if name in ARDUINO_DISABLED_LIBRARIES:
|
if name not in ARDUINO_DISABLED_LIBRARIES
|
||||||
continue
|
]
|
||||||
dependency_name, dependency = _platformio_library_to_dependency(library)
|
for component in generate_idf_components(libraries):
|
||||||
dependencies[dependency_name] = dependency
|
dependencies[component.get_sanitized_name()] = {
|
||||||
|
"override_path": str(component.path)
|
||||||
|
}
|
||||||
|
|
||||||
if CORE.data[KEY_ESP32][KEY_COMPONENTS]:
|
if CORE.data[KEY_ESP32][KEY_COMPONENTS]:
|
||||||
components: dict = CORE.data[KEY_ESP32][KEY_COMPONENTS]
|
components: dict = CORE.data[KEY_ESP32][KEY_COMPONENTS]
|
||||||
@@ -2658,16 +2799,32 @@ def copy_files():
|
|||||||
|
|
||||||
|
|
||||||
def _decode_pc(config, addr):
|
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)
|
try:
|
||||||
if not idedata.addr2line_path or not idedata.firmware_elf_path:
|
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")
|
_LOGGER.debug("decode_pc no addr2line")
|
||||||
return
|
return
|
||||||
command = [idedata.addr2line_path, "-pfiaC", "-e", idedata.firmware_elf_path, addr]
|
command = [str(addr2line_path), "-pfiaC", "-e", str(firmware_elf_path), addr]
|
||||||
try:
|
try:
|
||||||
translation = subprocess.check_output(command, close_fds=False).decode().strip()
|
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)
|
_LOGGER.debug("Caught exception for command %s", command, exc_info=1)
|
||||||
return
|
return
|
||||||
|
|
||||||
|
|||||||
@@ -9,7 +9,6 @@ from .const import (
|
|||||||
VARIANT_ESP32P4,
|
VARIANT_ESP32P4,
|
||||||
VARIANT_ESP32S2,
|
VARIANT_ESP32S2,
|
||||||
VARIANT_ESP32S3,
|
VARIANT_ESP32S3,
|
||||||
VARIANTS,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
STANDARD_BOARDS = {
|
STANDARD_BOARDS = {
|
||||||
@@ -25,9 +24,6 @@ STANDARD_BOARDS = {
|
|||||||
VARIANT_ESP32S3: "esp32-s3-devkitc-1",
|
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 = {
|
ESP32_BASE_PINS = {
|
||||||
"TX": 1,
|
"TX": 1,
|
||||||
"RX": 3,
|
"RX": 3,
|
||||||
|
|||||||
@@ -24,9 +24,12 @@ VARIANT_ESP32C5 = "ESP32C5"
|
|||||||
VARIANT_ESP32C6 = "ESP32C6"
|
VARIANT_ESP32C6 = "ESP32C6"
|
||||||
VARIANT_ESP32C61 = "ESP32C61"
|
VARIANT_ESP32C61 = "ESP32C61"
|
||||||
VARIANT_ESP32H2 = "ESP32H2"
|
VARIANT_ESP32H2 = "ESP32H2"
|
||||||
|
VARIANT_ESP32H4 = "ESP32H4"
|
||||||
|
VARIANT_ESP32H21 = "ESP32H21"
|
||||||
VARIANT_ESP32P4 = "ESP32P4"
|
VARIANT_ESP32P4 = "ESP32P4"
|
||||||
VARIANT_ESP32S2 = "ESP32S2"
|
VARIANT_ESP32S2 = "ESP32S2"
|
||||||
VARIANT_ESP32S3 = "ESP32S3"
|
VARIANT_ESP32S3 = "ESP32S3"
|
||||||
|
VARIANT_ESP32S31 = "ESP32S31"
|
||||||
VARIANTS = [
|
VARIANTS = [
|
||||||
VARIANT_ESP32,
|
VARIANT_ESP32,
|
||||||
VARIANT_ESP32C2,
|
VARIANT_ESP32C2,
|
||||||
@@ -35,9 +38,12 @@ VARIANTS = [
|
|||||||
VARIANT_ESP32C6,
|
VARIANT_ESP32C6,
|
||||||
VARIANT_ESP32C61,
|
VARIANT_ESP32C61,
|
||||||
VARIANT_ESP32H2,
|
VARIANT_ESP32H2,
|
||||||
|
VARIANT_ESP32H4,
|
||||||
|
VARIANT_ESP32H21,
|
||||||
VARIANT_ESP32P4,
|
VARIANT_ESP32P4,
|
||||||
VARIANT_ESP32S2,
|
VARIANT_ESP32S2,
|
||||||
VARIANT_ESP32S3,
|
VARIANT_ESP32S3,
|
||||||
|
VARIANT_ESP32S31,
|
||||||
]
|
]
|
||||||
|
|
||||||
VARIANT_FRIENDLY = {
|
VARIANT_FRIENDLY = {
|
||||||
@@ -48,9 +54,12 @@ VARIANT_FRIENDLY = {
|
|||||||
VARIANT_ESP32C6: "ESP32-C6",
|
VARIANT_ESP32C6: "ESP32-C6",
|
||||||
VARIANT_ESP32C61: "ESP32-C61",
|
VARIANT_ESP32C61: "ESP32-C61",
|
||||||
VARIANT_ESP32H2: "ESP32-H2",
|
VARIANT_ESP32H2: "ESP32-H2",
|
||||||
|
VARIANT_ESP32H4: "ESP32-H4",
|
||||||
|
VARIANT_ESP32H21: "ESP32-H21",
|
||||||
VARIANT_ESP32P4: "ESP32-P4",
|
VARIANT_ESP32P4: "ESP32-P4",
|
||||||
VARIANT_ESP32S2: "ESP32-S2",
|
VARIANT_ESP32S2: "ESP32-S2",
|
||||||
VARIANT_ESP32S3: "ESP32-S3",
|
VARIANT_ESP32S3: "ESP32-S3",
|
||||||
|
VARIANT_ESP32S31: "ESP32-S31",
|
||||||
}
|
}
|
||||||
|
|
||||||
esp32_ns = cg.esphome_ns.namespace("esp32")
|
esp32_ns = cg.esphome_ns.namespace("esp32")
|
||||||
|
|||||||
@@ -8,7 +8,9 @@
|
|||||||
|
|
||||||
void setup(); // NOLINT(readability-redundant-declaration)
|
void setup(); // NOLINT(readability-redundant-declaration)
|
||||||
|
|
||||||
// Weak stub for initArduino - overridden when the Arduino component is present
|
// Weak stub for initArduino - overridden when the Arduino component is present.
|
||||||
|
// Name must match the Arduino framework's entry point, so the naming check is suppressed.
|
||||||
|
// NOLINTNEXTLINE(readability-identifier-naming)
|
||||||
extern "C" __attribute__((weak)) void initArduino() {}
|
extern "C" __attribute__((weak)) void initArduino() {}
|
||||||
|
|
||||||
namespace esphome {
|
namespace esphome {
|
||||||
|
|||||||
@@ -41,6 +41,7 @@ static inline bool is_return_addr(uint32_t addr) {
|
|||||||
// Use memcpy for alignment safety — RISC-V C extension means code addresses
|
// Use memcpy for alignment safety — RISC-V C extension means code addresses
|
||||||
// are only 2-byte aligned, so addr-4 may not be 4-byte aligned.
|
// are only 2-byte aligned, so addr-4 may not be 4-byte aligned.
|
||||||
uint32_t inst;
|
uint32_t inst;
|
||||||
|
// NOLINTNEXTLINE(performance-no-int-to-ptr) - reading code memory at a raw address is the point
|
||||||
memcpy(&inst, (const void *) (addr - 4), sizeof(inst));
|
memcpy(&inst, (const void *) (addr - 4), sizeof(inst));
|
||||||
// RISC-V instruction encoding: bits [6:0] = opcode, bits [11:7] = rd
|
// RISC-V instruction encoding: bits [6:0] = opcode, bits [11:7] = rd
|
||||||
uint32_t opcode = inst & 0x7f; // Extract 7-bit opcode
|
uint32_t opcode = inst & 0x7f; // Extract 7-bit opcode
|
||||||
@@ -51,6 +52,7 @@ static inline bool is_return_addr(uint32_t addr) {
|
|||||||
// Check for 2-byte compressed c.jalr before this address (C extension).
|
// Check for 2-byte compressed c.jalr before this address (C extension).
|
||||||
// c.jalr saves to ra implicitly: funct4=1001, rs1!=0, rs2=0, op=10
|
// c.jalr saves to ra implicitly: funct4=1001, rs1!=0, rs2=0, op=10
|
||||||
if (addr >= 2) {
|
if (addr >= 2) {
|
||||||
|
// NOLINTNEXTLINE(performance-no-int-to-ptr) - reading code memory at a raw address is the point
|
||||||
uint16_t c_inst = *(uint16_t *) (addr - 2);
|
uint16_t c_inst = *(uint16_t *) (addr - 2);
|
||||||
if ((c_inst & 0xf07f) == 0x9002 && (c_inst & 0x0f80) != 0)
|
if ((c_inst & 0xf07f) == 0x9002 && (c_inst & 0x0f80) != 0)
|
||||||
return true;
|
return true;
|
||||||
@@ -101,6 +103,7 @@ static uint8_t IRAM_ATTR capture_riscv_backtrace(RvExcFrame *frame, uint32_t *ou
|
|||||||
out[count++] = frame->ra;
|
out[count++] = frame->ra;
|
||||||
}
|
}
|
||||||
*reg_count = count;
|
*reg_count = count;
|
||||||
|
// NOLINTNEXTLINE(performance-no-int-to-ptr) - walking the raw stack by address is the point
|
||||||
auto *scan_start = (uint32_t *) frame->sp;
|
auto *scan_start = (uint32_t *) frame->sp;
|
||||||
for (uint32_t i = 0; i < 64 && count < max; i++) {
|
for (uint32_t i = 0; i < 64 && count < max; i++) {
|
||||||
uint32_t val = scan_start[i];
|
uint32_t val = scan_start[i];
|
||||||
@@ -354,6 +357,8 @@ void crash_handler_log() {
|
|||||||
#if SOC_CPU_CORES_NUM > 1
|
#if SOC_CPU_CORES_NUM > 1
|
||||||
append_addrs_to_hint(hint, sizeof(hint), pos, s_raw_crash_data.other_backtrace,
|
append_addrs_to_hint(hint, sizeof(hint), pos, s_raw_crash_data.other_backtrace,
|
||||||
s_raw_crash_data.other_backtrace_count, s_raw_crash_data.other_reg_frame_count);
|
s_raw_crash_data.other_backtrace_count, s_raw_crash_data.other_reg_frame_count);
|
||||||
|
#else
|
||||||
|
(void) pos; // There is no second-core append on single-core targets, so pos would otherwise be unread.
|
||||||
#endif
|
#endif
|
||||||
ESP_LOGE(TAG, "%s", hint);
|
ESP_LOGE(TAG, "%s", hint);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -31,9 +31,12 @@ from .const import (
|
|||||||
VARIANT_ESP32C6,
|
VARIANT_ESP32C6,
|
||||||
VARIANT_ESP32C61,
|
VARIANT_ESP32C61,
|
||||||
VARIANT_ESP32H2,
|
VARIANT_ESP32H2,
|
||||||
|
VARIANT_ESP32H4,
|
||||||
|
VARIANT_ESP32H21,
|
||||||
VARIANT_ESP32P4,
|
VARIANT_ESP32P4,
|
||||||
VARIANT_ESP32S2,
|
VARIANT_ESP32S2,
|
||||||
VARIANT_ESP32S3,
|
VARIANT_ESP32S3,
|
||||||
|
VARIANT_ESP32S31,
|
||||||
esp32_ns,
|
esp32_ns,
|
||||||
)
|
)
|
||||||
from .gpio_esp32 import esp32_validate_gpio_pin, esp32_validate_supports
|
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_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_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_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_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_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_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)
|
ESP32InternalGPIOPin = esp32_ns.class_("ESP32InternalGPIOPin", cg.InternalGPIOPin)
|
||||||
|
|
||||||
@@ -120,6 +126,14 @@ _esp32_validations = {
|
|||||||
pin_validation=esp32_h2_validate_gpio_pin,
|
pin_validation=esp32_h2_validate_gpio_pin,
|
||||||
usage_validation=esp32_h2_validate_supports,
|
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(
|
VARIANT_ESP32P4: ESP32ValidationFunctions(
|
||||||
pin_validation=esp32_p4_validate_gpio_pin,
|
pin_validation=esp32_p4_validate_gpio_pin,
|
||||||
usage_validation=esp32_p4_validate_supports,
|
usage_validation=esp32_p4_validate_supports,
|
||||||
@@ -132,6 +146,10 @@ _esp32_validations = {
|
|||||||
pin_validation=esp32_s3_validate_gpio_pin,
|
pin_validation=esp32_s3_validate_gpio_pin,
|
||||||
usage_validation=esp32_s3_validate_supports,
|
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
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user