Compare commits

...

35 Commits

Author SHA1 Message Date
J. Nick Koston
17706e44dc template triggers 2026-02-01 20:31:54 +01:00
Simon Fischer
1ff2f3b6a3 [dlms_meter] Add dlms smart meter component (#8009)
Co-authored-by: Thomas Rupprecht <rupprecht.thomas@gmail.com>
Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com>
Co-authored-by: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com>
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-31 10:48:27 -05:00
Jonathan Swoboda
891382a32e [max7219] Allocate buffer in constructor (#13660)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-31 09:59:13 -05:00
J. Nick Koston
0fd50b2381 [esp32] Disable unused per-tag log filtering, saving ~536 bytes RAM (#13662) 2026-01-31 01:21:52 -06:00
Clyde Stubbs
9dcb469460 [core] Simplify generation of Lambda during to_code() (#13533) 2026-01-31 12:18:30 +11:00
J0k3r2k1
5e3561d60b [mipi_spi] Fix log_pin() FlashStringHelper compatibility (#13624)
Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com>
Co-authored-by: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com>
Co-authored-by: J. Nick Koston <nick@home-assistant.io>
2026-01-30 14:33:45 -06:00
Thomas Rupprecht
ca9ed369f9 [pmsx003] support device-types PMS1003, PMS3003, PMS9003M (#13640) 2026-01-30 14:59:47 -05:00
J. Nick Koston
4e96b20b46 [mqtt] Restore ESP8266 on_message defer to prevent stack overflow (#13648) 2026-01-30 12:49:14 -06:00
J. Nick Koston
a1a60c44da [web_server_base] Update ESPAsyncWebServer to 3.9.6 (#13639) 2026-01-30 12:48:34 -06:00
Shivam Maurya
898c8a5836 [core] ESP32 chip revision text (#13647) 2026-01-30 11:01:00 -05:00
Thomas Rupprecht
20edd11ca7 [pmsx003] Improvements (#13626) 2026-01-29 22:48:16 -05:00
J. Nick Koston
9a8c71a58b [logger] Fix USB Serial JTAG VFS linker errors when using UART on IDF (#13628)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-01-29 21:31:01 -06:00
Jonathan Swoboda
1a7435250e Merge branch 'release' into dev 2026-01-29 22:22:23 -05:00
Jonathan Swoboda
3c91d72403 Merge pull request #13632 from esphome/bump-2026.1.3
2026.1.3
2026-01-29 22:22:10 -05:00
Jonathan Swoboda
0a63fc6f05 Bump version to 2026.1.3 2026-01-29 21:11:09 -05:00
J. Nick Koston
50e739ee8e [http_request] Fix empty body for chunked transfer encoding responses (#13599) 2026-01-29 21:11:09 -05:00
J. Nick Koston
6c84f20491 [wifi] Fix ESP8266 yield panic when WiFi scan fails (#13603) 2026-01-29 21:11:09 -05:00
Cody Cutrer
a68506f924 [ld2450] preserve precision of angle (#13600) 2026-01-29 21:11:08 -05:00
esphomebot
a20d42ca0b Update webserver local assets to 20260127-190637 (#13573)
Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com>
2026-01-29 21:11:08 -05:00
J. Nick Koston
4ec8846198 [web_server] Add name_id to SSE for entity ID format migration (#13535) 2026-01-29 21:11:08 -05:00
J. Nick Koston
40ea65b1c0 [socket] ESP8266: call delay(0) instead of esp_delay(0, cb) for zero timeout (#13530) 2026-01-29 21:11:08 -05:00
J. Nick Koston
f7937ef952 [ota] Improve error message when device closes connection without responding (#13562) 2026-01-29 21:11:08 -05:00
sebcaps
d6bf137026 [mhz19] Fix Uninitialized var warning message (#13526) 2026-01-29 21:11:08 -05:00
esphomebot
ed9a672f44 Update webserver local assets to 20260122-204614 (#13455)
Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com>
2026-01-29 21:11:08 -05:00
David Woodhouse
823b5ac1ab [ch423] Add CH423 I/O expander component (#13079)
Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com>
2026-01-29 18:16:15 -05:00
dependabot[bot]
6de2049076 Bump actions/cache from 5.0.2 to 5.0.3 in /.github/actions/restore-python (#13622)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-01-29 14:35:52 -06:00
dependabot[bot]
cd43f8474e Bump actions/cache from 5.0.2 to 5.0.3 (#13621)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-01-29 14:35:32 -06:00
J. Nick Koston
ecc0b366b3 [esp32] Reduce compile time by excluding unused IDF components (#13610) 2026-01-29 13:21:12 -06:00
tomaszduda23
6a17db8857 [nrf52,zigbee] Support for number component (#13581) 2026-01-29 11:52:46 -05:00
Keith Burzinski
0843ec6ae8 [const] Move CONF_AUDIO_DAC (#13614) 2026-01-29 04:39:40 +00:00
J. Nick Koston
74c84c8747 [esp32] Add advanced sdkconfig options to reduce build time and binary size (#13611) 2026-01-28 18:20:39 -10:00
rwrozelle
3e9a6c582e [mdns] Do not broadcast registration when using openthread component (#13592) 2026-01-28 18:16:59 -10:00
Keith Burzinski
084113926c [es8156] Add bits_per_sample validation, comment code (#13612) 2026-01-28 22:03:50 -06:00
J. Nick Koston
a5f60750c2 [tx20] Eliminate heap allocations in wind sensor (#13298) 2026-01-29 16:07:41 +13:00
Clyde Stubbs
a382383d83 [workflows] Add deprecation check (#13584) 2026-01-29 12:08:45 +13:00
121 changed files with 3381 additions and 265 deletions

View File

@@ -1 +1 @@
cf3d341206b4184ec8b7fe85141aef4fe4696aa720c3f8a06d4e57930574bdab 069fa9526c52f7c580a9ec17c7678d12f142221387e9b561c18f95394d4629a3

View File

@@ -22,7 +22,7 @@ runs:
python-version: ${{ inputs.python-version }} python-version: ${{ inputs.python-version }}
- name: Restore Python virtual environment - name: Restore Python virtual environment
id: cache-venv id: cache-venv
uses: actions/cache/restore@8b402f58fbc84540c8b491a91e594a4576fec3d7 # v5.0.2 uses: actions/cache/restore@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5.0.3
with: with:
path: venv path: venv
# yamllint disable-line rule:line-length # yamllint disable-line rule:line-length

View File

@@ -3,6 +3,7 @@ module.exports = {
BOT_COMMENT_MARKER: '<!-- auto-label-pr-bot -->', BOT_COMMENT_MARKER: '<!-- auto-label-pr-bot -->',
CODEOWNERS_MARKER: '<!-- codeowners-request -->', CODEOWNERS_MARKER: '<!-- codeowners-request -->',
TOO_BIG_MARKER: '<!-- too-big-request -->', TOO_BIG_MARKER: '<!-- too-big-request -->',
DEPRECATED_COMPONENT_MARKER: '<!-- deprecated-component-request -->',
MANAGED_LABELS: [ MANAGED_LABELS: [
'new-component', 'new-component',
@@ -27,6 +28,7 @@ module.exports = {
'breaking-change', 'breaking-change',
'developer-breaking-change', 'developer-breaking-change',
'code-quality', 'code-quality',
'deprecated-component'
], ],
DOCS_PR_PATTERNS: [ DOCS_PR_PATTERNS: [

View File

@@ -251,6 +251,76 @@ async function detectPRTemplateCheckboxes(context) {
return labels; return labels;
} }
// Strategy: Deprecated component detection
async function detectDeprecatedComponents(github, context, changedFiles) {
const labels = new Set();
const deprecatedInfo = [];
const { owner, repo } = context.repo;
// Compile regex once for better performance
const componentFileRegex = /^esphome\/components\/([^\/]+)\//;
// Get files that are modified or added in components directory
const componentFiles = changedFiles.filter(file => componentFileRegex.test(file));
if (componentFiles.length === 0) {
return { labels, deprecatedInfo };
}
// Extract unique component names using the same regex
const components = new Set();
for (const file of componentFiles) {
const match = file.match(componentFileRegex);
if (match) {
components.add(match[1]);
}
}
// Get PR head to fetch files from the PR branch
const prNumber = context.payload.pull_request.number;
// Check each component's __init__.py for DEPRECATED_COMPONENT constant
for (const component of components) {
const initFile = `esphome/components/${component}/__init__.py`;
try {
// Fetch file content from PR head using GitHub API
const { data: fileData } = await github.rest.repos.getContent({
owner,
repo,
path: initFile,
ref: `refs/pull/${prNumber}/head`
});
// Decode base64 content
const content = Buffer.from(fileData.content, 'base64').toString('utf8');
// Look for DEPRECATED_COMPONENT = "message" or DEPRECATED_COMPONENT = 'message'
// Support single quotes, double quotes, and triple quotes (for multiline)
const doubleQuoteMatch = content.match(/DEPRECATED_COMPONENT\s*=\s*"""([\s\S]*?)"""/s) ||
content.match(/DEPRECATED_COMPONENT\s*=\s*"((?:[^"\\]|\\.)*)"/);
const singleQuoteMatch = content.match(/DEPRECATED_COMPONENT\s*=\s*'''([\s\S]*?)'''/s) ||
content.match(/DEPRECATED_COMPONENT\s*=\s*'((?:[^'\\]|\\.)*)'/);
const deprecatedMatch = doubleQuoteMatch || singleQuoteMatch;
if (deprecatedMatch) {
labels.add('deprecated-component');
deprecatedInfo.push({
component: component,
message: deprecatedMatch[1].trim()
});
console.log(`Found deprecated component: ${component}`);
}
} catch (error) {
// Only log if it's not a simple "file not found" error (404)
if (error.status !== 404) {
console.log(`Error reading ${initFile}:`, error.message);
}
}
}
return { labels, deprecatedInfo };
}
// Strategy: Requirements detection // Strategy: Requirements detection
async function detectRequirements(allLabels, prFiles, context) { async function detectRequirements(allLabels, prFiles, context) {
const labels = new Set(); const labels = new Set();
@@ -298,5 +368,6 @@ module.exports = {
detectCodeOwner, detectCodeOwner,
detectTests, detectTests,
detectPRTemplateCheckboxes, detectPRTemplateCheckboxes,
detectDeprecatedComponents,
detectRequirements detectRequirements
}; };

View File

@@ -11,6 +11,7 @@ const {
detectCodeOwner, detectCodeOwner,
detectTests, detectTests,
detectPRTemplateCheckboxes, detectPRTemplateCheckboxes,
detectDeprecatedComponents,
detectRequirements detectRequirements
} = require('./detectors'); } = require('./detectors');
const { handleReviews } = require('./reviews'); const { handleReviews } = require('./reviews');
@@ -112,6 +113,7 @@ module.exports = async ({ github, context }) => {
codeOwnerLabels, codeOwnerLabels,
testLabels, testLabels,
checkboxLabels, checkboxLabels,
deprecatedResult
] = await Promise.all([ ] = await Promise.all([
detectMergeBranch(context), detectMergeBranch(context),
detectComponentPlatforms(changedFiles, apiData), detectComponentPlatforms(changedFiles, apiData),
@@ -124,8 +126,13 @@ module.exports = async ({ github, context }) => {
detectCodeOwner(github, context, changedFiles), detectCodeOwner(github, context, changedFiles),
detectTests(changedFiles), detectTests(changedFiles),
detectPRTemplateCheckboxes(context), detectPRTemplateCheckboxes(context),
detectDeprecatedComponents(github, context, changedFiles)
]); ]);
// Extract deprecated component info
const deprecatedLabels = deprecatedResult.labels;
const deprecatedInfo = deprecatedResult.deprecatedInfo;
// Combine all labels // Combine all labels
const allLabels = new Set([ const allLabels = new Set([
...branchLabels, ...branchLabels,
@@ -139,6 +146,7 @@ module.exports = async ({ github, context }) => {
...codeOwnerLabels, ...codeOwnerLabels,
...testLabels, ...testLabels,
...checkboxLabels, ...checkboxLabels,
...deprecatedLabels
]); ]);
// Detect requirements based on all other labels // Detect requirements based on all other labels
@@ -169,7 +177,7 @@ module.exports = async ({ github, context }) => {
console.log('Computed labels:', finalLabels.join(', ')); console.log('Computed labels:', finalLabels.join(', '));
// Handle reviews // Handle reviews
await handleReviews(github, context, finalLabels, originalLabelCount, prFiles, totalAdditions, totalDeletions, MAX_LABELS, TOO_BIG_THRESHOLD); await handleReviews(github, context, finalLabels, originalLabelCount, deprecatedInfo, prFiles, totalAdditions, totalDeletions, MAX_LABELS, TOO_BIG_THRESHOLD);
// Apply labels // Apply labels
await applyLabels(github, context, finalLabels); await applyLabels(github, context, finalLabels);

View File

@@ -2,12 +2,29 @@ const {
BOT_COMMENT_MARKER, BOT_COMMENT_MARKER,
CODEOWNERS_MARKER, CODEOWNERS_MARKER,
TOO_BIG_MARKER, TOO_BIG_MARKER,
DEPRECATED_COMPONENT_MARKER
} = require('./constants'); } = require('./constants');
// Generate review messages // Generate review messages
function generateReviewMessages(finalLabels, originalLabelCount, prFiles, totalAdditions, totalDeletions, prAuthor, MAX_LABELS, TOO_BIG_THRESHOLD) { function generateReviewMessages(finalLabels, originalLabelCount, deprecatedInfo, prFiles, totalAdditions, totalDeletions, prAuthor, MAX_LABELS, TOO_BIG_THRESHOLD) {
const messages = []; const messages = [];
// Deprecated component message
if (finalLabels.includes('deprecated-component') && deprecatedInfo && deprecatedInfo.length > 0) {
let message = `${DEPRECATED_COMPONENT_MARKER}\n### ⚠️ Deprecated Component\n\n`;
message += `Hey there @${prAuthor},\n`;
message += `This PR modifies one or more deprecated components. Please be aware:\n\n`;
for (const info of deprecatedInfo) {
message += `#### Component: \`${info.component}\`\n`;
message += `${info.message}\n\n`;
}
message += `Consider migrating to the recommended alternative if applicable.`;
messages.push(message);
}
// Too big message // Too big message
if (finalLabels.includes('too-big')) { if (finalLabels.includes('too-big')) {
const testAdditions = prFiles const testAdditions = prFiles
@@ -54,14 +71,14 @@ function generateReviewMessages(finalLabels, originalLabelCount, prFiles, totalA
} }
// Handle reviews // Handle reviews
async function handleReviews(github, context, finalLabels, originalLabelCount, prFiles, totalAdditions, totalDeletions, MAX_LABELS, TOO_BIG_THRESHOLD) { async function handleReviews(github, context, finalLabels, originalLabelCount, deprecatedInfo, prFiles, totalAdditions, totalDeletions, MAX_LABELS, TOO_BIG_THRESHOLD) {
const { owner, repo } = context.repo; const { owner, repo } = context.repo;
const pr_number = context.issue.number; const pr_number = context.issue.number;
const prAuthor = context.payload.pull_request.user.login; const prAuthor = context.payload.pull_request.user.login;
const reviewMessages = generateReviewMessages(finalLabels, originalLabelCount, prFiles, totalAdditions, totalDeletions, prAuthor, MAX_LABELS, TOO_BIG_THRESHOLD); const reviewMessages = generateReviewMessages(finalLabels, originalLabelCount, deprecatedInfo, prFiles, totalAdditions, totalDeletions, prAuthor, MAX_LABELS, TOO_BIG_THRESHOLD);
const hasReviewableLabels = finalLabels.some(label => const hasReviewableLabels = finalLabels.some(label =>
['too-big', 'needs-codeowners'].includes(label) ['too-big', 'needs-codeowners', 'deprecated-component'].includes(label)
); );
const { data: reviews } = await github.rest.pulls.listReviews({ const { data: reviews } = await github.rest.pulls.listReviews({

View File

@@ -47,7 +47,7 @@ jobs:
python-version: ${{ env.DEFAULT_PYTHON }} python-version: ${{ env.DEFAULT_PYTHON }}
- name: Restore Python virtual environment - name: Restore Python virtual environment
id: cache-venv id: cache-venv
uses: actions/cache@8b402f58fbc84540c8b491a91e594a4576fec3d7 # v5.0.2 uses: actions/cache@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5.0.3
with: with:
path: venv path: venv
# yamllint disable-line rule:line-length # yamllint disable-line rule:line-length
@@ -157,7 +157,7 @@ jobs:
token: ${{ secrets.CODECOV_TOKEN }} token: ${{ secrets.CODECOV_TOKEN }}
- name: Save Python virtual environment cache - name: Save Python virtual environment cache
if: github.ref == 'refs/heads/dev' if: github.ref == 'refs/heads/dev'
uses: actions/cache/save@8b402f58fbc84540c8b491a91e594a4576fec3d7 # v5.0.2 uses: actions/cache/save@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5.0.3
with: with:
path: venv path: venv
key: ${{ runner.os }}-${{ steps.restore-python.outputs.python-version }}-venv-${{ needs.common.outputs.cache-key }} key: ${{ runner.os }}-${{ steps.restore-python.outputs.python-version }}-venv-${{ needs.common.outputs.cache-key }}
@@ -193,7 +193,7 @@ 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: Restore components graph cache - name: Restore components graph cache
uses: actions/cache/restore@8b402f58fbc84540c8b491a91e594a4576fec3d7 # v5.0.2 uses: actions/cache/restore@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5.0.3
with: with:
path: .temp/components_graph.json path: .temp/components_graph.json
key: components-graph-${{ hashFiles('esphome/components/**/*.py') }} key: components-graph-${{ hashFiles('esphome/components/**/*.py') }}
@@ -223,7 +223,7 @@ jobs:
echo "component-test-batches=$(echo "$output" | jq -c '.component_test_batches')" >> $GITHUB_OUTPUT echo "component-test-batches=$(echo "$output" | jq -c '.component_test_batches')" >> $GITHUB_OUTPUT
- name: Save components graph cache - name: Save components graph cache
if: github.ref == 'refs/heads/dev' if: github.ref == 'refs/heads/dev'
uses: actions/cache/save@8b402f58fbc84540c8b491a91e594a4576fec3d7 # v5.0.2 uses: actions/cache/save@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5.0.3
with: with:
path: .temp/components_graph.json path: .temp/components_graph.json
key: components-graph-${{ hashFiles('esphome/components/**/*.py') }} key: components-graph-${{ hashFiles('esphome/components/**/*.py') }}
@@ -245,7 +245,7 @@ jobs:
python-version: "3.13" python-version: "3.13"
- name: Restore Python virtual environment - name: Restore Python virtual environment
id: cache-venv id: cache-venv
uses: actions/cache@8b402f58fbc84540c8b491a91e594a4576fec3d7 # v5.0.2 uses: actions/cache@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5.0.3
with: with:
path: venv path: venv
key: ${{ runner.os }}-${{ steps.python.outputs.python-version }}-venv-${{ needs.common.outputs.cache-key }} key: ${{ runner.os }}-${{ steps.python.outputs.python-version }}-venv-${{ needs.common.outputs.cache-key }}
@@ -334,14 +334,14 @@ jobs:
- name: Cache platformio - name: Cache platformio
if: github.ref == 'refs/heads/dev' if: github.ref == 'refs/heads/dev'
uses: actions/cache@8b402f58fbc84540c8b491a91e594a4576fec3d7 # v5.0.2 uses: actions/cache@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5.0.3
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'
uses: actions/cache/restore@8b402f58fbc84540c8b491a91e594a4576fec3d7 # v5.0.2 uses: actions/cache/restore@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5.0.3
with: with:
path: ~/.platformio path: ~/.platformio
key: platformio-${{ matrix.pio_cache_key }}-${{ hashFiles('platformio.ini') }} key: platformio-${{ matrix.pio_cache_key }}-${{ hashFiles('platformio.ini') }}
@@ -413,14 +413,14 @@ jobs:
- name: Cache platformio - name: Cache platformio
if: github.ref == 'refs/heads/dev' if: github.ref == 'refs/heads/dev'
uses: actions/cache@8b402f58fbc84540c8b491a91e594a4576fec3d7 # v5.0.2 uses: actions/cache@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5.0.3
with: with:
path: ~/.platformio path: ~/.platformio
key: platformio-tidyesp32-${{ hashFiles('platformio.ini') }} key: platformio-tidyesp32-${{ hashFiles('platformio.ini') }}
- name: Cache platformio - name: Cache platformio
if: github.ref != 'refs/heads/dev' if: github.ref != 'refs/heads/dev'
uses: actions/cache/restore@8b402f58fbc84540c8b491a91e594a4576fec3d7 # v5.0.2 uses: actions/cache/restore@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5.0.3
with: with:
path: ~/.platformio path: ~/.platformio
key: platformio-tidyesp32-${{ hashFiles('platformio.ini') }} key: platformio-tidyesp32-${{ hashFiles('platformio.ini') }}
@@ -502,14 +502,14 @@ jobs:
- name: Cache platformio - name: Cache platformio
if: github.ref == 'refs/heads/dev' if: github.ref == 'refs/heads/dev'
uses: actions/cache@8b402f58fbc84540c8b491a91e594a4576fec3d7 # v5.0.2 uses: actions/cache@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5.0.3
with: with:
path: ~/.platformio path: ~/.platformio
key: platformio-tidyesp32-${{ hashFiles('platformio.ini') }} key: platformio-tidyesp32-${{ hashFiles('platformio.ini') }}
- name: Cache platformio - name: Cache platformio
if: github.ref != 'refs/heads/dev' if: github.ref != 'refs/heads/dev'
uses: actions/cache/restore@8b402f58fbc84540c8b491a91e594a4576fec3d7 # v5.0.2 uses: actions/cache/restore@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5.0.3
with: with:
path: ~/.platformio path: ~/.platformio
key: platformio-tidyesp32-${{ hashFiles('platformio.ini') }} key: platformio-tidyesp32-${{ hashFiles('platformio.ini') }}
@@ -735,7 +735,7 @@ jobs:
- name: Restore cached memory analysis - name: Restore cached memory analysis
id: cache-memory-analysis id: cache-memory-analysis
if: steps.check-script.outputs.skip != 'true' if: steps.check-script.outputs.skip != 'true'
uses: actions/cache/restore@8b402f58fbc84540c8b491a91e594a4576fec3d7 # v5.0.2 uses: actions/cache/restore@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5.0.3
with: with:
path: memory-analysis-target.json path: memory-analysis-target.json
key: ${{ steps.cache-key.outputs.cache-key }} key: ${{ steps.cache-key.outputs.cache-key }}
@@ -759,7 +759,7 @@ jobs:
- name: Cache platformio - name: Cache platformio
if: steps.check-script.outputs.skip != 'true' && steps.cache-memory-analysis.outputs.cache-hit != 'true' if: steps.check-script.outputs.skip != 'true' && steps.cache-memory-analysis.outputs.cache-hit != 'true'
uses: actions/cache/restore@8b402f58fbc84540c8b491a91e594a4576fec3d7 # v5.0.2 uses: actions/cache/restore@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5.0.3
with: with:
path: ~/.platformio path: ~/.platformio
key: platformio-memory-${{ fromJSON(needs.determine-jobs.outputs.memory_impact).platform }}-${{ hashFiles('platformio.ini') }} key: platformio-memory-${{ fromJSON(needs.determine-jobs.outputs.memory_impact).platform }}-${{ hashFiles('platformio.ini') }}
@@ -800,7 +800,7 @@ jobs:
- name: Save memory analysis to cache - name: Save memory analysis to cache
if: steps.check-script.outputs.skip != 'true' && steps.cache-memory-analysis.outputs.cache-hit != 'true' && steps.build.outcome == 'success' if: steps.check-script.outputs.skip != 'true' && steps.cache-memory-analysis.outputs.cache-hit != 'true' && steps.build.outcome == 'success'
uses: actions/cache/save@8b402f58fbc84540c8b491a91e594a4576fec3d7 # v5.0.2 uses: actions/cache/save@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5.0.3
with: with:
path: memory-analysis-target.json path: memory-analysis-target.json
key: ${{ steps.cache-key.outputs.cache-key }} key: ${{ steps.cache-key.outputs.cache-key }}
@@ -847,7 +847,7 @@ 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 platformio
uses: actions/cache/restore@8b402f58fbc84540c8b491a91e594a4576fec3d7 # v5.0.2 uses: actions/cache/restore@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5.0.3
with: with:
path: ~/.platformio path: ~/.platformio
key: platformio-memory-${{ fromJSON(needs.determine-jobs.outputs.memory_impact).platform }}-${{ hashFiles('platformio.ini') }} key: platformio-memory-${{ fromJSON(needs.determine-jobs.outputs.memory_impact).platform }}-${{ hashFiles('platformio.ini') }}

View File

@@ -104,6 +104,7 @@ esphome/components/cc1101/* @gabest11 @lygris
esphome/components/ccs811/* @habbie esphome/components/ccs811/* @habbie
esphome/components/cd74hc4067/* @asoehlke esphome/components/cd74hc4067/* @asoehlke
esphome/components/ch422g/* @clydebarrow @jesterret esphome/components/ch422g/* @clydebarrow @jesterret
esphome/components/ch423/* @dwmw2
esphome/components/chsc6x/* @kkosik20 esphome/components/chsc6x/* @kkosik20
esphome/components/climate/* @esphome/core esphome/components/climate/* @esphome/core
esphome/components/climate_ir/* @glmnet esphome/components/climate_ir/* @glmnet
@@ -133,6 +134,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/dps310/* @kbx81 esphome/components/dps310/* @kbx81
esphome/components/ds1307/* @badbadc0ffee esphome/components/ds1307/* @badbadc0ffee
esphome/components/ds2484/* @mrk-its esphome/components/ds2484/* @mrk-its

View File

@@ -2,7 +2,7 @@ import logging
import esphome.codegen as cg import esphome.codegen as cg
from esphome.components import sensor, voltage_sampler from esphome.components import sensor, voltage_sampler
from esphome.components.esp32 import get_esp32_variant from esphome.components.esp32 import get_esp32_variant, include_builtin_idf_component
from esphome.components.nrf52.const import AIN_TO_GPIO, EXTRA_ADC from esphome.components.nrf52.const import AIN_TO_GPIO, EXTRA_ADC
from esphome.components.zephyr import ( from esphome.components.zephyr import (
zephyr_add_overlay, zephyr_add_overlay,
@@ -118,6 +118,9 @@ async def to_code(config):
cg.add(var.set_sampling_mode(config[CONF_SAMPLING_MODE])) cg.add(var.set_sampling_mode(config[CONF_SAMPLING_MODE]))
if CORE.is_esp32: if CORE.is_esp32:
# Re-enable ESP-IDF's ADC driver (excluded by default to save compile time)
include_builtin_idf_component("esp_adc")
if attenuation := config.get(CONF_ATTENUATION): if attenuation := config.get(CONF_ATTENUATION):
if attenuation == "auto": if attenuation == "auto":
cg.add(var.set_autorange(cg.global_ns.true)) cg.add(var.set_autorange(cg.global_ns.true))

View File

@@ -1,5 +1,5 @@
import esphome.codegen as cg import esphome.codegen as cg
from esphome.components.esp32 import add_idf_component from esphome.components.esp32 import add_idf_component, include_builtin_idf_component
import esphome.config_validation as cv import esphome.config_validation as cv
from esphome.const import CONF_BITS_PER_SAMPLE, CONF_NUM_CHANNELS, CONF_SAMPLE_RATE from esphome.const import CONF_BITS_PER_SAMPLE, CONF_NUM_CHANNELS, CONF_SAMPLE_RATE
import esphome.final_validate as fv import esphome.final_validate as fv
@@ -166,6 +166,9 @@ def final_validate_audio_schema(
async def to_code(config): async def to_code(config):
# Re-enable ESP-IDF's HTTP client (excluded by default to save compile time)
include_builtin_idf_component("esp_http_client")
add_idf_component( add_idf_component(
name="esphome/esp-audio-libs", name="esphome/esp-audio-libs",
ref="2.0.3", ref="2.0.3",

View File

@@ -0,0 +1,103 @@
from esphome import pins
import esphome.codegen as cg
from esphome.components import i2c
from esphome.components.i2c import I2CBus
import esphome.config_validation as cv
from esphome.const import (
CONF_I2C_ID,
CONF_ID,
CONF_INPUT,
CONF_INVERTED,
CONF_MODE,
CONF_NUMBER,
CONF_OPEN_DRAIN,
CONF_OUTPUT,
)
from esphome.core import CORE
CODEOWNERS = ["@dwmw2"]
DEPENDENCIES = ["i2c"]
MULTI_CONF = True
ch423_ns = cg.esphome_ns.namespace("ch423")
CH423Component = ch423_ns.class_("CH423Component", cg.Component, i2c.I2CDevice)
CH423GPIOPin = ch423_ns.class_(
"CH423GPIOPin", cg.GPIOPin, cg.Parented.template(CH423Component)
)
CONF_CH423 = "ch423"
# Note that no address is configurable - each register in the CH423 has a dedicated i2c address
CONFIG_SCHEMA = cv.Schema(
{
cv.GenerateID(CONF_ID): cv.declare_id(CH423Component),
cv.GenerateID(CONF_I2C_ID): cv.use_id(I2CBus),
}
).extend(cv.COMPONENT_SCHEMA)
async def to_code(config):
var = cg.new_Pvariable(config[CONF_ID])
await cg.register_component(var, config)
# Can't use register_i2c_device because there is no CONF_ADDRESS
parent = await cg.get_variable(config[CONF_I2C_ID])
cg.add(var.set_i2c_bus(parent))
# This is used as a final validation step so that modes have been fully transformed.
def pin_mode_check(pin_config, _):
if pin_config[CONF_MODE][CONF_INPUT] and pin_config[CONF_NUMBER] >= 8:
raise cv.Invalid("CH423 only supports input on pins 0-7")
if pin_config[CONF_MODE][CONF_OPEN_DRAIN] and pin_config[CONF_NUMBER] < 8:
raise cv.Invalid("CH423 only supports open drain output on pins 8-23")
ch423_id = pin_config[CONF_CH423]
pin_num = pin_config[CONF_NUMBER]
is_output = pin_config[CONF_MODE][CONF_OUTPUT]
is_open_drain = pin_config[CONF_MODE][CONF_OPEN_DRAIN]
# Track pin modes per CH423 instance in CORE.data
ch423_modes = CORE.data.setdefault(CONF_CH423, {})
if ch423_id not in ch423_modes:
ch423_modes[ch423_id] = {"gpio_output": None, "gpo_open_drain": None}
if pin_num < 8:
# GPIO pins (0-7): all must have same direction
if ch423_modes[ch423_id]["gpio_output"] is None:
ch423_modes[ch423_id]["gpio_output"] = is_output
elif ch423_modes[ch423_id]["gpio_output"] != is_output:
raise cv.Invalid(
"CH423 GPIO pins (0-7) must all be configured as input or all as output"
)
# GPO pins (8-23): all must have same open-drain setting
elif ch423_modes[ch423_id]["gpo_open_drain"] is None:
ch423_modes[ch423_id]["gpo_open_drain"] = is_open_drain
elif ch423_modes[ch423_id]["gpo_open_drain"] != is_open_drain:
raise cv.Invalid(
"CH423 GPO pins (8-23) must all be configured as push-pull or all as open-drain"
)
CH423_PIN_SCHEMA = pins.gpio_base_schema(
CH423GPIOPin,
cv.int_range(min=0, max=23),
modes=[CONF_INPUT, CONF_OUTPUT, CONF_OPEN_DRAIN],
).extend(
{
cv.Required(CONF_CH423): cv.use_id(CH423Component),
}
)
@pins.PIN_SCHEMA_REGISTRY.register(CONF_CH423, CH423_PIN_SCHEMA, pin_mode_check)
async def ch423_pin_to_code(config):
var = cg.new_Pvariable(config[CONF_ID])
parent = await cg.get_variable(config[CONF_CH423])
cg.add(var.set_parent(parent))
num = config[CONF_NUMBER]
cg.add(var.set_pin(num))
cg.add(var.set_inverted(config[CONF_INVERTED]))
cg.add(var.set_flags(pins.gpio_flags_expr(config[CONF_MODE])))
return var

View File

@@ -0,0 +1,148 @@
#include "ch423.h"
#include "esphome/core/log.h"
#include "esphome/core/progmem.h"
namespace esphome::ch423 {
static constexpr uint8_t CH423_REG_SYS = 0x24; // Set system parameters (0x48 >> 1)
static constexpr uint8_t CH423_SYS_IO_OE = 0x01; // IO output enable
static constexpr uint8_t CH423_SYS_OD_EN = 0x04; // Open drain enable for OC pins
static constexpr uint8_t CH423_REG_IO = 0x30; // Write/read IO7-IO0 (0x60 >> 1)
static constexpr uint8_t CH423_REG_IO_RD = 0x26; // Read IO7-IO0 (0x4D >> 1, rounded down)
static constexpr uint8_t CH423_REG_OCL = 0x22; // Write OC7-OC0 (0x44 >> 1)
static constexpr uint8_t CH423_REG_OCH = 0x23; // Write OC15-OC8 (0x46 >> 1)
static const char *const TAG = "ch423";
void CH423Component::setup() {
// set outputs before mode
this->write_outputs_();
// Set system parameters and check for errors
bool success = this->write_reg_(CH423_REG_SYS, this->sys_params_);
// Only read inputs if pins are configured for input (IO_OE not set)
if (success && !(this->sys_params_ & CH423_SYS_IO_OE)) {
success = this->read_inputs_();
}
if (!success) {
ESP_LOGE(TAG, "CH423 not detected");
this->mark_failed();
return;
}
ESP_LOGCONFIG(TAG, "Initialization complete. Warning: %d, Error: %d", this->status_has_warning(),
this->status_has_error());
}
void CH423Component::loop() {
// Clear all the previously read flags.
this->pin_read_flags_ = 0x00;
}
void CH423Component::dump_config() {
ESP_LOGCONFIG(TAG, "CH423:");
if (this->is_failed()) {
ESP_LOGE(TAG, ESP_LOG_MSG_COMM_FAIL);
}
}
void CH423Component::pin_mode(uint8_t pin, gpio::Flags flags) {
if (pin < 8) {
if (flags & gpio::FLAG_OUTPUT) {
this->sys_params_ |= CH423_SYS_IO_OE;
}
} else if (pin >= 8 && pin < 24) {
if (flags & gpio::FLAG_OPEN_DRAIN) {
this->sys_params_ |= CH423_SYS_OD_EN;
}
}
}
bool CH423Component::digital_read(uint8_t pin) {
if (this->pin_read_flags_ == 0 || this->pin_read_flags_ & (1 << pin)) {
// Read values on first access or in case it's being read again in the same loop
this->read_inputs_();
}
this->pin_read_flags_ |= (1 << pin);
return (this->input_bits_ & (1 << pin)) != 0;
}
void CH423Component::digital_write(uint8_t pin, bool value) {
if (value) {
this->output_bits_ |= (1 << pin);
} else {
this->output_bits_ &= ~(1 << pin);
}
this->write_outputs_();
}
bool CH423Component::read_inputs_() {
if (this->is_failed()) {
return false;
}
// reading inputs requires IO_OE to be 0
if (this->sys_params_ & CH423_SYS_IO_OE) {
return false;
}
uint8_t result = this->read_reg_(CH423_REG_IO_RD);
this->input_bits_ = result;
this->status_clear_warning();
return true;
}
// Write a register. Can't use the standard write_byte() method because there is no single pre-configured i2c address.
bool CH423Component::write_reg_(uint8_t reg, uint8_t value) {
auto err = this->bus_->write_readv(reg, &value, 1, nullptr, 0);
if (err != i2c::ERROR_OK) {
char buf[64];
ESPHOME_snprintf_P(buf, sizeof(buf), ESPHOME_PSTR("write failed for register 0x%X, error %d"), reg, err);
this->status_set_warning(buf);
return false;
}
this->status_clear_warning();
return true;
}
uint8_t CH423Component::read_reg_(uint8_t reg) {
uint8_t value;
auto err = this->bus_->write_readv(reg, nullptr, 0, &value, 1);
if (err != i2c::ERROR_OK) {
char buf[64];
ESPHOME_snprintf_P(buf, sizeof(buf), ESPHOME_PSTR("read failed for register 0x%X, error %d"), reg, err);
this->status_set_warning(buf);
return 0;
}
this->status_clear_warning();
return value;
}
bool CH423Component::write_outputs_() {
bool success = true;
// Write IO7-IO0
success &= this->write_reg_(CH423_REG_IO, static_cast<uint8_t>(this->output_bits_));
// Write OC7-OC0
success &= this->write_reg_(CH423_REG_OCL, static_cast<uint8_t>(this->output_bits_ >> 8));
// Write OC15-OC8
success &= this->write_reg_(CH423_REG_OCH, static_cast<uint8_t>(this->output_bits_ >> 16));
return success;
}
float CH423Component::get_setup_priority() const { return setup_priority::IO; }
// Run our loop() method very early in the loop, so that we cache read values
// before other components call our digital_read() method.
float CH423Component::get_loop_priority() const { return 9.0f; } // Just after WIFI
void CH423GPIOPin::pin_mode(gpio::Flags flags) { this->parent_->pin_mode(this->pin_, flags); }
bool CH423GPIOPin::digital_read() { return this->parent_->digital_read(this->pin_) ^ this->inverted_; }
void CH423GPIOPin::digital_write(bool value) { this->parent_->digital_write(this->pin_, value ^ this->inverted_); }
size_t CH423GPIOPin::dump_summary(char *buffer, size_t len) const {
return snprintf(buffer, len, "EXIO%u via CH423", this->pin_);
}
void CH423GPIOPin::set_flags(gpio::Flags flags) {
flags_ = flags;
this->parent_->pin_mode(this->pin_, flags);
}
} // namespace esphome::ch423

View File

@@ -0,0 +1,67 @@
#pragma once
#include "esphome/core/component.h"
#include "esphome/core/hal.h"
#include "esphome/components/i2c/i2c.h"
namespace esphome::ch423 {
class CH423Component : public Component, public i2c::I2CDevice {
public:
CH423Component() = default;
/// Check i2c availability and setup masks
void setup() override;
/// Poll for input changes periodically
void loop() override;
/// Helper function to read the value of a pin.
bool digital_read(uint8_t pin);
/// Helper function to write the value of a pin.
void digital_write(uint8_t pin, bool value);
/// Helper function to set the pin mode of a pin.
void pin_mode(uint8_t pin, gpio::Flags flags);
float get_setup_priority() const override;
float get_loop_priority() const override;
void dump_config() override;
protected:
bool write_reg_(uint8_t reg, uint8_t value);
uint8_t read_reg_(uint8_t reg);
bool read_inputs_();
bool write_outputs_();
/// The mask to write as output state - 1 means HIGH, 0 means LOW
uint32_t output_bits_{0x00};
/// Flags to check if read previously during this loop
uint8_t pin_read_flags_{0x00};
/// Copy of last read values
uint8_t input_bits_{0x00};
/// System parameters
uint8_t sys_params_{0x00};
};
/// Helper class to expose a CH423 pin as a GPIO pin.
class CH423GPIOPin : public GPIOPin {
public:
void setup() override{};
void pin_mode(gpio::Flags flags) override;
bool digital_read() override;
void digital_write(bool value) override;
size_t dump_summary(char *buffer, size_t len) const override;
void set_parent(CH423Component *parent) { parent_ = parent; }
void set_pin(uint8_t pin) { pin_ = pin; }
void set_inverted(bool inverted) { inverted_ = inverted; }
void set_flags(gpio::Flags flags);
gpio::Flags get_flags() const override { return this->flags_; }
protected:
CH423Component *parent_{};
uint8_t pin_{};
bool inverted_{};
gpio::Flags flags_{};
};
} // namespace esphome::ch423

View File

@@ -15,7 +15,7 @@ from esphome.const import (
CONF_UPDATE_INTERVAL, CONF_UPDATE_INTERVAL,
SCHEDULER_DONT_RUN, SCHEDULER_DONT_RUN,
) )
from esphome.core import CoroPriority, coroutine_with_priority from esphome.core import CORE, CoroPriority, coroutine_with_priority
IS_PLATFORM_COMPONENT = True IS_PLATFORM_COMPONENT = True
@@ -222,3 +222,8 @@ async def display_is_displaying_page_to_code(config, condition_id, template_arg,
async def to_code(config): async def to_code(config):
cg.add_global(display_ns.using) cg.add_global(display_ns.using)
cg.add_define("USE_DISPLAY") cg.add_define("USE_DISPLAY")
if CORE.is_esp32:
# Re-enable ESP-IDF's LCD driver (excluded by default to save compile time)
from esphome.components.esp32 import include_builtin_idf_component
include_builtin_idf_component("esp_lcd")

View File

@@ -0,0 +1,57 @@
import esphome.codegen as cg
from esphome.components import uart
import esphome.config_validation as cv
from esphome.const import CONF_ID, PLATFORM_ESP32, PLATFORM_ESP8266
CODEOWNERS = ["@SimonFischer04"]
DEPENDENCIES = ["uart"]
CONF_DLMS_METER_ID = "dlms_meter_id"
CONF_DECRYPTION_KEY = "decryption_key"
CONF_PROVIDER = "provider"
PROVIDERS = {"generic": 0, "netznoe": 1}
dlms_meter_component_ns = cg.esphome_ns.namespace("dlms_meter")
DlmsMeterComponent = dlms_meter_component_ns.class_(
"DlmsMeterComponent", cg.Component, uart.UARTDevice
)
def validate_key(value):
value = cv.string_strict(value)
if len(value) != 32:
raise cv.Invalid("Decryption key must be 32 hex characters (16 bytes)")
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
CONFIG_SCHEMA = cv.All(
cv.Schema(
{
cv.GenerateID(): cv.declare_id(DlmsMeterComponent),
cv.Required(CONF_DECRYPTION_KEY): validate_key,
cv.Optional(CONF_PROVIDER, default="generic"): cv.enum(
PROVIDERS, lower=True
),
}
)
.extend(uart.UART_DEVICE_SCHEMA)
.extend(cv.COMPONENT_SCHEMA),
cv.only_on([PLATFORM_ESP8266, PLATFORM_ESP32]),
)
FINAL_VALIDATE_SCHEMA = uart.final_validate_device_schema(
"dlms_meter", baud_rate=2400, require_rx=True
)
async def to_code(config):
var = cg.new_Pvariable(config[CONF_ID])
await cg.register_component(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}}}")))
cg.add(var.set_provider(PROVIDERS[config[CONF_PROVIDER]]))

View File

@@ -0,0 +1,71 @@
#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

View File

@@ -0,0 +1,468 @@
#include "dlms_meter.h"
#include <cmath>
#if defined(USE_ESP8266_FRAMEWORK_ARDUINO)
#include <bearssl/bearssl.h>
#elif defined(USE_ESP32)
#include "mbedtls/esp_config.h"
#include "mbedtls/gcm.h"
#endif
namespace esphome::dlms_meter {
static constexpr const char *TAG = "dlms_meter";
void DlmsMeterComponent::dump_config() {
const char *provider_name = this->provider_ == PROVIDER_NETZNOE ? "Netz NOE" : "Generic";
ESP_LOGCONFIG(TAG,
"DLMS Meter:\n"
" Provider: %s\n"
" Read Timeout: %u ms",
provider_name, this->read_timeout_);
#define DLMS_METER_LOG_SENSOR(s) LOG_SENSOR(" ", #s, this->s##_sensor_);
DLMS_METER_SENSOR_LIST(DLMS_METER_LOG_SENSOR, )
#define DLMS_METER_LOG_TEXT_SENSOR(s) LOG_TEXT_SENSOR(" ", #s, this->s##_text_sensor_);
DLMS_METER_TEXT_SENSOR_LIST(DLMS_METER_LOG_TEXT_SENSOR, )
}
void DlmsMeterComponent::loop() {
// Read while data is available, netznoe uses two frames so allow 2x max frame length
while (this->available()) {
if (this->receive_buffer_.size() >= MBUS_MAX_FRAME_LENGTH * 2) {
ESP_LOGW(TAG, "Receive buffer full, dropping remaining bytes");
break;
}
uint8_t c;
this->read_byte(&c);
this->receive_buffer_.push_back(c);
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) {
ESP_LOGV(TAG, "Parsing M-Bus frames");
uint16_t frame_offset = 0; // Offset is used if the M-Bus message is split into multiple frames
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,
uint8_t &systitle_length, uint16_t &header_offset) {
ESP_LOGV(TAG, "Parsing DLMS header");
if (mbus_payload.size() < DLMS_HEADER_LENGTH + DLMS_HEADER_EXT_OFFSET) {
ESP_LOGE(TAG, "DLMS: Payload too short");
this->receive_buffer_.clear();
return false;
}
if (mbus_payload[DLMS_CIPHER_OFFSET] != GLO_CIPHERING) { // Only general-glo-ciphering is supported (0xDB)
ESP_LOGE(TAG, "DLMS: Unsupported cipher");
this->receive_buffer_.clear();
return false;
}
systitle_length = mbus_payload[DLMS_SYST_OFFSET];
if (systitle_length != 0x08) { // Only system titles with length of 8 are supported
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,
uint16_t header_offset) {
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];
#if defined(USE_ESP8266_FRAMEWORK_ARDUINO)
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)
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;
}
#else
#error "Invalid Platform"
#endif
if (payload_ptr[0] != DATA_NOTIFICATION || payload_ptr[5] != TIMESTAMP_DATETIME) {
ESP_LOGE(TAG, "OBIS: Packet was decrypted but data is invalid");
this->receive_buffer_.clear();
return false;
}
ESP_LOGV(TAG, "Decrypted payload: %d bytes", message_length);
return true;
}
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 *= powf(10.0f, 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);
}
}
}
this->receive_buffer_.clear();
ESP_LOGI(TAG, "Received valid data");
this->publish_sensors(data);
this->status_clear_warning();
}
} // namespace esphome::dlms_meter

View File

@@ -0,0 +1,96 @@
#pragma once
#include "esphome/core/component.h"
#include "esphome/core/defines.h"
#include "esphome/core/log.h"
#ifdef USE_SENSOR
#include "esphome/components/sensor/sensor.h"
#endif
#ifdef USE_TEXT_SENSOR
#include "esphome/components/text_sensor/text_sensor.h"
#endif
#include "esphome/components/uart/uart.h"
#include "mbus.h"
#include "dlms.h"
#include "obis.h"
#include <array>
#include <vector>
namespace esphome::dlms_meter {
#ifndef DLMS_METER_SENSOR_LIST
#define DLMS_METER_SENSOR_LIST(F, SEP)
#endif
#ifndef DLMS_METER_TEXT_SENSOR_LIST
#define DLMS_METER_TEXT_SENSOR_LIST(F, SEP)
#endif
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
};
// Provider constants
enum Providers : uint32_t { PROVIDER_GENERIC = 0x00, PROVIDER_NETZNOE = 0x01 };
class DlmsMeterComponent : public Component, public uart::UARTDevice {
public:
DlmsMeterComponent() = default;
void dump_config() override;
void loop() override;
void set_decryption_key(const std::array<uint8_t, 16> &key) { this->decryption_key_ = key; }
void set_provider(uint32_t provider) { this->provider_ = provider; }
void publish_sensors(MeterData &data) {
#define DLMS_METER_PUBLISH_SENSOR(s) \
if (this->s##_sensor_ != nullptr) \
s##_sensor_->publish_state(data.s);
DLMS_METER_SENSOR_LIST(DLMS_METER_PUBLISH_SENSOR, )
#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:
bool parse_mbus_(std::vector<uint8_t> &mbus_payload);
bool parse_dlms_(const std::vector<uint8_t> &mbus_payload, uint16_t &message_length, uint8_t &systitle_length,
uint16_t &header_offset);
bool decrypt_(std::vector<uint8_t> &mbus_payload, uint16_t message_length, uint8_t systitle_length,
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::vector<uint8_t> mbus_payload_; // Parsed M-Bus payload, reused to avoid heap churn
uint32_t last_read_ = 0; // Timestamp when data was last read
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
std::array<uint8_t, 16> decryption_key_;
};
} // namespace esphome::dlms_meter

View File

@@ -0,0 +1,69 @@
#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

View File

@@ -0,0 +1,94 @@
#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

View File

@@ -0,0 +1,124 @@
import esphome.codegen as cg
from esphome.components import sensor
import esphome.config_validation as cv
from esphome.const import (
CONF_ID,
DEVICE_CLASS_CURRENT,
DEVICE_CLASS_ENERGY,
DEVICE_CLASS_POWER,
DEVICE_CLASS_POWER_FACTOR,
DEVICE_CLASS_VOLTAGE,
STATE_CLASS_MEASUREMENT,
STATE_CLASS_TOTAL_INCREASING,
UNIT_AMPERE,
UNIT_VOLT,
UNIT_WATT,
UNIT_WATT_HOURS,
)
from .. import CONF_DLMS_METER_ID, DlmsMeterComponent
AUTO_LOAD = ["dlms_meter"]
CONFIG_SCHEMA = 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,
),
# 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)
async def to_code(config):
hub = await cg.get_variable(config[CONF_DLMS_METER_ID])
sensors = []
for key, conf in config.items():
if not isinstance(conf, dict):
continue
id = conf[CONF_ID]
if id and id.type == sensor.Sensor:
sens = await sensor.new_sensor(conf)
cg.add(getattr(hub, f"set_{key}_sensor")(sens))
sensors.append(f"F({key})")
if sensors:
cg.add_define(
"DLMS_METER_SENSOR_LIST(F, sep)", cg.RawExpression(" sep ".join(sensors))
)

View File

@@ -0,0 +1,37 @@
import esphome.codegen as cg
from esphome.components import text_sensor
import esphome.config_validation as cv
from esphome.const import CONF_ID
from .. import CONF_DLMS_METER_ID, DlmsMeterComponent
AUTO_LOAD = ["dlms_meter"]
CONFIG_SCHEMA = cv.Schema(
{
cv.GenerateID(CONF_DLMS_METER_ID): cv.use_id(DlmsMeterComponent),
cv.Optional("timestamp"): text_sensor.text_sensor_schema(),
# Netz NOE
cv.Optional("meternumber"): text_sensor.text_sensor_schema(),
}
).extend(cv.COMPONENT_SCHEMA)
async def to_code(config):
hub = await cg.get_variable(config[CONF_DLMS_METER_ID])
text_sensors = []
for key, conf in config.items():
if not isinstance(conf, dict):
continue
id = conf[CONF_ID]
if id and id.type == text_sensor.TextSensor:
sens = await text_sensor.new_text_sensor(conf)
cg.add(getattr(hub, f"set_{key}_text_sensor")(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)),
)

View File

@@ -2,7 +2,8 @@ import esphome.codegen as cg
from esphome.components import i2c from esphome.components import i2c
from esphome.components.audio_dac import AudioDac from esphome.components.audio_dac import AudioDac
import esphome.config_validation as cv import esphome.config_validation as cv
from esphome.const import CONF_ID from esphome.const import CONF_AUDIO_DAC, CONF_BITS_PER_SAMPLE, CONF_ID
import esphome.final_validate as fv
CODEOWNERS = ["@kbx81"] CODEOWNERS = ["@kbx81"]
DEPENDENCIES = ["i2c"] DEPENDENCIES = ["i2c"]
@@ -21,6 +22,29 @@ CONFIG_SCHEMA = (
) )
def _final_validate(config):
full_config = fv.full_config.get()
# Check all speaker configurations for ones that reference this es8156
speaker_configs = full_config.get("speaker", [])
for speaker_config in speaker_configs:
audio_dac_id = speaker_config.get(CONF_AUDIO_DAC)
if (
audio_dac_id is not None
and audio_dac_id == config[CONF_ID]
and (bits_per_sample := speaker_config.get(CONF_BITS_PER_SAMPLE))
is not None
and bits_per_sample > 24
):
raise cv.Invalid(
f"ES8156 does not support more than 24 bits per sample. "
f"The speaker referencing this audio_dac has bits_per_sample set to {bits_per_sample}."
)
FINAL_VALIDATE_SCHEMA = _final_validate
async def to_code(config): async def to_code(config):
var = cg.new_Pvariable(config[CONF_ID]) var = cg.new_Pvariable(config[CONF_ID])
await cg.register_component(var, config) await cg.register_component(var, config)

View File

@@ -17,24 +17,61 @@ static const char *const TAG = "es8156";
} }
void ES8156::setup() { void ES8156::setup() {
// REG02 MODE CONFIG 1: Enable software mode for I2C control of volume/mute
// Bit 2: SOFT_MODE_SEL=1 (software mode enabled)
ES8156_ERROR_FAILED(this->write_byte(ES8156_REG02_SCLK_MODE, 0x04)); ES8156_ERROR_FAILED(this->write_byte(ES8156_REG02_SCLK_MODE, 0x04));
// Analog system configuration (active-low power down bits, active-high enables)
// REG20 ANALOG SYSTEM: Configure analog signal path
ES8156_ERROR_FAILED(this->write_byte(ES8156_REG20_ANALOG_SYS1, 0x2A)); ES8156_ERROR_FAILED(this->write_byte(ES8156_REG20_ANALOG_SYS1, 0x2A));
// REG21 ANALOG SYSTEM: VSEL=0x1C (bias level ~120%), normal VREF ramp speed
ES8156_ERROR_FAILED(this->write_byte(ES8156_REG21_ANALOG_SYS2, 0x3C)); ES8156_ERROR_FAILED(this->write_byte(ES8156_REG21_ANALOG_SYS2, 0x3C));
// REG22 ANALOG SYSTEM: Line out mode (HPSW=0), OUT_MUTE=0 (not muted)
ES8156_ERROR_FAILED(this->write_byte(ES8156_REG22_ANALOG_SYS3, 0x00)); ES8156_ERROR_FAILED(this->write_byte(ES8156_REG22_ANALOG_SYS3, 0x00));
// REG24 ANALOG SYSTEM: Low power mode for VREFBUF, HPCOM, DACVRP; DAC normal power
// Bits 2:0 = 0x07: LPVREFBUF=1, LPHPCOM=1, LPDACVRP=1, LPDAC=0
ES8156_ERROR_FAILED(this->write_byte(ES8156_REG24_ANALOG_LP, 0x07)); ES8156_ERROR_FAILED(this->write_byte(ES8156_REG24_ANALOG_LP, 0x07));
// REG23 ANALOG SYSTEM: Lowest bias (IBIAS_SW=0), VMIDLVL=VDDA/2, normal impedance
ES8156_ERROR_FAILED(this->write_byte(ES8156_REG23_ANALOG_SYS4, 0x00)); ES8156_ERROR_FAILED(this->write_byte(ES8156_REG23_ANALOG_SYS4, 0x00));
// Timing and interface configuration
// REG0A/0B TIME CONTROL: Fast state machine transitions
ES8156_ERROR_FAILED(this->write_byte(ES8156_REG0A_TIME_CONTROL1, 0x01)); ES8156_ERROR_FAILED(this->write_byte(ES8156_REG0A_TIME_CONTROL1, 0x01));
ES8156_ERROR_FAILED(this->write_byte(ES8156_REG0B_TIME_CONTROL2, 0x01)); ES8156_ERROR_FAILED(this->write_byte(ES8156_REG0B_TIME_CONTROL2, 0x01));
// REG11 SDP INTERFACE CONFIG: Default I2S format (24-bit, I2S mode)
ES8156_ERROR_FAILED(this->write_byte(ES8156_REG11_DAC_SDP, 0x00)); ES8156_ERROR_FAILED(this->write_byte(ES8156_REG11_DAC_SDP, 0x00));
// REG19 EQ CONTROL 1: EQ disabled (EQ_ON=0), EQ_BAND_NUM=2
ES8156_ERROR_FAILED(this->write_byte(ES8156_REG19_EQ_CONTROL1, 0x20)); ES8156_ERROR_FAILED(this->write_byte(ES8156_REG19_EQ_CONTROL1, 0x20));
// REG0D P2S CONTROL: Parallel-to-serial converter settings
ES8156_ERROR_FAILED(this->write_byte(ES8156_REG0D_P2S_CONTROL, 0x14)); ES8156_ERROR_FAILED(this->write_byte(ES8156_REG0D_P2S_CONTROL, 0x14));
// REG09 MISC CONTROL 2: Default settings
ES8156_ERROR_FAILED(this->write_byte(ES8156_REG09_MISC_CONTROL2, 0x00)); ES8156_ERROR_FAILED(this->write_byte(ES8156_REG09_MISC_CONTROL2, 0x00));
// REG18 MISC CONTROL 3: Stereo channel routing, no inversion
// Bits 5:4 CHN_CROSS: 0=L→L/R→R, 1=L to both, 2=R to both, 3=swap L/R
// Bits 3:2: LCH_INV/RCH_INV channel inversion
ES8156_ERROR_FAILED(this->write_byte(ES8156_REG18_MISC_CONTROL3, 0x00)); ES8156_ERROR_FAILED(this->write_byte(ES8156_REG18_MISC_CONTROL3, 0x00));
// REG08 CLOCK OFF: Enable all internal clocks (0x3F = all clock gates open)
ES8156_ERROR_FAILED(this->write_byte(ES8156_REG08_CLOCK_ON_OFF, 0x3F)); ES8156_ERROR_FAILED(this->write_byte(ES8156_REG08_CLOCK_ON_OFF, 0x3F));
// REG00 RESET CONTROL: Reset sequence
// First: RST_DIG=1 (assert digital reset)
ES8156_ERROR_FAILED(this->write_byte(ES8156_REG00_RESET, 0x02)); ES8156_ERROR_FAILED(this->write_byte(ES8156_REG00_RESET, 0x02));
// Then: CSM_ON=1 (enable chip state machine), RST_DIG=1
ES8156_ERROR_FAILED(this->write_byte(ES8156_REG00_RESET, 0x03)); ES8156_ERROR_FAILED(this->write_byte(ES8156_REG00_RESET, 0x03));
// REG25 ANALOG SYSTEM: Power up analog blocks
// VMIDSEL=2 (normal VMID operation), PDN_ANA=0, ENREFR=0, ENHPCOM=0
// PDN_DACVREFGEN=0, PDN_VREFBUF=0, PDN_DAC=0 (all enabled)
ES8156_ERROR_FAILED(this->write_byte(ES8156_REG25_ANALOG_SYS5, 0x20)); ES8156_ERROR_FAILED(this->write_byte(ES8156_REG25_ANALOG_SYS5, 0x20));
} }

View File

@@ -53,6 +53,7 @@ from .const import ( # noqa
KEY_BOARD, KEY_BOARD,
KEY_COMPONENTS, KEY_COMPONENTS,
KEY_ESP32, KEY_ESP32,
KEY_EXCLUDE_COMPONENTS,
KEY_EXTRA_BUILD_FILES, KEY_EXTRA_BUILD_FILES,
KEY_FLASH_SIZE, KEY_FLASH_SIZE,
KEY_FULL_CERT_BUNDLE, KEY_FULL_CERT_BUNDLE,
@@ -86,6 +87,7 @@ IS_TARGET_PLATFORM = True
CONF_ASSERTION_LEVEL = "assertion_level" CONF_ASSERTION_LEVEL = "assertion_level"
CONF_COMPILER_OPTIMIZATION = "compiler_optimization" CONF_COMPILER_OPTIMIZATION = "compiler_optimization"
CONF_ENABLE_IDF_EXPERIMENTAL_FEATURES = "enable_idf_experimental_features" CONF_ENABLE_IDF_EXPERIMENTAL_FEATURES = "enable_idf_experimental_features"
CONF_INCLUDE_BUILTIN_IDF_COMPONENTS = "include_builtin_idf_components"
CONF_ENABLE_LWIP_ASSERT = "enable_lwip_assert" CONF_ENABLE_LWIP_ASSERT = "enable_lwip_assert"
CONF_ENABLE_OTA_ROLLBACK = "enable_ota_rollback" CONF_ENABLE_OTA_ROLLBACK = "enable_ota_rollback"
CONF_EXECUTE_FROM_PSRAM = "execute_from_psram" CONF_EXECUTE_FROM_PSRAM = "execute_from_psram"
@@ -114,6 +116,36 @@ COMPILER_OPTIMIZATIONS = {
"SIZE": "CONFIG_COMPILER_OPTIMIZATION_SIZE", "SIZE": "CONFIG_COMPILER_OPTIMIZATION_SIZE",
} }
# ESP-IDF components excluded by default to reduce compile time.
# Components can be re-enabled by calling include_builtin_idf_component() in to_code().
#
# Cannot be excluded (dependencies of required components):
# - "console": espressif/mdns unconditionally depends on it
# - "sdmmc": driver -> esp_driver_sdmmc -> sdmmc dependency chain
DEFAULT_EXCLUDED_IDF_COMPONENTS = (
"cmock", # Unit testing mock framework - ESPHome doesn't use IDF's testing
"esp_adc", # ADC driver - only needed by adc component
"esp_driver_i2s", # I2S driver - only needed by i2s_audio component
"esp_driver_rmt", # RMT driver - only needed by remote_transmitter/receiver, neopixelbus
"esp_driver_touch_sens", # Touch sensor driver - only needed by esp32_touch
"esp_eth", # Ethernet driver - only needed by ethernet component
"esp_hid", # HID host/device support - ESPHome doesn't implement HID functionality
"esp_http_client", # HTTP client - only needed by http_request component
"esp_https_ota", # ESP-IDF HTTPS OTA - ESPHome has its own OTA implementation
"esp_https_server", # HTTPS server - ESPHome has its own web server
"esp_lcd", # LCD controller drivers - only needed by display component
"esp_local_ctrl", # Local control over HTTPS/BLE - ESPHome has native API
"espcoredump", # Core dump support - ESPHome has its own debug component
"fatfs", # FAT filesystem - ESPHome doesn't use filesystem storage
"mqtt", # ESP-IDF MQTT library - ESPHome has its own MQTT implementation
"perfmon", # Xtensa performance monitor - ESPHome has its own debug component
"protocomm", # Protocol communication for provisioning - unused by ESPHome
"spiffs", # SPIFFS filesystem - ESPHome doesn't use filesystem storage (IDF only)
"unity", # Unit testing framework - ESPHome doesn't use IDF's testing
"wear_levelling", # Flash wear levelling for fatfs - unused since fatfs unused
"wifi_provisioning", # WiFi provisioning - ESPHome uses its own improv implementation
)
# ESP32 (original) chip revision options # ESP32 (original) chip revision options
# Setting minimum revision to 3.0 or higher: # Setting minimum revision to 3.0 or higher:
# - Reduces flash size by excluding workaround code for older chip bugs # - Reduces flash size by excluding workaround code for older chip bugs
@@ -203,6 +235,9 @@ def set_core_data(config):
) )
CORE.data[KEY_ESP32][KEY_SDKCONFIG_OPTIONS] = {} CORE.data[KEY_ESP32][KEY_SDKCONFIG_OPTIONS] = {}
CORE.data[KEY_ESP32][KEY_COMPONENTS] = {} CORE.data[KEY_ESP32][KEY_COMPONENTS] = {}
# Initialize with default exclusions - components can call include_builtin_idf_component()
# to re-enable any they need
CORE.data[KEY_ESP32][KEY_EXCLUDE_COMPONENTS] = set(DEFAULT_EXCLUDED_IDF_COMPONENTS)
CORE.data[KEY_CORE][KEY_FRAMEWORK_VERSION] = cv.Version.parse( CORE.data[KEY_CORE][KEY_FRAMEWORK_VERSION] = cv.Version.parse(
config[CONF_FRAMEWORK][CONF_VERSION] config[CONF_FRAMEWORK][CONF_VERSION]
) )
@@ -328,6 +363,28 @@ def add_idf_component(
} }
def exclude_builtin_idf_component(name: str) -> None:
"""Exclude an ESP-IDF component from the build.
This reduces compile time by skipping components that are not needed.
The component will be passed to ESP-IDF's EXCLUDE_COMPONENTS cmake variable.
Note: Components that are dependencies of other required components
cannot be excluded - ESP-IDF will still build them.
"""
CORE.data[KEY_ESP32][KEY_EXCLUDE_COMPONENTS].add(name)
def include_builtin_idf_component(name: str) -> None:
"""Remove an ESP-IDF component from the exclusion list.
Call this from components that need an ESP-IDF component that is
excluded by default in DEFAULT_EXCLUDED_IDF_COMPONENTS. This ensures the
component will be built when needed.
"""
CORE.data[KEY_ESP32][KEY_EXCLUDE_COMPONENTS].discard(name)
def add_extra_script(stage: str, filename: str, path: Path): def add_extra_script(stage: str, filename: str, path: Path):
"""Add an extra script to the project.""" """Add an extra script to the project."""
key = f"{stage}:{filename}" key = f"{stage}:{filename}"
@@ -672,11 +729,26 @@ CONF_RINGBUF_IN_IRAM = "ringbuf_in_iram"
CONF_HEAP_IN_IRAM = "heap_in_iram" CONF_HEAP_IN_IRAM = "heap_in_iram"
CONF_LOOP_TASK_STACK_SIZE = "loop_task_stack_size" CONF_LOOP_TASK_STACK_SIZE = "loop_task_stack_size"
CONF_USE_FULL_CERTIFICATE_BUNDLE = "use_full_certificate_bundle" CONF_USE_FULL_CERTIFICATE_BUNDLE = "use_full_certificate_bundle"
CONF_DISABLE_DEBUG_STUBS = "disable_debug_stubs"
CONF_DISABLE_OCD_AWARE = "disable_ocd_aware"
CONF_DISABLE_USB_SERIAL_JTAG_SECONDARY = "disable_usb_serial_jtag_secondary"
CONF_DISABLE_DEV_NULL_VFS = "disable_dev_null_vfs"
CONF_DISABLE_MBEDTLS_PEER_CERT = "disable_mbedtls_peer_cert"
CONF_DISABLE_MBEDTLS_PKCS7 = "disable_mbedtls_pkcs7"
CONF_DISABLE_REGI2C_IN_IRAM = "disable_regi2c_in_iram"
CONF_DISABLE_FATFS = "disable_fatfs"
# VFS requirement tracking # VFS requirement tracking
# Components that need VFS features can call require_vfs_select() or require_vfs_dir() # Components that need VFS features can call require_vfs_*() functions
KEY_VFS_SELECT_REQUIRED = "vfs_select_required" KEY_VFS_SELECT_REQUIRED = "vfs_select_required"
KEY_VFS_DIR_REQUIRED = "vfs_dir_required" KEY_VFS_DIR_REQUIRED = "vfs_dir_required"
KEY_VFS_TERMIOS_REQUIRED = "vfs_termios_required"
# Feature requirement tracking - components can call require_* functions to re-enable
# These are stored in CORE.data[KEY_ESP32] dict
KEY_USB_SERIAL_JTAG_SECONDARY_REQUIRED = "usb_serial_jtag_secondary_required"
KEY_MBEDTLS_PEER_CERT_REQUIRED = "mbedtls_peer_cert_required"
KEY_MBEDTLS_PKCS7_REQUIRED = "mbedtls_pkcs7_required"
KEY_FATFS_REQUIRED = "fatfs_required"
def require_vfs_select() -> None: def require_vfs_select() -> None:
@@ -697,6 +769,15 @@ def require_vfs_dir() -> None:
CORE.data[KEY_VFS_DIR_REQUIRED] = True CORE.data[KEY_VFS_DIR_REQUIRED] = True
def require_vfs_termios() -> None:
"""Mark that VFS termios support is required by a component.
Call this from components that use terminal I/O functions (usb_serial_jtag_vfs_*, etc.).
This prevents CONFIG_VFS_SUPPORT_TERMIOS from being disabled.
"""
CORE.data[KEY_VFS_TERMIOS_REQUIRED] = True
def require_full_certificate_bundle() -> None: def require_full_certificate_bundle() -> None:
"""Request the full certificate bundle instead of the common-CAs-only bundle. """Request the full certificate bundle instead of the common-CAs-only bundle.
@@ -709,6 +790,43 @@ def require_full_certificate_bundle() -> None:
CORE.data[KEY_ESP32][KEY_FULL_CERT_BUNDLE] = True CORE.data[KEY_ESP32][KEY_FULL_CERT_BUNDLE] = True
def require_usb_serial_jtag_secondary() -> None:
"""Mark that USB Serial/JTAG secondary console is required by a component.
Call this from components (e.g., logger) that need USB Serial/JTAG console output.
This prevents CONFIG_ESP_CONSOLE_SECONDARY_USB_SERIAL_JTAG from being disabled.
"""
CORE.data[KEY_ESP32][KEY_USB_SERIAL_JTAG_SECONDARY_REQUIRED] = True
def require_mbedtls_peer_cert() -> None:
"""Mark that mbedTLS peer certificate retention is required by a component.
Call this from components that need access to the peer certificate after
the TLS handshake is complete. This prevents CONFIG_MBEDTLS_SSL_KEEP_PEER_CERTIFICATE
from being disabled.
"""
CORE.data[KEY_ESP32][KEY_MBEDTLS_PEER_CERT_REQUIRED] = True
def require_mbedtls_pkcs7() -> None:
"""Mark that mbedTLS PKCS#7 support is required by a component.
Call this from components that need PKCS#7 certificate validation.
This prevents CONFIG_MBEDTLS_PKCS7_C from being disabled.
"""
CORE.data[KEY_ESP32][KEY_MBEDTLS_PKCS7_REQUIRED] = True
def require_fatfs() -> None:
"""Mark that FATFS support is required by a component.
Call this from components that use FATFS (e.g., SD card, storage components).
This prevents FATFS from being disabled when disable_fatfs is set.
"""
CORE.data[KEY_ESP32][KEY_FATFS_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 *)
@@ -793,6 +911,19 @@ FRAMEWORK_SCHEMA = cv.Schema(
cv.Optional( cv.Optional(
CONF_USE_FULL_CERTIFICATE_BUNDLE, default=False CONF_USE_FULL_CERTIFICATE_BUNDLE, default=False
): cv.boolean, ): cv.boolean,
cv.Optional(
CONF_INCLUDE_BUILTIN_IDF_COMPONENTS, default=[]
): cv.ensure_list(cv.string_strict),
cv.Optional(CONF_DISABLE_DEBUG_STUBS, default=True): cv.boolean,
cv.Optional(CONF_DISABLE_OCD_AWARE, default=True): cv.boolean,
cv.Optional(
CONF_DISABLE_USB_SERIAL_JTAG_SECONDARY, default=True
): cv.boolean,
cv.Optional(CONF_DISABLE_DEV_NULL_VFS, default=True): cv.boolean,
cv.Optional(CONF_DISABLE_MBEDTLS_PEER_CERT, default=True): cv.boolean,
cv.Optional(CONF_DISABLE_MBEDTLS_PKCS7, default=True): cv.boolean,
cv.Optional(CONF_DISABLE_REGI2C_IN_IRAM, default=True): cv.boolean,
cv.Optional(CONF_DISABLE_FATFS, default=True): cv.boolean,
} }
), ),
cv.Optional(CONF_COMPONENTS, default=[]): cv.ensure_list( cv.Optional(CONF_COMPONENTS, default=[]): cv.ensure_list(
@@ -982,6 +1113,19 @@ def _configure_lwip_max_sockets(conf: dict) -> None:
add_idf_sdkconfig_option("CONFIG_LWIP_MAX_SOCKETS", max_sockets) add_idf_sdkconfig_option("CONFIG_LWIP_MAX_SOCKETS", max_sockets)
@coroutine_with_priority(CoroPriority.FINAL)
async def _write_exclude_components() -> None:
"""Write EXCLUDE_COMPONENTS cmake arg after all components have registered exclusions."""
if KEY_ESP32 not in CORE.data:
return
excluded = CORE.data[KEY_ESP32].get(KEY_EXCLUDE_COMPONENTS)
if excluded:
exclude_list = ";".join(sorted(excluded))
cg.add_platformio_option(
"board_build.cmake_extra_args", f"-DEXCLUDE_COMPONENTS={exclude_list}"
)
@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."""
@@ -1185,6 +1329,10 @@ async def to_code(config):
# Disable dynamic log level control to save memory # Disable dynamic log level control to save memory
add_idf_sdkconfig_option("CONFIG_LOG_DYNAMIC_LEVEL_CONTROL", False) add_idf_sdkconfig_option("CONFIG_LOG_DYNAMIC_LEVEL_CONTROL", False)
# Disable per-tag log level filtering since dynamic level control is disabled above
# This saves ~250 bytes of RAM (tag cache) and associated code
add_idf_sdkconfig_option("CONFIG_LOG_TAG_LEVEL_IMPL_NONE", True)
# Reduce PHY TX power in the event of a brownout # Reduce PHY TX power in the event of a brownout
add_idf_sdkconfig_option("CONFIG_ESP_PHY_REDUCE_TX_POWER", True) add_idf_sdkconfig_option("CONFIG_ESP_PHY_REDUCE_TX_POWER", True)
@@ -1195,6 +1343,11 @@ async def to_code(config):
# Apply LWIP optimization settings # Apply LWIP optimization settings
advanced = conf[CONF_ADVANCED] advanced = conf[CONF_ADVANCED]
# Re-include any IDF components the user explicitly requested
for component_name in advanced.get(CONF_INCLUDE_BUILTIN_IDF_COMPONENTS, []):
include_builtin_idf_component(component_name)
# DHCP server: only disable if explicitly set to false # DHCP server: only disable if explicitly set to false
# WiFi component handles its own optimization when AP mode is not used # WiFi component handles its own optimization when AP mode is not used
# When using Arduino with Ethernet, DHCP server functions must be available # When using Arduino with Ethernet, DHCP server functions must be available
@@ -1233,11 +1386,18 @@ async def to_code(config):
add_idf_sdkconfig_option("CONFIG_LIBC_LOCKS_PLACE_IN_IRAM", False) add_idf_sdkconfig_option("CONFIG_LIBC_LOCKS_PLACE_IN_IRAM", False)
# Disable VFS support for termios (terminal I/O functions) # Disable VFS support for termios (terminal I/O functions)
# ESPHome doesn't use termios functions on ESP32 (only used in host UART driver). # USB Serial JTAG VFS functions require termios support.
# Components that need it (e.g., logger when USB_SERIAL_JTAG is supported but not selected
# as the logger output) call require_vfs_termios().
# Saves approximately 1.8KB of flash when disabled (default). # Saves approximately 1.8KB of flash when disabled (default).
add_idf_sdkconfig_option( if CORE.data.get(KEY_VFS_TERMIOS_REQUIRED, False):
"CONFIG_VFS_SUPPORT_TERMIOS", not advanced[CONF_DISABLE_VFS_SUPPORT_TERMIOS] # Component requires VFS termios - force enable regardless of user setting
) add_idf_sdkconfig_option("CONFIG_VFS_SUPPORT_TERMIOS", True)
else:
# No component needs it - allow user to control (default: disabled)
add_idf_sdkconfig_option(
"CONFIG_VFS_SUPPORT_TERMIOS", not advanced[CONF_DISABLE_VFS_SUPPORT_TERMIOS]
)
# Disable VFS support for select() with file descriptors # Disable VFS support for select() with file descriptors
# ESPHome only uses select() with sockets via lwip_select(), which still works. # ESPHome only uses select() with sockets via lwip_select(), which still works.
@@ -1316,6 +1476,61 @@ async def to_code(config):
add_idf_sdkconfig_option(f"CONFIG_LOG_DEFAULT_LEVEL_{conf[CONF_LOG_LEVEL]}", True) add_idf_sdkconfig_option(f"CONFIG_LOG_DEFAULT_LEVEL_{conf[CONF_LOG_LEVEL]}", True)
# Disable OpenOCD debug stubs to save code size
# These are used for on-chip debugging with OpenOCD/JTAG, rarely needed for ESPHome
if advanced[CONF_DISABLE_DEBUG_STUBS]:
add_idf_sdkconfig_option("CONFIG_ESP_DEBUG_STUBS_ENABLE", False)
# Disable OCD-aware exception handlers
# When enabled, the panic handler detects JTAG debugger and halts instead of resetting
# Most ESPHome users don't use JTAG debugging
if advanced[CONF_DISABLE_OCD_AWARE]:
add_idf_sdkconfig_option("CONFIG_ESP_DEBUG_OCDAWARE", False)
# Disable USB Serial/JTAG secondary console
# Components like logger can call require_usb_serial_jtag_secondary() to re-enable
if CORE.data[KEY_ESP32].get(KEY_USB_SERIAL_JTAG_SECONDARY_REQUIRED, False):
add_idf_sdkconfig_option("CONFIG_ESP_CONSOLE_SECONDARY_USB_SERIAL_JTAG", True)
elif advanced[CONF_DISABLE_USB_SERIAL_JTAG_SECONDARY]:
add_idf_sdkconfig_option("CONFIG_ESP_CONSOLE_SECONDARY_NONE", True)
# Disable /dev/null VFS initialization
# ESPHome doesn't typically need /dev/null
if advanced[CONF_DISABLE_DEV_NULL_VFS]:
add_idf_sdkconfig_option("CONFIG_VFS_INITIALIZE_DEV_NULL", False)
# Disable keeping peer certificate after TLS handshake
# Saves ~4KB heap per connection, but prevents certificate inspection after handshake
# Components that need it can call require_mbedtls_peer_cert()
if CORE.data[KEY_ESP32].get(KEY_MBEDTLS_PEER_CERT_REQUIRED, False):
add_idf_sdkconfig_option("CONFIG_MBEDTLS_SSL_KEEP_PEER_CERTIFICATE", True)
elif advanced[CONF_DISABLE_MBEDTLS_PEER_CERT]:
add_idf_sdkconfig_option("CONFIG_MBEDTLS_SSL_KEEP_PEER_CERTIFICATE", False)
# Disable PKCS#7 support in mbedTLS
# Only needed for specific certificate validation scenarios
# Components that need it can call require_mbedtls_pkcs7()
if CORE.data[KEY_ESP32].get(KEY_MBEDTLS_PKCS7_REQUIRED, False):
# Component called require_mbedtls_pkcs7() - enable regardless of user setting
add_idf_sdkconfig_option("CONFIG_MBEDTLS_PKCS7_C", True)
elif advanced[CONF_DISABLE_MBEDTLS_PKCS7]:
add_idf_sdkconfig_option("CONFIG_MBEDTLS_PKCS7_C", False)
# Disable regi2c control functions in IRAM
# Only needed if using analog peripherals (ADC, DAC, etc.) from ISRs while cache is disabled
if advanced[CONF_DISABLE_REGI2C_IN_IRAM]:
add_idf_sdkconfig_option("CONFIG_ESP_REGI2C_CTRL_FUNC_IN_IRAM", False)
# Disable FATFS support
# Components that need FATFS (SD card, etc.) can call require_fatfs()
if CORE.data[KEY_ESP32].get(KEY_FATFS_REQUIRED, False):
# Component called require_fatfs() - enable regardless of user setting
add_idf_sdkconfig_option("CONFIG_FATFS_LFN_NONE", False)
add_idf_sdkconfig_option("CONFIG_FATFS_VOLUME_COUNT", 2)
elif advanced[CONF_DISABLE_FATFS]:
add_idf_sdkconfig_option("CONFIG_FATFS_LFN_NONE", True)
add_idf_sdkconfig_option("CONFIG_FATFS_VOLUME_COUNT", 0)
for name, value in conf[CONF_SDKCONFIG_OPTIONS].items(): for name, value in conf[CONF_SDKCONFIG_OPTIONS].items():
add_idf_sdkconfig_option(name, RawSdkconfigValue(value)) add_idf_sdkconfig_option(name, RawSdkconfigValue(value))
@@ -1324,6 +1539,11 @@ async def to_code(config):
if conf[CONF_COMPONENTS]: if conf[CONF_COMPONENTS]:
CORE.add_job(_add_yaml_idf_components, conf[CONF_COMPONENTS]) CORE.add_job(_add_yaml_idf_components, conf[CONF_COMPONENTS])
# Write EXCLUDE_COMPONENTS at FINAL priority after all components have had
# a chance to call include_builtin_idf_component() to re-enable components they need.
# Default exclusions are added in set_core_data() during config validation.
CORE.add_job(_write_exclude_components)
APP_PARTITION_SIZES = { APP_PARTITION_SIZES = {
"2MB": 0x0C0000, # 768 KB "2MB": 0x0C0000, # 768 KB

View File

@@ -6,6 +6,7 @@ KEY_FLASH_SIZE = "flash_size"
KEY_VARIANT = "variant" KEY_VARIANT = "variant"
KEY_SDKCONFIG_OPTIONS = "sdkconfig_options" KEY_SDKCONFIG_OPTIONS = "sdkconfig_options"
KEY_COMPONENTS = "components" KEY_COMPONENTS = "components"
KEY_EXCLUDE_COMPONENTS = "exclude_components"
KEY_REPO = "repo" KEY_REPO = "repo"
KEY_REF = "ref" KEY_REF = "ref"
KEY_REFRESH = "refresh" KEY_REFRESH = "refresh"

View File

@@ -5,6 +5,7 @@ from esphome import pins
import esphome.codegen as cg import esphome.codegen as cg
from esphome.components import esp32, light from esphome.components import esp32, light
from esphome.components.const import CONF_USE_PSRAM from esphome.components.const import CONF_USE_PSRAM
from esphome.components.esp32 import include_builtin_idf_component
import esphome.config_validation as cv import esphome.config_validation as cv
from esphome.const import ( from esphome.const import (
CONF_CHIPSET, CONF_CHIPSET,
@@ -129,6 +130,9 @@ CONFIG_SCHEMA = cv.All(
async def to_code(config): async def to_code(config):
# Re-enable ESP-IDF's RMT driver (excluded by default to save compile time)
include_builtin_idf_component("esp_driver_rmt")
var = cg.new_Pvariable(config[CONF_OUTPUT_ID]) var = cg.new_Pvariable(config[CONF_OUTPUT_ID])
await light.register_light(var, config) await light.register_light(var, config)
await cg.register_component(var, config) await cg.register_component(var, config)

View File

@@ -6,6 +6,7 @@ from esphome.components.esp32 import (
VARIANT_ESP32S3, VARIANT_ESP32S3,
get_esp32_variant, get_esp32_variant,
gpio, gpio,
include_builtin_idf_component,
) )
import esphome.config_validation as cv import esphome.config_validation as cv
from esphome.const import ( from esphome.const import (
@@ -266,6 +267,9 @@ CONFIG_SCHEMA = cv.All(
async def to_code(config): async def to_code(config):
# Re-enable ESP-IDF's touch sensor driver (excluded by default to save compile time)
include_builtin_idf_component("esp_driver_touch_sens")
touch = cg.new_Pvariable(config[CONF_ID]) touch = cg.new_Pvariable(config[CONF_ID])
await cg.register_component(touch, config) await cg.register_component(touch, config)

View File

@@ -14,6 +14,7 @@ from esphome.components.esp32 import (
add_idf_component, add_idf_component,
add_idf_sdkconfig_option, add_idf_sdkconfig_option,
get_esp32_variant, get_esp32_variant,
include_builtin_idf_component,
) )
from esphome.components.network import ip_address_literal from esphome.components.network import ip_address_literal
from esphome.components.spi import CONF_INTERFACE_INDEX, get_spi_interface from esphome.components.spi import CONF_INTERFACE_INDEX, get_spi_interface
@@ -419,6 +420,9 @@ async def to_code(config):
# Also disable WiFi/BT coexistence since WiFi is disabled # Also disable WiFi/BT coexistence since WiFi is disabled
add_idf_sdkconfig_option("CONFIG_SW_COEXIST_ENABLE", False) add_idf_sdkconfig_option("CONFIG_SW_COEXIST_ENABLE", False)
# Re-enable ESP-IDF's Ethernet driver (excluded by default to save compile time)
include_builtin_idf_component("esp_eth")
if config[CONF_TYPE] == "LAN8670": if config[CONF_TYPE] == "LAN8670":
# Add LAN867x 10BASE-T1S PHY support component # Add LAN867x 10BASE-T1S PHY support component
add_idf_component(name="espressif/lan867x", ref="2.0.0") add_idf_component(name="espressif/lan867x", ref="2.0.0")

View File

@@ -155,6 +155,9 @@ async def to_code(config):
cg.add(var.set_watchdog_timeout(timeout_ms)) cg.add(var.set_watchdog_timeout(timeout_ms))
if CORE.is_esp32: if CORE.is_esp32:
# Re-enable ESP-IDF's HTTP client (excluded by default to save compile time)
esp32.include_builtin_idf_component("esp_http_client")
cg.add(var.set_buffer_size_rx(config[CONF_BUFFER_SIZE_RX])) cg.add(var.set_buffer_size_rx(config[CONF_BUFFER_SIZE_RX]))
cg.add(var.set_buffer_size_tx(config[CONF_BUFFER_SIZE_TX])) cg.add(var.set_buffer_size_tx(config[CONF_BUFFER_SIZE_TX]))
cg.add(var.set_verify_ssl(config[CONF_VERIFY_SSL])) cg.add(var.set_verify_ssl(config[CONF_VERIFY_SSL]))

View File

@@ -11,12 +11,6 @@ namespace i2c {
static const char *const TAG = "i2c"; static const char *const TAG = "i2c";
void I2CBus::i2c_scan_() { void I2CBus::i2c_scan_() {
// suppress logs from the IDF I2C library during the scan
#if defined(USE_ESP32) && defined(USE_LOGGER)
auto previous = esp_log_level_get("*");
esp_log_level_set("*", ESP_LOG_NONE);
#endif
for (uint8_t address = 8; address != 120; address++) { for (uint8_t address = 8; address != 120; address++) {
auto err = write_readv(address, nullptr, 0, nullptr, 0); auto err = write_readv(address, nullptr, 0, nullptr, 0);
if (err == ERROR_OK) { if (err == ERROR_OK) {
@@ -27,9 +21,6 @@ void I2CBus::i2c_scan_() {
// it takes 16sec to scan on nrf52. It prevents board reset. // it takes 16sec to scan on nrf52. It prevents board reset.
arch_feed_wdt(); arch_feed_wdt();
} }
#if defined(USE_ESP32) && defined(USE_LOGGER)
esp_log_level_set("*", previous);
#endif
} }
ErrorCode I2CDevice::read_register(uint8_t a_register, uint8_t *data, size_t len) { ErrorCode I2CDevice::read_register(uint8_t a_register, uint8_t *data, size_t len) {

View File

@@ -1,6 +1,11 @@
from esphome import pins from esphome import pins
import esphome.codegen as cg import esphome.codegen as cg
from esphome.components.esp32 import ( from esphome.components.esp32 import (
add_idf_sdkconfig_option,
get_esp32_variant,
include_builtin_idf_component,
)
from esphome.components.esp32.const import (
VARIANT_ESP32, VARIANT_ESP32,
VARIANT_ESP32C3, VARIANT_ESP32C3,
VARIANT_ESP32C5, VARIANT_ESP32C5,
@@ -10,8 +15,6 @@ from esphome.components.esp32 import (
VARIANT_ESP32P4, VARIANT_ESP32P4,
VARIANT_ESP32S2, VARIANT_ESP32S2,
VARIANT_ESP32S3, VARIANT_ESP32S3,
add_idf_sdkconfig_option,
get_esp32_variant,
) )
import esphome.config_validation as cv import esphome.config_validation as cv
from esphome.const import CONF_BITS_PER_SAMPLE, CONF_CHANNEL, CONF_ID, CONF_SAMPLE_RATE from esphome.const import CONF_BITS_PER_SAMPLE, CONF_CHANNEL, CONF_ID, CONF_SAMPLE_RATE
@@ -272,6 +275,10 @@ FINAL_VALIDATE_SCHEMA = _final_validate
async def to_code(config): async def to_code(config):
var = cg.new_Pvariable(config[CONF_ID]) var = cg.new_Pvariable(config[CONF_ID])
await cg.register_component(var, config) await cg.register_component(var, config)
# Re-enable ESP-IDF's I2S driver (excluded by default to save compile time)
include_builtin_idf_component("esp_driver_i2s")
if use_legacy(): if use_legacy():
cg.add_define("USE_I2S_LEGACY") cg.add_define("USE_I2S_LEGACY")

View File

@@ -16,6 +16,8 @@ from esphome.components.esp32 import (
VARIANT_ESP32S3, VARIANT_ESP32S3,
add_idf_sdkconfig_option, add_idf_sdkconfig_option,
get_esp32_variant, get_esp32_variant,
require_usb_serial_jtag_secondary,
require_vfs_termios,
) )
from esphome.components.libretiny import get_libretiny_component, get_libretiny_family from esphome.components.libretiny import get_libretiny_component, get_libretiny_family
from esphome.components.libretiny.const import ( from esphome.components.libretiny.const import (
@@ -397,9 +399,15 @@ async def to_code(config):
elif config[CONF_HARDWARE_UART] == USB_SERIAL_JTAG: elif config[CONF_HARDWARE_UART] == USB_SERIAL_JTAG:
add_idf_sdkconfig_option("CONFIG_ESP_CONSOLE_USB_SERIAL_JTAG", True) add_idf_sdkconfig_option("CONFIG_ESP_CONSOLE_USB_SERIAL_JTAG", True)
cg.add_define("USE_LOGGER_UART_SELECTION_USB_SERIAL_JTAG") cg.add_define("USE_LOGGER_UART_SELECTION_USB_SERIAL_JTAG")
# Define platform support flags for components that need auto-detection
try: try:
uart_selection(USB_SERIAL_JTAG) uart_selection(USB_SERIAL_JTAG)
cg.add_define("USE_LOGGER_USB_SERIAL_JTAG") cg.add_define("USE_LOGGER_USB_SERIAL_JTAG")
# USB Serial JTAG code is compiled when platform supports it.
# Enable secondary USB serial JTAG console so the VFS functions are available.
if CORE.is_esp32 and config[CONF_HARDWARE_UART] != USB_SERIAL_JTAG:
require_usb_serial_jtag_secondary()
require_vfs_termios()
except cv.Invalid: except cv.Invalid:
pass pass
try: try:

View File

@@ -114,9 +114,6 @@ void Logger::pre_setup() {
global_logger = this; global_logger = this;
esp_log_set_vprintf(esp_idf_log_vprintf_); esp_log_set_vprintf(esp_idf_log_vprintf_);
if (ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE) {
esp_log_level_set("*", ESP_LOG_VERBOSE);
}
ESP_LOGI(TAG, "Log initialized"); ESP_LOGI(TAG, "Log initialized");
} }

View File

@@ -28,11 +28,10 @@ CONFIG_SCHEMA = (
async def to_code(config): async def to_code(config):
var = cg.new_Pvariable(config[CONF_ID]) var = cg.new_Pvariable(config[CONF_ID], config[CONF_NUM_CHIPS])
await spi.register_spi_device(var, config, write_only=True) await spi.register_spi_device(var, config, write_only=True)
await display.register_display(var, config) await display.register_display(var, config)
cg.add(var.set_num_chips(config[CONF_NUM_CHIPS]))
cg.add(var.set_intensity(config[CONF_INTENSITY])) cg.add(var.set_intensity(config[CONF_INTENSITY]))
cg.add(var.set_reverse(config[CONF_REVERSE_ENABLE])) cg.add(var.set_reverse(config[CONF_REVERSE_ENABLE]))

View File

@@ -3,8 +3,7 @@
#include "esphome/core/helpers.h" #include "esphome/core/helpers.h"
#include "esphome/core/log.h" #include "esphome/core/log.h"
namespace esphome { namespace esphome::max7219 {
namespace max7219 {
static const char *const TAG = "max7219"; static const char *const TAG = "max7219";
@@ -115,12 +114,14 @@ const uint8_t MAX7219_ASCII_TO_RAW[95] PROGMEM = {
}; };
float MAX7219Component::get_setup_priority() const { return setup_priority::PROCESSOR; } float MAX7219Component::get_setup_priority() const { return setup_priority::PROCESSOR; }
MAX7219Component::MAX7219Component(uint8_t num_chips) : num_chips_(num_chips) {
this->buffer_ = new uint8_t[this->num_chips_ * 8]; // NOLINT
memset(this->buffer_, 0, this->num_chips_ * 8);
}
void MAX7219Component::setup() { void MAX7219Component::setup() {
this->spi_setup(); this->spi_setup();
this->buffer_ = new uint8_t[this->num_chips_ * 8]; // NOLINT
for (uint8_t i = 0; i < this->num_chips_ * 8; i++)
this->buffer_[i] = 0;
// let's assume the user has all 8 digits connected, only important in daisy chained setups anyway // let's assume the user has all 8 digits connected, only important in daisy chained setups anyway
this->send_to_all_(MAX7219_REGISTER_SCAN_LIMIT, 7); this->send_to_all_(MAX7219_REGISTER_SCAN_LIMIT, 7);
// let's use our own ASCII -> led pattern encoding // let's use our own ASCII -> led pattern encoding
@@ -229,7 +230,6 @@ void MAX7219Component::set_intensity(uint8_t intensity) {
this->intensity_ = intensity; this->intensity_ = intensity;
} }
} }
void MAX7219Component::set_num_chips(uint8_t num_chips) { this->num_chips_ = num_chips; }
uint8_t MAX7219Component::strftime(uint8_t pos, const char *format, ESPTime time) { uint8_t MAX7219Component::strftime(uint8_t pos, const char *format, ESPTime time) {
char buffer[64]; char buffer[64];
@@ -240,5 +240,4 @@ uint8_t MAX7219Component::strftime(uint8_t pos, const char *format, ESPTime time
} }
uint8_t MAX7219Component::strftime(const char *format, ESPTime time) { return this->strftime(0, format, time); } uint8_t MAX7219Component::strftime(const char *format, ESPTime time) { return this->strftime(0, format, time); }
} // namespace max7219 } // namespace esphome::max7219
} // namespace esphome

View File

@@ -6,8 +6,7 @@
#include "esphome/components/spi/spi.h" #include "esphome/components/spi/spi.h"
#include "esphome/components/display/display.h" #include "esphome/components/display/display.h"
namespace esphome { namespace esphome::max7219 {
namespace max7219 {
class MAX7219Component; class MAX7219Component;
@@ -17,6 +16,8 @@ class MAX7219Component : public PollingComponent,
public spi::SPIDevice<spi::BIT_ORDER_MSB_FIRST, spi::CLOCK_POLARITY_LOW, public spi::SPIDevice<spi::BIT_ORDER_MSB_FIRST, spi::CLOCK_POLARITY_LOW,
spi::CLOCK_PHASE_LEADING, spi::DATA_RATE_1MHZ> { spi::CLOCK_PHASE_LEADING, spi::DATA_RATE_1MHZ> {
public: public:
explicit MAX7219Component(uint8_t num_chips);
void set_writer(max7219_writer_t &&writer); void set_writer(max7219_writer_t &&writer);
void setup() override; void setup() override;
@@ -30,7 +31,6 @@ class MAX7219Component : public PollingComponent,
void display(); void display();
void set_intensity(uint8_t intensity); void set_intensity(uint8_t intensity);
void set_num_chips(uint8_t num_chips);
void set_reverse(bool reverse) { this->reverse_ = reverse; }; void set_reverse(bool reverse) { this->reverse_ = reverse; };
/// Evaluate the printf-format and print the result at the given position. /// Evaluate the printf-format and print the result at the given position.
@@ -56,10 +56,9 @@ class MAX7219Component : public PollingComponent,
uint8_t intensity_{15}; // Intensity of the display from 0 to 15 (most) uint8_t intensity_{15}; // Intensity of the display from 0 to 15 (most)
bool intensity_changed_{}; // True if we need to re-send the intensity bool intensity_changed_{}; // True if we need to re-send the intensity
uint8_t num_chips_{1}; uint8_t num_chips_{1};
uint8_t *buffer_; uint8_t *buffer_{nullptr};
bool reverse_{false}; bool reverse_{false};
max7219_writer_t writer_{}; max7219_writer_t writer_{};
}; };
} // namespace max7219 } // namespace esphome::max7219
} // namespace esphome

View File

@@ -12,6 +12,10 @@ namespace esphome::mdns {
static const char *const TAG = "mdns"; static const char *const TAG = "mdns";
static void register_esp32(MDNSComponent *comp, StaticVector<MDNSService, MDNS_SERVICE_COUNT> &services) { static void register_esp32(MDNSComponent *comp, StaticVector<MDNSService, MDNS_SERVICE_COUNT> &services) {
#ifdef USE_OPENTHREAD
// OpenThread handles service registration via SRP client
// Services are compiled by MDNSComponent::compile_records_() and consumed by OpenThreadSrpComponent
#else
esp_err_t err = mdns_init(); esp_err_t err = mdns_init();
if (err != ESP_OK) { if (err != ESP_OK) {
ESP_LOGW(TAG, "Init failed: %s", esp_err_to_name(err)); ESP_LOGW(TAG, "Init failed: %s", esp_err_to_name(err));
@@ -41,13 +45,16 @@ static void register_esp32(MDNSComponent *comp, StaticVector<MDNSService, MDNS_S
ESP_LOGW(TAG, "Failed to register service %s: %s", MDNS_STR_ARG(service.service_type), esp_err_to_name(err)); ESP_LOGW(TAG, "Failed to register service %s: %s", MDNS_STR_ARG(service.service_type), esp_err_to_name(err));
} }
} }
#endif
} }
void MDNSComponent::setup() { this->setup_buffers_and_register_(register_esp32); } void MDNSComponent::setup() { this->setup_buffers_and_register_(register_esp32); }
void MDNSComponent::on_shutdown() { void MDNSComponent::on_shutdown() {
#ifndef USE_OPENTHREAD
mdns_free(); mdns_free();
delay(40); // Allow the mdns packets announcing service removal to be sent delay(40); // Allow the mdns packets announcing service removal to be sent
#endif
} }
} // namespace esphome::mdns } // namespace esphome::mdns

View File

@@ -1,6 +1,39 @@
#include "mipi_spi.h" #include "mipi_spi.h"
#include "esphome/core/log.h" #include "esphome/core/log.h"
namespace esphome { namespace esphome::mipi_spi {
namespace mipi_spi {} // namespace mipi_spi
} // namespace esphome void internal_dump_config(const char *model, int width, int height, int offset_width, int offset_height, uint8_t madctl,
bool invert_colors, int display_bits, bool is_big_endian, const optional<uint8_t> &brightness,
GPIOPin *cs, GPIOPin *reset, GPIOPin *dc, int spi_mode, uint32_t data_rate, int bus_width) {
ESP_LOGCONFIG(TAG,
"MIPI_SPI Display\n"
" Model: %s\n"
" Width: %d\n"
" Height: %d\n"
" Swap X/Y: %s\n"
" Mirror X: %s\n"
" Mirror Y: %s\n"
" Invert colors: %s\n"
" Color order: %s\n"
" Display pixels: %d bits\n"
" Endianness: %s\n"
" SPI Mode: %d\n"
" SPI Data rate: %uMHz\n"
" SPI Bus width: %d",
model, width, height, YESNO(madctl & MADCTL_MV), YESNO(madctl & (MADCTL_MX | MADCTL_XFLIP)),
YESNO(madctl & (MADCTL_MY | MADCTL_YFLIP)), YESNO(invert_colors), (madctl & MADCTL_BGR) ? "BGR" : "RGB",
display_bits, is_big_endian ? "Big" : "Little", spi_mode, static_cast<unsigned>(data_rate / 1000000),
bus_width);
LOG_PIN(" CS Pin: ", cs);
LOG_PIN(" Reset Pin: ", reset);
LOG_PIN(" DC Pin: ", dc);
if (offset_width != 0)
ESP_LOGCONFIG(TAG, " Offset width: %d", offset_width);
if (offset_height != 0)
ESP_LOGCONFIG(TAG, " Offset height: %d", offset_height);
if (brightness.has_value())
ESP_LOGCONFIG(TAG, " Brightness: %u", brightness.value());
}
} // namespace esphome::mipi_spi

View File

@@ -63,6 +63,11 @@ enum BusType {
BUS_TYPE_SINGLE_16 = 16, // Single bit bus, but 16 bits per transfer BUS_TYPE_SINGLE_16 = 16, // Single bit bus, but 16 bits per transfer
}; };
// Helper function for dump_config - defined in mipi_spi.cpp to allow use of LOG_PIN macro
void internal_dump_config(const char *model, int width, int height, int offset_width, int offset_height, uint8_t madctl,
bool invert_colors, int display_bits, bool is_big_endian, const optional<uint8_t> &brightness,
GPIOPin *cs, GPIOPin *reset, GPIOPin *dc, int spi_mode, uint32_t data_rate, int bus_width);
/** /**
* Base class for MIPI SPI displays. * Base class for MIPI SPI displays.
* All the methods are defined here in the header file, as it is not possible to define templated methods in a cpp file. * All the methods are defined here in the header file, as it is not possible to define templated methods in a cpp file.
@@ -201,37 +206,9 @@ class MipiSpi : public display::Display,
} }
void dump_config() override { void dump_config() override {
esph_log_config(TAG, internal_dump_config(this->model_, WIDTH, HEIGHT, OFFSET_WIDTH, OFFSET_HEIGHT, this->madctl_, this->invert_colors_,
"MIPI_SPI Display\n" DISPLAYPIXEL * 8, IS_BIG_ENDIAN, this->brightness_, this->cs_, this->reset_pin_, this->dc_pin_,
" Model: %s\n" this->mode_, this->data_rate_, BUS_TYPE);
" Width: %u\n"
" Height: %u",
this->model_, WIDTH, HEIGHT);
if constexpr (OFFSET_WIDTH != 0)
esph_log_config(TAG, " Offset width: %u", OFFSET_WIDTH);
if constexpr (OFFSET_HEIGHT != 0)
esph_log_config(TAG, " Offset height: %u", OFFSET_HEIGHT);
esph_log_config(TAG,
" Swap X/Y: %s\n"
" Mirror X: %s\n"
" Mirror Y: %s\n"
" Invert colors: %s\n"
" Color order: %s\n"
" Display pixels: %d bits\n"
" Endianness: %s\n",
YESNO(this->madctl_ & MADCTL_MV), YESNO(this->madctl_ & (MADCTL_MX | MADCTL_XFLIP)),
YESNO(this->madctl_ & (MADCTL_MY | MADCTL_YFLIP)), YESNO(this->invert_colors_),
this->madctl_ & MADCTL_BGR ? "BGR" : "RGB", DISPLAYPIXEL * 8, IS_BIG_ENDIAN ? "Big" : "Little");
if (this->brightness_.has_value())
esph_log_config(TAG, " Brightness: %u", this->brightness_.value());
log_pin(TAG, " CS Pin: ", this->cs_);
log_pin(TAG, " Reset Pin: ", this->reset_pin_);
log_pin(TAG, " DC Pin: ", this->dc_pin_);
esph_log_config(TAG,
" SPI Mode: %d\n"
" SPI Data rate: %dMHz\n"
" SPI Bus width: %d",
this->mode_, static_cast<unsigned>(this->data_rate_ / 1000000), BUS_TYPE);
} }
protected: protected:

View File

@@ -4,7 +4,10 @@ from esphome import automation
from esphome.automation import Condition from esphome.automation import Condition
import esphome.codegen as cg import esphome.codegen as cg
from esphome.components import logger, socket from esphome.components import logger, socket
from esphome.components.esp32 import add_idf_sdkconfig_option from esphome.components.esp32 import (
add_idf_sdkconfig_option,
include_builtin_idf_component,
)
from esphome.config_helpers import filter_source_files_from_platform from esphome.config_helpers import filter_source_files_from_platform
import esphome.config_validation as cv import esphome.config_validation as cv
from esphome.const import ( from esphome.const import (
@@ -360,6 +363,8 @@ async def to_code(config):
# This enables low-latency MQTT event processing instead of waiting for select() timeout # This enables low-latency MQTT event processing instead of waiting for select() timeout
if CORE.is_esp32: if CORE.is_esp32:
socket.require_wake_loop_threadsafe() socket.require_wake_loop_threadsafe()
# Re-enable ESP-IDF's mqtt component (excluded by default to save compile time)
include_builtin_idf_component("mqtt")
cg.add_define("USE_MQTT") cg.add_define("USE_MQTT")
cg.add_global(mqtt_ns.using) cg.add_global(mqtt_ns.using)

View File

@@ -643,10 +643,34 @@ static bool topic_match(const char *message, const char *subscription) {
} }
void MQTTClientComponent::on_message(const std::string &topic, const std::string &payload) { void MQTTClientComponent::on_message(const std::string &topic, const std::string &payload) {
for (auto &subscription : this->subscriptions_) { #ifdef USE_ESP8266
if (topic_match(topic.c_str(), subscription.topic.c_str())) // IMPORTANT: This defer is REQUIRED to prevent stack overflow crashes on ESP8266.
subscription.callback(topic, payload); //
} // On ESP8266, this callback is invoked directly from the lwIP/AsyncTCP network stack
// which runs in the "sys" context with a very limited stack (~4KB). By the time we
// reach this function, the stack is already partially consumed by the network
// processing chain: tcp_input -> AsyncClient::_recv -> AsyncMqttClient::_onMessage -> here.
//
// MQTT subscription callbacks can trigger arbitrary user actions (automations, HTTP
// requests, sensor updates, etc.) which may have deep call stacks of their own.
// For example, an HTTP request action requires: DNS lookup -> TCP connect -> TLS
// handshake (if HTTPS) -> request formatting. This easily overflows the remaining
// system stack space, causing a LoadStoreAlignmentCause exception or silent corruption.
//
// By deferring to the main loop, we ensure callbacks execute with a fresh, full-size
// stack in the normal application context rather than the constrained network task.
//
// DO NOT REMOVE THIS DEFER without understanding the above. It may appear to work
// in simple tests but will cause crashes with complex automations.
this->defer([this, topic, payload]() {
#endif
for (auto &subscription : this->subscriptions_) {
if (topic_match(topic.c_str(), subscription.topic.c_str()))
subscription.callback(topic, payload);
}
#ifdef USE_ESP8266
});
#endif
} }
// Setters // Setters

View File

@@ -1,7 +1,12 @@
from esphome import pins from esphome import pins
import esphome.codegen as cg import esphome.codegen as cg
from esphome.components import light from esphome.components import light
from esphome.components.esp32 import VARIANT_ESP32C3, VARIANT_ESP32S3, get_esp32_variant from esphome.components.esp32 import (
VARIANT_ESP32C3,
VARIANT_ESP32S3,
get_esp32_variant,
include_builtin_idf_component,
)
import esphome.config_validation as cv import esphome.config_validation as cv
from esphome.const import ( from esphome.const import (
CONF_CHANNEL, CONF_CHANNEL,
@@ -205,6 +210,10 @@ async def to_code(config):
has_white = "W" in config[CONF_TYPE] has_white = "W" in config[CONF_TYPE]
method = config[CONF_METHOD] method = config[CONF_METHOD]
# Re-enable ESP-IDF's RMT driver if using RMT method (excluded by default)
if CORE.is_esp32 and method[CONF_TYPE] == METHOD_ESP32_RMT:
include_builtin_idf_component("esp_driver_rmt")
method_template = METHODS[method[CONF_TYPE]].to_code( method_template = METHODS[method[CONF_TYPE]].to_code(
method, config[CONF_VARIANT], config[CONF_INVERT] method, config[CONF_VARIANT], config[CONF_INVERT]
) )

View File

@@ -177,6 +177,8 @@ async def to_code(config):
cg.add_define("USE_NEXTION_TFT_UPLOAD") cg.add_define("USE_NEXTION_TFT_UPLOAD")
cg.add(var.set_tft_url(config[CONF_TFT_URL])) cg.add(var.set_tft_url(config[CONF_TFT_URL]))
if CORE.is_esp32: if CORE.is_esp32:
# Re-enable ESP-IDF's HTTP client (excluded by default to save compile time)
esp32.include_builtin_idf_component("esp_http_client")
esp32.add_idf_sdkconfig_option("CONFIG_ESP_TLS_INSECURE", True) esp32.add_idf_sdkconfig_option("CONFIG_ESP_TLS_INSECURE", True)
esp32.add_idf_sdkconfig_option( esp32.add_idf_sdkconfig_option(
"CONFIG_ESP_TLS_SKIP_SERVER_CERT_VERIFY", True "CONFIG_ESP_TLS_SKIP_SERVER_CERT_VERIFY", True

View File

@@ -1,6 +1,6 @@
from esphome import automation from esphome import automation
import esphome.codegen as cg import esphome.codegen as cg
from esphome.components import mqtt, web_server from esphome.components import mqtt, web_server, zigbee
import esphome.config_validation as cv import esphome.config_validation as cv
from esphome.const import ( from esphome.const import (
CONF_ABOVE, CONF_ABOVE,
@@ -189,6 +189,7 @@ validate_unit_of_measurement = cv.string_strict
_NUMBER_SCHEMA = ( _NUMBER_SCHEMA = (
cv.ENTITY_BASE_SCHEMA.extend(web_server.WEBSERVER_SORTING_SCHEMA) cv.ENTITY_BASE_SCHEMA.extend(web_server.WEBSERVER_SORTING_SCHEMA)
.extend(cv.MQTT_COMMAND_COMPONENT_SCHEMA) .extend(cv.MQTT_COMMAND_COMPONENT_SCHEMA)
.extend(zigbee.NUMBER_SCHEMA)
.extend( .extend(
{ {
cv.OnlyWith(CONF_MQTT_ID, "mqtt"): cv.declare_id(mqtt.MQTTNumberComponent), cv.OnlyWith(CONF_MQTT_ID, "mqtt"): cv.declare_id(mqtt.MQTTNumberComponent),
@@ -214,6 +215,7 @@ _NUMBER_SCHEMA = (
_NUMBER_SCHEMA.add_extra(entity_duplicate_validator("number")) _NUMBER_SCHEMA.add_extra(entity_duplicate_validator("number"))
_NUMBER_SCHEMA.add_extra(zigbee.validate_number)
def number_schema( def number_schema(
@@ -277,6 +279,8 @@ async def setup_number_core_(
if web_server_config := config.get(CONF_WEB_SERVER): if web_server_config := config.get(CONF_WEB_SERVER):
await web_server.add_entity_config(var, web_server_config) await web_server.add_entity_config(var, web_server_config)
await zigbee.setup_number(var, config, min_value, max_value, step)
async def register_number( async def register_number(
var, config, *, min_value: float, max_value: float, step: float var, config, *, min_value: float, max_value: float, step: float

View File

@@ -2,21 +2,20 @@
#include "esphome/core/log.h" #include "esphome/core/log.h"
#include "esphome/core/application.h" #include "esphome/core/application.h"
namespace esphome { namespace esphome::pmsx003 {
namespace pmsx003 {
static const char *const TAG = "pmsx003"; static const char *const TAG = "pmsx003";
static const uint8_t START_CHARACTER_1 = 0x42; static const uint8_t START_CHARACTER_1 = 0x42;
static const uint8_t START_CHARACTER_2 = 0x4D; static const uint8_t START_CHARACTER_2 = 0x4D;
static const uint16_t PMS_STABILISING_MS = 30000; // time taken for the sensor to become stable after power on in ms static const uint16_t STABILISING_MS = 30000; // time taken for the sensor to become stable after power on in ms
static const uint16_t PMS_CMD_MEASUREMENT_MODE_PASSIVE = static const uint16_t CMD_MEASUREMENT_MODE_PASSIVE =
0x0000; // use `PMS_CMD_MANUAL_MEASUREMENT` to trigger a measurement 0x0000; // use `Command::MANUAL_MEASUREMENT` to trigger a measurement
static const uint16_t PMS_CMD_MEASUREMENT_MODE_ACTIVE = 0x0001; // automatically perform measurements static const uint16_t CMD_MEASUREMENT_MODE_ACTIVE = 0x0001; // automatically perform measurements
static const uint16_t PMS_CMD_SLEEP_MODE_SLEEP = 0x0000; // go to sleep mode static const uint16_t CMD_SLEEP_MODE_SLEEP = 0x0000; // go to sleep mode
static const uint16_t PMS_CMD_SLEEP_MODE_WAKEUP = 0x0001; // wake up from sleep mode static const uint16_t CMD_SLEEP_MODE_WAKEUP = 0x0001; // wake up from sleep mode
void PMSX003Component::setup() {} void PMSX003Component::setup() {}
@@ -42,7 +41,7 @@ void PMSX003Component::dump_config() {
LOG_SENSOR(" ", "Temperature", this->temperature_sensor_); LOG_SENSOR(" ", "Temperature", this->temperature_sensor_);
LOG_SENSOR(" ", "Humidity", this->humidity_sensor_); LOG_SENSOR(" ", "Humidity", this->humidity_sensor_);
if (this->update_interval_ <= PMS_STABILISING_MS) { if (this->update_interval_ <= STABILISING_MS) {
ESP_LOGCONFIG(TAG, " Mode: active continuous (sensor default)"); ESP_LOGCONFIG(TAG, " Mode: active continuous (sensor default)");
} else { } else {
ESP_LOGCONFIG(TAG, " Mode: passive with sleep/wake cycles"); ESP_LOGCONFIG(TAG, " Mode: passive with sleep/wake cycles");
@@ -55,44 +54,44 @@ void PMSX003Component::loop() {
const uint32_t now = App.get_loop_component_start_time(); const uint32_t now = App.get_loop_component_start_time();
// Initialize sensor mode on first loop // Initialize sensor mode on first loop
if (this->initialised_ == 0) { if (!this->initialised_) {
if (this->update_interval_ > PMS_STABILISING_MS) { if (this->update_interval_ > STABILISING_MS) {
// Long update interval: use passive mode with sleep/wake cycles // Long update interval: use passive mode with sleep/wake cycles
this->send_command_(PMS_CMD_MEASUREMENT_MODE, PMS_CMD_MEASUREMENT_MODE_PASSIVE); this->send_command_(Command::MEASUREMENT_MODE, CMD_MEASUREMENT_MODE_PASSIVE);
this->send_command_(PMS_CMD_SLEEP_MODE, PMS_CMD_SLEEP_MODE_WAKEUP); this->send_command_(Command::SLEEP_MODE, CMD_SLEEP_MODE_WAKEUP);
} else { } else {
// Short/zero update interval: use active continuous mode // Short/zero update interval: use active continuous mode
this->send_command_(PMS_CMD_MEASUREMENT_MODE, PMS_CMD_MEASUREMENT_MODE_ACTIVE); this->send_command_(Command::MEASUREMENT_MODE, CMD_MEASUREMENT_MODE_ACTIVE);
} }
this->initialised_ = 1; this->initialised_ = true;
} }
// If we update less often than it takes the device to stabilise, spin the fan down // If we update less often than it takes the device to stabilise, spin the fan down
// rather than running it constantly. It does take some time to stabilise, so we // rather than running it constantly. It does take some time to stabilise, so we
// need to keep track of what state we're in. // need to keep track of what state we're in.
if (this->update_interval_ > PMS_STABILISING_MS) { if (this->update_interval_ > STABILISING_MS) {
switch (this->state_) { switch (this->state_) {
case PMSX003_STATE_IDLE: case State::IDLE:
// Power on the sensor now so it'll be ready when we hit the update time // Power on the sensor now so it'll be ready when we hit the update time
if (now - this->last_update_ < (this->update_interval_ - PMS_STABILISING_MS)) if (now - this->last_update_ < (this->update_interval_ - STABILISING_MS))
return; return;
this->state_ = PMSX003_STATE_STABILISING; this->state_ = State::STABILISING;
this->send_command_(PMS_CMD_SLEEP_MODE, PMS_CMD_SLEEP_MODE_WAKEUP); this->send_command_(Command::SLEEP_MODE, CMD_SLEEP_MODE_WAKEUP);
this->fan_on_time_ = now; this->fan_on_time_ = now;
return; return;
case PMSX003_STATE_STABILISING: case State::STABILISING:
// wait for the sensor to be stable // wait for the sensor to be stable
if (now - this->fan_on_time_ < PMS_STABILISING_MS) if (now - this->fan_on_time_ < STABILISING_MS)
return; return;
// consume any command responses that are in the serial buffer // consume any command responses that are in the serial buffer
while (this->available()) while (this->available())
this->read_byte(&this->data_[0]); this->read_byte(&this->data_[0]);
// Trigger a new read // Trigger a new read
this->send_command_(PMS_CMD_MANUAL_MEASUREMENT, 0); this->send_command_(Command::MANUAL_MEASUREMENT, 0);
this->state_ = PMSX003_STATE_WAITING; this->state_ = State::WAITING;
break; break;
case PMSX003_STATE_WAITING: case State::WAITING:
// Just go ahead and read stuff // Just go ahead and read stuff
break; break;
} }
@@ -180,27 +179,31 @@ optional<bool> PMSX003Component::check_byte_() {
} }
bool PMSX003Component::check_payload_length_(uint16_t payload_length) { bool PMSX003Component::check_payload_length_(uint16_t payload_length) {
// https://avaldebe.github.io/PyPMS/sensors/Plantower/
switch (this->type_) { switch (this->type_) {
case PMSX003_TYPE_X003: case Type::PMS1003:
// The expected payload length is typically 28 bytes. return payload_length == 28; // 2*13+2
// However, a 20-byte payload check was already present in the code. case Type::PMS3003: // Data 7/8/9 not set/reserved
// No official documentation was found confirming this. return payload_length == 20; // 2*9+2
// Retaining this check to avoid breaking existing behavior. case Type::PMSX003: // Data 13 not set/reserved
// Deprecated: Length 20 is for PMS3003 backwards compatibility
return payload_length == 28 || payload_length == 20; // 2*13+2 return payload_length == 28 || payload_length == 20; // 2*13+2
case PMSX003_TYPE_5003T: case Type::PMS5003S:
case PMSX003_TYPE_5003S: case Type::PMS5003T: // Data 13 not set/reserved
return payload_length == 28; // 2*13+2 (Data 13 not set/reserved) return payload_length == 28; // 2*13+2
case PMSX003_TYPE_5003ST: case Type::PMS5003ST: // Data 16 not set/reserved
return payload_length == 36; // 2*17+2 (Data 16 not set/reserved) return payload_length == 36; // 2*17+2
case Type::PMS9003M:
return payload_length == 28; // 2*13+2
} }
return false; return false;
} }
void PMSX003Component::send_command_(PMSX0003Command cmd, uint16_t data) { void PMSX003Component::send_command_(Command cmd, uint16_t data) {
uint8_t send_data[7] = { uint8_t send_data[7] = {
START_CHARACTER_1, // Start Byte 1 START_CHARACTER_1, // Start Byte 1
START_CHARACTER_2, // Start Byte 2 START_CHARACTER_2, // Start Byte 2
cmd, // Command static_cast<uint8_t>(cmd), // Command
uint8_t((data >> 8) & 0xFF), // Data 1 uint8_t((data >> 8) & 0xFF), // Data 1
uint8_t((data >> 0) & 0xFF), // Data 2 uint8_t((data >> 0) & 0xFF), // Data 2
0, // Verify Byte 1 0, // Verify Byte 1
@@ -265,7 +268,7 @@ void PMSX003Component::parse_data_() {
if (this->pm_particles_25um_sensor_ != nullptr) if (this->pm_particles_25um_sensor_ != nullptr)
this->pm_particles_25um_sensor_->publish_state(pm_particles_25um); this->pm_particles_25um_sensor_->publish_state(pm_particles_25um);
if (this->type_ == PMSX003_TYPE_5003T) { if (this->type_ == Type::PMS5003T) {
ESP_LOGD(TAG, ESP_LOGD(TAG,
"Got PM0.3 Particles: %u Count/0.1L, PM0.5 Particles: %u Count/0.1L, PM1.0 Particles: %u Count/0.1L, " "Got PM0.3 Particles: %u Count/0.1L, PM0.5 Particles: %u Count/0.1L, PM1.0 Particles: %u Count/0.1L, "
"PM2.5 Particles %u Count/0.1L", "PM2.5 Particles %u Count/0.1L",
@@ -289,7 +292,7 @@ void PMSX003Component::parse_data_() {
} }
// Formaldehyde // Formaldehyde
if (this->type_ == PMSX003_TYPE_5003ST || this->type_ == PMSX003_TYPE_5003S) { if (this->type_ == Type::PMS5003S || this->type_ == Type::PMS5003ST) {
const uint16_t formaldehyde = this->get_16_bit_uint_(28); const uint16_t formaldehyde = this->get_16_bit_uint_(28);
ESP_LOGD(TAG, "Got Formaldehyde: %u µg/m^3", formaldehyde); ESP_LOGD(TAG, "Got Formaldehyde: %u µg/m^3", formaldehyde);
@@ -299,8 +302,8 @@ void PMSX003Component::parse_data_() {
} }
// Temperature and Humidity // Temperature and Humidity
if (this->type_ == PMSX003_TYPE_5003ST || this->type_ == PMSX003_TYPE_5003T) { if (this->type_ == Type::PMS5003T || this->type_ == Type::PMS5003ST) {
const uint8_t temperature_offset = (this->type_ == PMSX003_TYPE_5003T) ? 24 : 30; const uint8_t temperature_offset = (this->type_ == Type::PMS5003T) ? 24 : 30;
const float temperature = static_cast<int16_t>(this->get_16_bit_uint_(temperature_offset)) / 10.0f; const float temperature = static_cast<int16_t>(this->get_16_bit_uint_(temperature_offset)) / 10.0f;
const float humidity = this->get_16_bit_uint_(temperature_offset + 2) / 10.0f; const float humidity = this->get_16_bit_uint_(temperature_offset + 2) / 10.0f;
@@ -314,22 +317,22 @@ void PMSX003Component::parse_data_() {
} }
// Firmware Version and Error Code // Firmware Version and Error Code
if (this->type_ == PMSX003_TYPE_5003ST) { if (this->type_ == Type::PMS1003 || this->type_ == Type::PMS5003ST || this->type_ == Type::PMS9003M) {
const uint8_t firmware_version = this->data_[36]; const uint8_t firmware_error_code_offset = (this->type_ == Type::PMS5003ST) ? 36 : 28;
const uint8_t error_code = this->data_[37]; const uint8_t firmware_version = this->data_[firmware_error_code_offset];
const uint8_t error_code = this->data_[firmware_error_code_offset + 1];
ESP_LOGD(TAG, "Got Firmware Version: 0x%02X, Error Code: 0x%02X", firmware_version, error_code); ESP_LOGD(TAG, "Got Firmware Version: 0x%02X, Error Code: 0x%02X", firmware_version, error_code);
} }
// Spin down the sensor again if we aren't going to need it until more time has // Spin down the sensor again if we aren't going to need it until more time has
// passed than it takes to stabilise // passed than it takes to stabilise
if (this->update_interval_ > PMS_STABILISING_MS) { if (this->update_interval_ > STABILISING_MS) {
this->send_command_(PMS_CMD_SLEEP_MODE, PMS_CMD_SLEEP_MODE_SLEEP); this->send_command_(Command::SLEEP_MODE, CMD_SLEEP_MODE_SLEEP);
this->state_ = PMSX003_STATE_IDLE; this->state_ = State::IDLE;
} }
this->status_clear_warning(); this->status_clear_warning();
} }
} // namespace pmsx003 } // namespace esphome::pmsx003
} // namespace esphome

View File

@@ -5,27 +5,28 @@
#include "esphome/components/sensor/sensor.h" #include "esphome/components/sensor/sensor.h"
#include "esphome/components/uart/uart.h" #include "esphome/components/uart/uart.h"
namespace esphome { namespace esphome::pmsx003 {
namespace pmsx003 {
enum PMSX0003Command : uint8_t { enum class Type : uint8_t {
PMS_CMD_MEASUREMENT_MODE = PMS1003 = 0,
0xE1, // Data Options: `PMS_CMD_MEASUREMENT_MODE_PASSIVE`, `PMS_CMD_MEASUREMENT_MODE_ACTIVE` PMS3003,
PMS_CMD_MANUAL_MEASUREMENT = 0xE2, PMSX003, // PMS5003, PMS6003, PMS7003, PMSA003 (NOT PMSA003I - see `pmsa003i` component)
PMS_CMD_SLEEP_MODE = 0xE4, // Data Options: `PMS_CMD_SLEEP_MODE_SLEEP`, `PMS_CMD_SLEEP_MODE_WAKEUP` PMS5003S,
PMS5003T,
PMS5003ST,
PMS9003M,
}; };
enum PMSX003Type { enum class Command : uint8_t {
PMSX003_TYPE_X003 = 0, MEASUREMENT_MODE = 0xE1, // Data Options: `CMD_MEASUREMENT_MODE_PASSIVE`, `CMD_MEASUREMENT_MODE_ACTIVE`
PMSX003_TYPE_5003T, MANUAL_MEASUREMENT = 0xE2,
PMSX003_TYPE_5003ST, SLEEP_MODE = 0xE4, // Data Options: `CMD_SLEEP_MODE_SLEEP`, `CMD_SLEEP_MODE_WAKEUP`
PMSX003_TYPE_5003S,
}; };
enum PMSX003State { enum class State : uint8_t {
PMSX003_STATE_IDLE = 0, IDLE = 0,
PMSX003_STATE_STABILISING, STABILISING,
PMSX003_STATE_WAITING, WAITING,
}; };
class PMSX003Component : public uart::UARTDevice, public Component { class PMSX003Component : public uart::UARTDevice, public Component {
@@ -37,7 +38,7 @@ class PMSX003Component : public uart::UARTDevice, public Component {
void set_update_interval(uint32_t update_interval) { this->update_interval_ = update_interval; } void set_update_interval(uint32_t update_interval) { this->update_interval_ = update_interval; }
void set_type(PMSX003Type type) { this->type_ = type; } void set_type(Type type) { this->type_ = type; }
void set_pm_1_0_std_sensor(sensor::Sensor *pm_1_0_std_sensor) { this->pm_1_0_std_sensor_ = pm_1_0_std_sensor; } void set_pm_1_0_std_sensor(sensor::Sensor *pm_1_0_std_sensor) { this->pm_1_0_std_sensor_ = pm_1_0_std_sensor; }
void set_pm_2_5_std_sensor(sensor::Sensor *pm_2_5_std_sensor) { this->pm_2_5_std_sensor_ = pm_2_5_std_sensor; } void set_pm_2_5_std_sensor(sensor::Sensor *pm_2_5_std_sensor) { this->pm_2_5_std_sensor_ = pm_2_5_std_sensor; }
@@ -77,20 +78,20 @@ class PMSX003Component : public uart::UARTDevice, public Component {
optional<bool> check_byte_(); optional<bool> check_byte_();
void parse_data_(); void parse_data_();
bool check_payload_length_(uint16_t payload_length); bool check_payload_length_(uint16_t payload_length);
void send_command_(PMSX0003Command cmd, uint16_t data); void send_command_(Command cmd, uint16_t data);
uint16_t get_16_bit_uint_(uint8_t start_index) const { uint16_t get_16_bit_uint_(uint8_t start_index) const {
return encode_uint16(this->data_[start_index], this->data_[start_index + 1]); return encode_uint16(this->data_[start_index], this->data_[start_index + 1]);
} }
Type type_;
State state_{State::IDLE};
bool initialised_{false};
uint8_t data_[64]; uint8_t data_[64];
uint8_t data_index_{0}; uint8_t data_index_{0};
uint8_t initialised_{0};
uint32_t fan_on_time_{0}; uint32_t fan_on_time_{0};
uint32_t last_update_{0}; uint32_t last_update_{0};
uint32_t last_transmission_{0}; uint32_t last_transmission_{0};
uint32_t update_interval_{0}; uint32_t update_interval_{0};
PMSX003State state_{PMSX003_STATE_IDLE};
PMSX003Type type_;
// "Standard Particle" // "Standard Particle"
sensor::Sensor *pm_1_0_std_sensor_{nullptr}; sensor::Sensor *pm_1_0_std_sensor_{nullptr};
@@ -118,5 +119,4 @@ class PMSX003Component : public uart::UARTDevice, public Component {
sensor::Sensor *humidity_sensor_{nullptr}; sensor::Sensor *humidity_sensor_{nullptr};
}; };
} // namespace pmsx003 } // namespace esphome::pmsx003
} // namespace esphome

View File

@@ -40,34 +40,128 @@ pmsx003_ns = cg.esphome_ns.namespace("pmsx003")
PMSX003Component = pmsx003_ns.class_("PMSX003Component", uart.UARTDevice, cg.Component) PMSX003Component = pmsx003_ns.class_("PMSX003Component", uart.UARTDevice, cg.Component)
PMSX003Sensor = pmsx003_ns.class_("PMSX003Sensor", sensor.Sensor) PMSX003Sensor = pmsx003_ns.class_("PMSX003Sensor", sensor.Sensor)
TYPE_PMSX003 = "PMSX003" TYPE_PMS1003 = "PMS1003"
TYPE_PMS3003 = "PMS3003"
TYPE_PMSX003 = "PMSX003" # PMS5003, PMS6003, PMS7003, PMSA003 (NOT PMSA003I - see `pmsa003i` component)
TYPE_PMS5003S = "PMS5003S"
TYPE_PMS5003T = "PMS5003T" TYPE_PMS5003T = "PMS5003T"
TYPE_PMS5003ST = "PMS5003ST" TYPE_PMS5003ST = "PMS5003ST"
TYPE_PMS5003S = "PMS5003S" TYPE_PMS9003M = "PMS9003M"
PMSX003Type = pmsx003_ns.enum("PMSX003Type") Type = pmsx003_ns.enum("Type", is_class=True)
PMSX003_TYPES = { PMSX003_TYPES = {
TYPE_PMSX003: PMSX003Type.PMSX003_TYPE_X003, TYPE_PMS1003: Type.PMS1003,
TYPE_PMS5003T: PMSX003Type.PMSX003_TYPE_5003T, TYPE_PMS3003: Type.PMS3003,
TYPE_PMS5003ST: PMSX003Type.PMSX003_TYPE_5003ST, TYPE_PMSX003: Type.PMSX003,
TYPE_PMS5003S: PMSX003Type.PMSX003_TYPE_5003S, TYPE_PMS5003S: Type.PMS5003S,
TYPE_PMS5003T: Type.PMS5003T,
TYPE_PMS5003ST: Type.PMS5003ST,
TYPE_PMS9003M: Type.PMS9003M,
} }
SENSORS_TO_TYPE = { SENSORS_TO_TYPE = {
CONF_PM_1_0: [TYPE_PMSX003, TYPE_PMS5003T, TYPE_PMS5003ST, TYPE_PMS5003S], CONF_PM_1_0_STD: [
CONF_PM_2_5: [TYPE_PMSX003, TYPE_PMS5003T, TYPE_PMS5003ST, TYPE_PMS5003S], TYPE_PMS1003,
CONF_PM_10_0: [TYPE_PMSX003, TYPE_PMS5003T, TYPE_PMS5003ST, TYPE_PMS5003S], TYPE_PMS3003,
CONF_PM_1_0_STD: [TYPE_PMSX003, TYPE_PMS5003T, TYPE_PMS5003ST, TYPE_PMS5003S], TYPE_PMSX003,
CONF_PM_2_5_STD: [TYPE_PMSX003, TYPE_PMS5003T, TYPE_PMS5003ST, TYPE_PMS5003S], TYPE_PMS5003S,
CONF_PM_10_0_STD: [TYPE_PMSX003, TYPE_PMS5003T, TYPE_PMS5003ST, TYPE_PMS5003S], TYPE_PMS5003T,
CONF_PM_0_3UM: [TYPE_PMSX003, TYPE_PMS5003T, TYPE_PMS5003ST, TYPE_PMS5003S], TYPE_PMS5003ST,
CONF_PM_0_5UM: [TYPE_PMSX003, TYPE_PMS5003T, TYPE_PMS5003ST, TYPE_PMS5003S], TYPE_PMS9003M,
CONF_PM_1_0UM: [TYPE_PMSX003, TYPE_PMS5003T, TYPE_PMS5003ST, TYPE_PMS5003S], ],
CONF_PM_2_5UM: [TYPE_PMSX003, TYPE_PMS5003T, TYPE_PMS5003ST, TYPE_PMS5003S], CONF_PM_2_5_STD: [
CONF_PM_5_0UM: [TYPE_PMSX003, TYPE_PMS5003ST, TYPE_PMS5003S], TYPE_PMS1003,
CONF_PM_10_0UM: [TYPE_PMSX003, TYPE_PMS5003ST, TYPE_PMS5003S], TYPE_PMS3003,
CONF_FORMALDEHYDE: [TYPE_PMS5003ST, TYPE_PMS5003S], TYPE_PMSX003,
TYPE_PMS5003S,
TYPE_PMS5003T,
TYPE_PMS5003ST,
TYPE_PMS9003M,
],
CONF_PM_10_0_STD: [
TYPE_PMS1003,
TYPE_PMS3003,
TYPE_PMSX003,
TYPE_PMS5003S,
TYPE_PMS5003T,
TYPE_PMS5003ST,
TYPE_PMS9003M,
],
CONF_PM_1_0: [
TYPE_PMS1003,
TYPE_PMS3003,
TYPE_PMSX003,
TYPE_PMS5003S,
TYPE_PMS5003T,
TYPE_PMS5003ST,
TYPE_PMS9003M,
],
CONF_PM_2_5: [
TYPE_PMS1003,
TYPE_PMS3003,
TYPE_PMSX003,
TYPE_PMS5003S,
TYPE_PMS5003T,
TYPE_PMS5003ST,
TYPE_PMS9003M,
],
CONF_PM_10_0: [
TYPE_PMS1003,
TYPE_PMS3003,
TYPE_PMSX003,
TYPE_PMS5003S,
TYPE_PMS5003T,
TYPE_PMS5003ST,
TYPE_PMS9003M,
],
CONF_PM_0_3UM: [
TYPE_PMS1003,
TYPE_PMSX003,
TYPE_PMS5003S,
TYPE_PMS5003T,
TYPE_PMS5003ST,
TYPE_PMS9003M,
],
CONF_PM_0_5UM: [
TYPE_PMS1003,
TYPE_PMSX003,
TYPE_PMS5003S,
TYPE_PMS5003T,
TYPE_PMS5003ST,
TYPE_PMS9003M,
],
CONF_PM_1_0UM: [
TYPE_PMS1003,
TYPE_PMSX003,
TYPE_PMS5003S,
TYPE_PMS5003T,
TYPE_PMS5003ST,
TYPE_PMS9003M,
],
CONF_PM_2_5UM: [
TYPE_PMS1003,
TYPE_PMSX003,
TYPE_PMS5003S,
TYPE_PMS5003T,
TYPE_PMS5003ST,
TYPE_PMS9003M,
],
CONF_PM_5_0UM: [
TYPE_PMS1003,
TYPE_PMSX003,
TYPE_PMS5003S,
TYPE_PMS5003ST,
TYPE_PMS9003M,
],
CONF_PM_10_0UM: [
TYPE_PMS1003,
TYPE_PMSX003,
TYPE_PMS5003S,
TYPE_PMS5003ST,
TYPE_PMS9003M,
],
CONF_FORMALDEHYDE: [TYPE_PMS5003S, TYPE_PMS5003ST],
CONF_TEMPERATURE: [TYPE_PMS5003T, TYPE_PMS5003ST], CONF_TEMPERATURE: [TYPE_PMS5003T, TYPE_PMS5003ST],
CONF_HUMIDITY: [TYPE_PMS5003T, TYPE_PMS5003ST], CONF_HUMIDITY: [TYPE_PMS5003T, TYPE_PMS5003ST],
} }

View File

@@ -170,6 +170,9 @@ CONFIG_SCHEMA = remote_base.validate_triggers(
async def to_code(config): async def to_code(config):
pin = await cg.gpio_pin_expression(config[CONF_PIN]) pin = await cg.gpio_pin_expression(config[CONF_PIN])
if CORE.is_esp32: if CORE.is_esp32:
# Re-enable ESP-IDF's RMT driver (excluded by default to save compile time)
esp32.include_builtin_idf_component("esp_driver_rmt")
var = cg.new_Pvariable(config[CONF_ID], pin) var = cg.new_Pvariable(config[CONF_ID], pin)
cg.add(var.set_rmt_symbols(config[CONF_RMT_SYMBOLS])) cg.add(var.set_rmt_symbols(config[CONF_RMT_SYMBOLS]))
cg.add(var.set_receive_symbols(config[CONF_RECEIVE_SYMBOLS])) cg.add(var.set_receive_symbols(config[CONF_RECEIVE_SYMBOLS]))

View File

@@ -112,6 +112,9 @@ async def digital_write_action_to_code(config, action_id, template_arg, args):
async def to_code(config): async def to_code(config):
pin = await cg.gpio_pin_expression(config[CONF_PIN]) pin = await cg.gpio_pin_expression(config[CONF_PIN])
if CORE.is_esp32: if CORE.is_esp32:
# Re-enable ESP-IDF's RMT driver (excluded by default to save compile time)
esp32.include_builtin_idf_component("esp_driver_rmt")
var = cg.new_Pvariable(config[CONF_ID], pin) var = cg.new_Pvariable(config[CONF_ID], pin)
cg.add(var.set_rmt_symbols(config[CONF_RMT_SYMBOLS])) cg.add(var.set_rmt_symbols(config[CONF_RMT_SYMBOLS]))
cg.add(var.set_non_blocking(config[CONF_NON_BLOCKING])) cg.add(var.set_non_blocking(config[CONF_NON_BLOCKING]))

View File

@@ -2,7 +2,7 @@ from esphome import automation
import esphome.codegen as cg import esphome.codegen as cg
from esphome.components import audio, audio_dac from esphome.components import audio, audio_dac
import esphome.config_validation as cv import esphome.config_validation as cv
from esphome.const import CONF_DATA, CONF_ID, CONF_VOLUME from esphome.const import CONF_AUDIO_DAC, CONF_DATA, CONF_ID, CONF_VOLUME
from esphome.core import CORE, ID from esphome.core import CORE, ID
from esphome.coroutine import CoroPriority, coroutine_with_priority from esphome.coroutine import CoroPriority, coroutine_with_priority
@@ -11,8 +11,6 @@ CODEOWNERS = ["@jesserockz", "@kahrendt"]
IS_PLATFORM_COMPONENT = True IS_PLATFORM_COMPONENT = True
CONF_AUDIO_DAC = "audio_dac"
speaker_ns = cg.esphome_ns.namespace("speaker") speaker_ns = cg.esphome_ns.namespace("speaker")
Speaker = speaker_ns.class_("Speaker") Speaker = speaker_ns.class_("Speaker")

View File

@@ -79,16 +79,19 @@ async def to_code(config):
var.get_close_trigger(), [], config[CONF_CLOSE_ACTION] var.get_close_trigger(), [], config[CONF_CLOSE_ACTION]
) )
if CONF_STOP_ACTION in config: if CONF_STOP_ACTION in config:
cg.add_define("USE_TEMPLATE_COVER_STOP_TRIGGER")
await automation.build_automation( await automation.build_automation(
var.get_stop_trigger(), [], config[CONF_STOP_ACTION] var.get_stop_trigger(), [], config[CONF_STOP_ACTION]
) )
cg.add(var.set_has_stop(True)) cg.add(var.set_has_stop(True))
if CONF_TOGGLE_ACTION in config: if CONF_TOGGLE_ACTION in config:
cg.add_define("USE_TEMPLATE_COVER_TOGGLE_TRIGGER")
await automation.build_automation( await automation.build_automation(
var.get_toggle_trigger(), [], config[CONF_TOGGLE_ACTION] var.get_toggle_trigger(), [], config[CONF_TOGGLE_ACTION]
) )
cg.add(var.set_has_toggle(True)) cg.add(var.set_has_toggle(True))
if CONF_TILT_ACTION in config: if CONF_TILT_ACTION in config:
cg.add_define("USE_TEMPLATE_COVER_TILT_TRIGGER")
await automation.build_automation( await automation.build_automation(
var.get_tilt_trigger(), [(float, "tilt")], config[CONF_TILT_ACTION] var.get_tilt_trigger(), [(float, "tilt")], config[CONF_TILT_ACTION]
) )
@@ -99,6 +102,7 @@ async def to_code(config):
) )
cg.add(var.set_tilt_lambda(tilt_template_)) cg.add(var.set_tilt_lambda(tilt_template_))
if CONF_POSITION_ACTION in config: if CONF_POSITION_ACTION in config:
cg.add_define("USE_TEMPLATE_COVER_POSITION_TRIGGER")
await automation.build_automation( await automation.build_automation(
var.get_position_trigger(), [(float, "pos")], config[CONF_POSITION_ACTION] var.get_position_trigger(), [(float, "pos")], config[CONF_POSITION_ACTION]
) )

View File

@@ -9,11 +9,25 @@ static const char *const TAG = "template.cover";
TemplateCover::TemplateCover() TemplateCover::TemplateCover()
: open_trigger_(new Trigger<>()), : open_trigger_(new Trigger<>()),
close_trigger_(new Trigger<>), close_trigger_(new Trigger<>())
stop_trigger_(new Trigger<>()), #ifdef USE_TEMPLATE_COVER_STOP_TRIGGER
toggle_trigger_(new Trigger<>()), ,
position_trigger_(new Trigger<float>()), stop_trigger_(new Trigger<>())
tilt_trigger_(new Trigger<float>()) {} #endif
#ifdef USE_TEMPLATE_COVER_TOGGLE_TRIGGER
,
toggle_trigger_(new Trigger<>())
#endif
#ifdef USE_TEMPLATE_COVER_POSITION_TRIGGER
,
position_trigger_(new Trigger<float>())
#endif
#ifdef USE_TEMPLATE_COVER_TILT_TRIGGER
,
tilt_trigger_(new Trigger<float>())
#endif
{
}
void TemplateCover::setup() { void TemplateCover::setup() {
switch (this->restore_mode_) { switch (this->restore_mode_) {
case COVER_NO_RESTORE: case COVER_NO_RESTORE:
@@ -64,22 +78,30 @@ void TemplateCover::set_assumed_state(bool assumed_state) { this->assumed_state_
float TemplateCover::get_setup_priority() const { return setup_priority::HARDWARE; } float TemplateCover::get_setup_priority() const { return setup_priority::HARDWARE; }
Trigger<> *TemplateCover::get_open_trigger() const { return this->open_trigger_; } Trigger<> *TemplateCover::get_open_trigger() const { return this->open_trigger_; }
Trigger<> *TemplateCover::get_close_trigger() const { return this->close_trigger_; } Trigger<> *TemplateCover::get_close_trigger() const { return this->close_trigger_; }
#ifdef USE_TEMPLATE_COVER_STOP_TRIGGER
Trigger<> *TemplateCover::get_stop_trigger() const { return this->stop_trigger_; } Trigger<> *TemplateCover::get_stop_trigger() const { return this->stop_trigger_; }
#endif
#ifdef USE_TEMPLATE_COVER_TOGGLE_TRIGGER
Trigger<> *TemplateCover::get_toggle_trigger() const { return this->toggle_trigger_; } Trigger<> *TemplateCover::get_toggle_trigger() const { return this->toggle_trigger_; }
#endif
void TemplateCover::dump_config() { LOG_COVER("", "Template Cover", this); } void TemplateCover::dump_config() { LOG_COVER("", "Template Cover", this); }
void TemplateCover::control(const CoverCall &call) { void TemplateCover::control(const CoverCall &call) {
#ifdef USE_TEMPLATE_COVER_STOP_TRIGGER
if (call.get_stop()) { if (call.get_stop()) {
this->stop_prev_trigger_(); this->stop_prev_trigger_();
this->stop_trigger_->trigger(); this->stop_trigger_->trigger();
this->prev_command_trigger_ = this->stop_trigger_; this->prev_command_trigger_ = this->stop_trigger_;
this->publish_state(); this->publish_state();
} }
#endif
#ifdef USE_TEMPLATE_COVER_TOGGLE_TRIGGER
if (call.get_toggle().has_value()) { if (call.get_toggle().has_value()) {
this->stop_prev_trigger_(); this->stop_prev_trigger_();
this->toggle_trigger_->trigger(); this->toggle_trigger_->trigger();
this->prev_command_trigger_ = this->toggle_trigger_; this->prev_command_trigger_ = this->toggle_trigger_;
this->publish_state(); this->publish_state();
} }
#endif
if (call.get_position().has_value()) { if (call.get_position().has_value()) {
auto pos = *call.get_position(); auto pos = *call.get_position();
this->stop_prev_trigger_(); this->stop_prev_trigger_();
@@ -90,15 +112,19 @@ void TemplateCover::control(const CoverCall &call) {
} else if (pos == COVER_CLOSED) { } else if (pos == COVER_CLOSED) {
this->close_trigger_->trigger(); this->close_trigger_->trigger();
this->prev_command_trigger_ = this->close_trigger_; this->prev_command_trigger_ = this->close_trigger_;
} else { }
#ifdef USE_TEMPLATE_COVER_POSITION_TRIGGER
else {
this->position_trigger_->trigger(pos); this->position_trigger_->trigger(pos);
} }
#endif
if (this->optimistic_) { if (this->optimistic_) {
this->position = pos; this->position = pos;
} }
} }
#ifdef USE_TEMPLATE_COVER_TILT_TRIGGER
if (call.get_tilt().has_value()) { if (call.get_tilt().has_value()) {
auto tilt = *call.get_tilt(); auto tilt = *call.get_tilt();
this->tilt_trigger_->trigger(tilt); this->tilt_trigger_->trigger(tilt);
@@ -107,24 +133,53 @@ void TemplateCover::control(const CoverCall &call) {
this->tilt = tilt; this->tilt = tilt;
} }
} }
#endif
this->publish_state(); this->publish_state();
} }
CoverTraits TemplateCover::get_traits() { CoverTraits TemplateCover::get_traits() {
auto traits = CoverTraits(); auto traits = CoverTraits();
traits.set_is_assumed_state(this->assumed_state_); traits.set_is_assumed_state(this->assumed_state_);
#ifdef USE_TEMPLATE_COVER_STOP_TRIGGER
traits.set_supports_stop(this->has_stop_); traits.set_supports_stop(this->has_stop_);
#else
traits.set_supports_stop(false);
#endif
#ifdef USE_TEMPLATE_COVER_TOGGLE_TRIGGER
traits.set_supports_toggle(this->has_toggle_); traits.set_supports_toggle(this->has_toggle_);
#else
traits.set_supports_toggle(false);
#endif
#ifdef USE_TEMPLATE_COVER_POSITION_TRIGGER
traits.set_supports_position(this->has_position_); traits.set_supports_position(this->has_position_);
#else
traits.set_supports_position(false);
#endif
#ifdef USE_TEMPLATE_COVER_TILT_TRIGGER
traits.set_supports_tilt(this->has_tilt_); traits.set_supports_tilt(this->has_tilt_);
#else
traits.set_supports_tilt(false);
#endif
return traits; return traits;
} }
#ifdef USE_TEMPLATE_COVER_POSITION_TRIGGER
Trigger<float> *TemplateCover::get_position_trigger() const { return this->position_trigger_; } Trigger<float> *TemplateCover::get_position_trigger() const { return this->position_trigger_; }
#endif
#ifdef USE_TEMPLATE_COVER_TILT_TRIGGER
Trigger<float> *TemplateCover::get_tilt_trigger() const { return this->tilt_trigger_; } Trigger<float> *TemplateCover::get_tilt_trigger() const { return this->tilt_trigger_; }
#endif
#ifdef USE_TEMPLATE_COVER_STOP_TRIGGER
void TemplateCover::set_has_stop(bool has_stop) { this->has_stop_ = has_stop; } void TemplateCover::set_has_stop(bool has_stop) { this->has_stop_ = has_stop; }
#endif
#ifdef USE_TEMPLATE_COVER_TOGGLE_TRIGGER
void TemplateCover::set_has_toggle(bool has_toggle) { this->has_toggle_ = has_toggle; } void TemplateCover::set_has_toggle(bool has_toggle) { this->has_toggle_ = has_toggle; }
#endif
#ifdef USE_TEMPLATE_COVER_POSITION_TRIGGER
void TemplateCover::set_has_position(bool has_position) { this->has_position_ = has_position; } void TemplateCover::set_has_position(bool has_position) { this->has_position_ = has_position; }
#endif
#ifdef USE_TEMPLATE_COVER_TILT_TRIGGER
void TemplateCover::set_has_tilt(bool has_tilt) { this->has_tilt_ = has_tilt; } void TemplateCover::set_has_tilt(bool has_tilt) { this->has_tilt_ = has_tilt; }
#endif
void TemplateCover::stop_prev_trigger_() { void TemplateCover::stop_prev_trigger_() {
if (this->prev_command_trigger_ != nullptr) { if (this->prev_command_trigger_ != nullptr) {
this->prev_command_trigger_->stop_action(); this->prev_command_trigger_->stop_action();

View File

@@ -21,16 +21,32 @@ class TemplateCover final : public cover::Cover, public Component {
template<typename F> void set_tilt_lambda(F &&f) { this->tilt_f_.set(std::forward<F>(f)); } template<typename F> void set_tilt_lambda(F &&f) { this->tilt_f_.set(std::forward<F>(f)); }
Trigger<> *get_open_trigger() const; Trigger<> *get_open_trigger() const;
Trigger<> *get_close_trigger() const; Trigger<> *get_close_trigger() const;
#ifdef USE_TEMPLATE_COVER_STOP_TRIGGER
Trigger<> *get_stop_trigger() const; Trigger<> *get_stop_trigger() const;
#endif
#ifdef USE_TEMPLATE_COVER_TOGGLE_TRIGGER
Trigger<> *get_toggle_trigger() const; Trigger<> *get_toggle_trigger() const;
#endif
#ifdef USE_TEMPLATE_COVER_POSITION_TRIGGER
Trigger<float> *get_position_trigger() const; Trigger<float> *get_position_trigger() const;
#endif
#ifdef USE_TEMPLATE_COVER_TILT_TRIGGER
Trigger<float> *get_tilt_trigger() const; Trigger<float> *get_tilt_trigger() const;
#endif
void set_optimistic(bool optimistic); void set_optimistic(bool optimistic);
void set_assumed_state(bool assumed_state); void set_assumed_state(bool assumed_state);
#ifdef USE_TEMPLATE_COVER_STOP_TRIGGER
void set_has_stop(bool has_stop); void set_has_stop(bool has_stop);
#endif
#ifdef USE_TEMPLATE_COVER_POSITION_TRIGGER
void set_has_position(bool has_position); void set_has_position(bool has_position);
#endif
#ifdef USE_TEMPLATE_COVER_TILT_TRIGGER
void set_has_tilt(bool has_tilt); void set_has_tilt(bool has_tilt);
#endif
#ifdef USE_TEMPLATE_COVER_TOGGLE_TRIGGER
void set_has_toggle(bool has_toggle); void set_has_toggle(bool has_toggle);
#endif
void set_restore_mode(TemplateCoverRestoreMode restore_mode) { restore_mode_ = restore_mode; } void set_restore_mode(TemplateCoverRestoreMode restore_mode) { restore_mode_ = restore_mode; }
void setup() override; void setup() override;
@@ -44,22 +60,39 @@ class TemplateCover final : public cover::Cover, public Component {
cover::CoverTraits get_traits() override; cover::CoverTraits get_traits() override;
void stop_prev_trigger_(); void stop_prev_trigger_();
// Ordered to minimize padding on 32-bit: 4-byte members first, then smaller
TemplateCoverRestoreMode restore_mode_{COVER_RESTORE}; TemplateCoverRestoreMode restore_mode_{COVER_RESTORE};
TemplateLambda<float> state_f_; TemplateLambda<float> state_f_;
TemplateLambda<float> tilt_f_; TemplateLambda<float> tilt_f_;
bool assumed_state_{false};
bool optimistic_{false};
Trigger<> *open_trigger_; Trigger<> *open_trigger_;
Trigger<> *close_trigger_; Trigger<> *close_trigger_;
bool has_stop_{false};
bool has_toggle_{false};
Trigger<> *stop_trigger_;
Trigger<> *toggle_trigger_;
Trigger<> *prev_command_trigger_{nullptr}; Trigger<> *prev_command_trigger_{nullptr};
#ifdef USE_TEMPLATE_COVER_STOP_TRIGGER
Trigger<> *stop_trigger_;
#endif
#ifdef USE_TEMPLATE_COVER_TOGGLE_TRIGGER
Trigger<> *toggle_trigger_;
#endif
#ifdef USE_TEMPLATE_COVER_POSITION_TRIGGER
Trigger<float> *position_trigger_; Trigger<float> *position_trigger_;
bool has_position_{false}; #endif
#ifdef USE_TEMPLATE_COVER_TILT_TRIGGER
Trigger<float> *tilt_trigger_; Trigger<float> *tilt_trigger_;
#endif
bool assumed_state_{false};
bool optimistic_{false};
#ifdef USE_TEMPLATE_COVER_STOP_TRIGGER
bool has_stop_{false};
#endif
#ifdef USE_TEMPLATE_COVER_TOGGLE_TRIGGER
bool has_toggle_{false};
#endif
#ifdef USE_TEMPLATE_COVER_POSITION_TRIGGER
bool has_position_{false};
#endif
#ifdef USE_TEMPLATE_COVER_TILT_TRIGGER
bool has_tilt_{false}; bool has_tilt_{false};
#endif
}; };
} // namespace esphome::template_ } // namespace esphome::template_

View File

@@ -141,6 +141,12 @@ async def to_code(config):
cg.add(var.set_initial_value(datetime_struct)) cg.add(var.set_initial_value(datetime_struct))
if CONF_SET_ACTION in config: if CONF_SET_ACTION in config:
if config[CONF_TYPE] == "DATE":
cg.add_define("USE_TEMPLATE_DATE_SET_TRIGGER")
elif config[CONF_TYPE] == "TIME":
cg.add_define("USE_TEMPLATE_TIME_SET_TRIGGER")
elif config[CONF_TYPE] == "DATETIME":
cg.add_define("USE_TEMPLATE_DATETIME_SET_TRIGGER")
await automation.build_automation( await automation.build_automation(
var.get_set_trigger(), var.get_set_trigger(),
[(cg.ESPTime, "x")], [(cg.ESPTime, "x")],

View File

@@ -62,7 +62,9 @@ void TemplateDate::control(const datetime::DateCall &call) {
if (has_day) if (has_day)
value.day_of_month = *call.get_day(); value.day_of_month = *call.get_day();
#ifdef USE_TEMPLATE_DATE_SET_TRIGGER
this->set_trigger_->trigger(value); this->set_trigger_->trigger(value);
#endif
if (this->optimistic_) { if (this->optimistic_) {
if (has_year) if (has_year)

View File

@@ -22,7 +22,9 @@ class TemplateDate final : public datetime::DateEntity, public PollingComponent
void dump_config() override; void dump_config() override;
float get_setup_priority() const override { return setup_priority::HARDWARE; } float get_setup_priority() const override { return setup_priority::HARDWARE; }
#ifdef USE_TEMPLATE_DATE_SET_TRIGGER
Trigger<ESPTime> *get_set_trigger() const { return this->set_trigger_; } Trigger<ESPTime> *get_set_trigger() const { return this->set_trigger_; }
#endif
void set_optimistic(bool optimistic) { this->optimistic_ = optimistic; } void set_optimistic(bool optimistic) { this->optimistic_ = optimistic; }
void set_initial_value(ESPTime initial_value) { this->initial_value_ = initial_value; } void set_initial_value(ESPTime initial_value) { this->initial_value_ = initial_value; }
@@ -34,7 +36,9 @@ class TemplateDate final : public datetime::DateEntity, public PollingComponent
bool optimistic_{false}; bool optimistic_{false};
ESPTime initial_value_{}; ESPTime initial_value_{};
bool restore_value_{false}; bool restore_value_{false};
#ifdef USE_TEMPLATE_DATE_SET_TRIGGER
Trigger<ESPTime> *set_trigger_ = new Trigger<ESPTime>(); Trigger<ESPTime> *set_trigger_ = new Trigger<ESPTime>();
#endif
TemplateLambda<ESPTime> f_; TemplateLambda<ESPTime> f_;
ESPPreferenceObject pref_; ESPPreferenceObject pref_;

View File

@@ -80,7 +80,9 @@ void TemplateDateTime::control(const datetime::DateTimeCall &call) {
if (has_second) if (has_second)
value.second = *call.get_second(); value.second = *call.get_second();
#ifdef USE_TEMPLATE_DATETIME_SET_TRIGGER
this->set_trigger_->trigger(value); this->set_trigger_->trigger(value);
#endif
if (this->optimistic_) { if (this->optimistic_) {
if (has_year) if (has_year)

View File

@@ -22,7 +22,9 @@ class TemplateDateTime final : public datetime::DateTimeEntity, public PollingCo
void dump_config() override; void dump_config() override;
float get_setup_priority() const override { return setup_priority::HARDWARE; } float get_setup_priority() const override { return setup_priority::HARDWARE; }
#ifdef USE_TEMPLATE_DATETIME_SET_TRIGGER
Trigger<ESPTime> *get_set_trigger() const { return this->set_trigger_; } Trigger<ESPTime> *get_set_trigger() const { return this->set_trigger_; }
#endif
void set_optimistic(bool optimistic) { this->optimistic_ = optimistic; } void set_optimistic(bool optimistic) { this->optimistic_ = optimistic; }
void set_initial_value(ESPTime initial_value) { this->initial_value_ = initial_value; } void set_initial_value(ESPTime initial_value) { this->initial_value_ = initial_value; }
@@ -34,7 +36,9 @@ class TemplateDateTime final : public datetime::DateTimeEntity, public PollingCo
bool optimistic_{false}; bool optimistic_{false};
ESPTime initial_value_{}; ESPTime initial_value_{};
bool restore_value_{false}; bool restore_value_{false};
#ifdef USE_TEMPLATE_DATETIME_SET_TRIGGER
Trigger<ESPTime> *set_trigger_ = new Trigger<ESPTime>(); Trigger<ESPTime> *set_trigger_ = new Trigger<ESPTime>();
#endif
TemplateLambda<ESPTime> f_; TemplateLambda<ESPTime> f_;
ESPPreferenceObject pref_; ESPPreferenceObject pref_;

View File

@@ -62,7 +62,9 @@ void TemplateTime::control(const datetime::TimeCall &call) {
if (has_second) if (has_second)
value.second = *call.get_second(); value.second = *call.get_second();
#ifdef USE_TEMPLATE_TIME_SET_TRIGGER
this->set_trigger_->trigger(value); this->set_trigger_->trigger(value);
#endif
if (this->optimistic_) { if (this->optimistic_) {
if (has_hour) if (has_hour)

View File

@@ -22,7 +22,9 @@ class TemplateTime final : public datetime::TimeEntity, public PollingComponent
void dump_config() override; void dump_config() override;
float get_setup_priority() const override { return setup_priority::HARDWARE; } float get_setup_priority() const override { return setup_priority::HARDWARE; }
#ifdef USE_TEMPLATE_TIME_SET_TRIGGER
Trigger<ESPTime> *get_set_trigger() const { return this->set_trigger_; } Trigger<ESPTime> *get_set_trigger() const { return this->set_trigger_; }
#endif
void set_optimistic(bool optimistic) { this->optimistic_ = optimistic; } void set_optimistic(bool optimistic) { this->optimistic_ = optimistic; }
void set_initial_value(ESPTime initial_value) { this->initial_value_ = initial_value; } void set_initial_value(ESPTime initial_value) { this->initial_value_ = initial_value; }
@@ -34,7 +36,9 @@ class TemplateTime final : public datetime::TimeEntity, public PollingComponent
bool optimistic_{false}; bool optimistic_{false};
ESPTime initial_value_{}; ESPTime initial_value_{};
bool restore_value_{false}; bool restore_value_{false};
#ifdef USE_TEMPLATE_TIME_SET_TRIGGER
Trigger<ESPTime> *set_trigger_ = new Trigger<ESPTime>(); Trigger<ESPTime> *set_trigger_ = new Trigger<ESPTime>();
#endif
TemplateLambda<ESPTime> f_; TemplateLambda<ESPTime> f_;
ESPPreferenceObject pref_; ESPPreferenceObject pref_;

View File

@@ -64,14 +64,17 @@ async def to_code(config):
) )
cg.add(var.set_state_lambda(template_)) cg.add(var.set_state_lambda(template_))
if CONF_UNLOCK_ACTION in config: if CONF_UNLOCK_ACTION in config:
cg.add_define("USE_TEMPLATE_LOCK_UNLOCK_TRIGGER")
await automation.build_automation( await automation.build_automation(
var.get_unlock_trigger(), [], config[CONF_UNLOCK_ACTION] var.get_unlock_trigger(), [], config[CONF_UNLOCK_ACTION]
) )
if CONF_LOCK_ACTION in config: if CONF_LOCK_ACTION in config:
cg.add_define("USE_TEMPLATE_LOCK_LOCK_TRIGGER")
await automation.build_automation( await automation.build_automation(
var.get_lock_trigger(), [], config[CONF_LOCK_ACTION] var.get_lock_trigger(), [], config[CONF_LOCK_ACTION]
) )
if CONF_OPEN_ACTION in config: if CONF_OPEN_ACTION in config:
cg.add_define("USE_TEMPLATE_LOCK_OPEN_TRIGGER")
await automation.build_automation( await automation.build_automation(
var.get_open_trigger(), [], config[CONF_OPEN_ACTION] var.get_open_trigger(), [], config[CONF_OPEN_ACTION]
) )

View File

@@ -8,7 +8,27 @@ using namespace esphome::lock;
static const char *const TAG = "template.lock"; static const char *const TAG = "template.lock";
TemplateLock::TemplateLock() TemplateLock::TemplateLock()
: lock_trigger_(new Trigger<>()), unlock_trigger_(new Trigger<>()), open_trigger_(new Trigger<>()) {} #ifdef USE_TEMPLATE_LOCK_LOCK_TRIGGER
: lock_trigger_(new Trigger<>())
#ifdef USE_TEMPLATE_LOCK_UNLOCK_TRIGGER
,
unlock_trigger_(new Trigger<>())
#endif
#ifdef USE_TEMPLATE_LOCK_OPEN_TRIGGER
,
open_trigger_(new Trigger<>())
#endif
#elif defined(USE_TEMPLATE_LOCK_UNLOCK_TRIGGER)
: unlock_trigger_(new Trigger<>())
#ifdef USE_TEMPLATE_LOCK_OPEN_TRIGGER
,
open_trigger_(new Trigger<>())
#endif
#elif defined(USE_TEMPLATE_LOCK_OPEN_TRIGGER)
: open_trigger_(new Trigger<>())
#endif
{
}
void TemplateLock::setup() { void TemplateLock::setup() {
if (!this->f_.has_value()) if (!this->f_.has_value())
@@ -22,34 +42,55 @@ void TemplateLock::loop() {
} }
} }
void TemplateLock::control(const lock::LockCall &call) { void TemplateLock::control(const lock::LockCall &call) {
#if defined(USE_TEMPLATE_LOCK_LOCK_TRIGGER) || defined(USE_TEMPLATE_LOCK_UNLOCK_TRIGGER) || \
defined(USE_TEMPLATE_LOCK_OPEN_TRIGGER)
if (this->prev_trigger_ != nullptr) { if (this->prev_trigger_ != nullptr) {
this->prev_trigger_->stop_action(); this->prev_trigger_->stop_action();
} }
#endif
auto state = *call.get_state(); auto state = *call.get_state();
#ifdef USE_TEMPLATE_LOCK_LOCK_TRIGGER
if (state == LOCK_STATE_LOCKED) { if (state == LOCK_STATE_LOCKED) {
this->prev_trigger_ = this->lock_trigger_; this->prev_trigger_ = this->lock_trigger_;
this->lock_trigger_->trigger(); this->lock_trigger_->trigger();
} else if (state == LOCK_STATE_UNLOCKED) { }
#endif
#ifdef USE_TEMPLATE_LOCK_UNLOCK_TRIGGER
#ifdef USE_TEMPLATE_LOCK_LOCK_TRIGGER
else
#endif
if (state == LOCK_STATE_UNLOCKED) {
this->prev_trigger_ = this->unlock_trigger_; this->prev_trigger_ = this->unlock_trigger_;
this->unlock_trigger_->trigger(); this->unlock_trigger_->trigger();
} }
#endif
if (this->optimistic_) if (this->optimistic_)
this->publish_state(state); this->publish_state(state);
} }
void TemplateLock::open_latch() { void TemplateLock::open_latch() {
#ifdef USE_TEMPLATE_LOCK_OPEN_TRIGGER
#if defined(USE_TEMPLATE_LOCK_LOCK_TRIGGER) || defined(USE_TEMPLATE_LOCK_UNLOCK_TRIGGER)
if (this->prev_trigger_ != nullptr) { if (this->prev_trigger_ != nullptr) {
this->prev_trigger_->stop_action(); this->prev_trigger_->stop_action();
} }
#endif
this->prev_trigger_ = this->open_trigger_; this->prev_trigger_ = this->open_trigger_;
this->open_trigger_->trigger(); this->open_trigger_->trigger();
#endif
} }
void TemplateLock::set_optimistic(bool optimistic) { this->optimistic_ = optimistic; } void TemplateLock::set_optimistic(bool optimistic) { this->optimistic_ = optimistic; }
float TemplateLock::get_setup_priority() const { return setup_priority::HARDWARE; } float TemplateLock::get_setup_priority() const { return setup_priority::HARDWARE; }
#ifdef USE_TEMPLATE_LOCK_LOCK_TRIGGER
Trigger<> *TemplateLock::get_lock_trigger() const { return this->lock_trigger_; } Trigger<> *TemplateLock::get_lock_trigger() const { return this->lock_trigger_; }
#endif
#ifdef USE_TEMPLATE_LOCK_UNLOCK_TRIGGER
Trigger<> *TemplateLock::get_unlock_trigger() const { return this->unlock_trigger_; } Trigger<> *TemplateLock::get_unlock_trigger() const { return this->unlock_trigger_; }
#endif
#ifdef USE_TEMPLATE_LOCK_OPEN_TRIGGER
Trigger<> *TemplateLock::get_open_trigger() const { return this->open_trigger_; } Trigger<> *TemplateLock::get_open_trigger() const { return this->open_trigger_; }
#endif
void TemplateLock::dump_config() { void TemplateLock::dump_config() {
LOG_LOCK("", "Template Lock", this); LOG_LOCK("", "Template Lock", this);
ESP_LOGCONFIG(TAG, " Optimistic: %s", YESNO(this->optimistic_)); ESP_LOGCONFIG(TAG, " Optimistic: %s", YESNO(this->optimistic_));

View File

@@ -15,9 +15,15 @@ class TemplateLock final : public lock::Lock, public Component {
void dump_config() override; void dump_config() override;
template<typename F> void set_state_lambda(F &&f) { this->f_.set(std::forward<F>(f)); } template<typename F> void set_state_lambda(F &&f) { this->f_.set(std::forward<F>(f)); }
#ifdef USE_TEMPLATE_LOCK_LOCK_TRIGGER
Trigger<> *get_lock_trigger() const; Trigger<> *get_lock_trigger() const;
#endif
#ifdef USE_TEMPLATE_LOCK_UNLOCK_TRIGGER
Trigger<> *get_unlock_trigger() const; Trigger<> *get_unlock_trigger() const;
#endif
#ifdef USE_TEMPLATE_LOCK_OPEN_TRIGGER
Trigger<> *get_open_trigger() const; Trigger<> *get_open_trigger() const;
#endif
void set_optimistic(bool optimistic); void set_optimistic(bool optimistic);
void loop() override; void loop() override;
@@ -29,10 +35,19 @@ class TemplateLock final : public lock::Lock, public Component {
TemplateLambda<lock::LockState> f_; TemplateLambda<lock::LockState> f_;
bool optimistic_{false}; bool optimistic_{false};
#ifdef USE_TEMPLATE_LOCK_LOCK_TRIGGER
Trigger<> *lock_trigger_; Trigger<> *lock_trigger_;
#endif
#ifdef USE_TEMPLATE_LOCK_UNLOCK_TRIGGER
Trigger<> *unlock_trigger_; Trigger<> *unlock_trigger_;
#endif
#ifdef USE_TEMPLATE_LOCK_OPEN_TRIGGER
Trigger<> *open_trigger_; Trigger<> *open_trigger_;
#endif
#if defined(USE_TEMPLATE_LOCK_LOCK_TRIGGER) || defined(USE_TEMPLATE_LOCK_UNLOCK_TRIGGER) || \
defined(USE_TEMPLATE_LOCK_OPEN_TRIGGER)
Trigger<> *prev_trigger_{nullptr}; Trigger<> *prev_trigger_{nullptr};
#endif
}; };
} // namespace esphome::template_ } // namespace esphome::template_

View File

@@ -89,6 +89,7 @@ async def to_code(config):
cg.add(var.set_restore_value(config[CONF_RESTORE_VALUE])) cg.add(var.set_restore_value(config[CONF_RESTORE_VALUE]))
if CONF_SET_ACTION in config: if CONF_SET_ACTION in config:
cg.add_define("USE_TEMPLATE_NUMBER_SET_TRIGGER")
await automation.build_automation( await automation.build_automation(
var.get_set_trigger(), [(float, "x")], config[CONF_SET_ACTION] var.get_set_trigger(), [(float, "x")], config[CONF_SET_ACTION]
) )

View File

@@ -36,7 +36,9 @@ void TemplateNumber::update() {
} }
void TemplateNumber::control(float value) { void TemplateNumber::control(float value) {
#ifdef USE_TEMPLATE_NUMBER_SET_TRIGGER
this->set_trigger_->trigger(value); this->set_trigger_->trigger(value);
#endif
if (this->optimistic_) if (this->optimistic_)
this->publish_state(value); this->publish_state(value);

View File

@@ -17,7 +17,9 @@ class TemplateNumber final : public number::Number, public PollingComponent {
void dump_config() override; void dump_config() override;
float get_setup_priority() const override { return setup_priority::HARDWARE; } float get_setup_priority() const override { return setup_priority::HARDWARE; }
#ifdef USE_TEMPLATE_NUMBER_SET_TRIGGER
Trigger<float> *get_set_trigger() const { return set_trigger_; } Trigger<float> *get_set_trigger() const { return set_trigger_; }
#endif
void set_optimistic(bool optimistic) { optimistic_ = optimistic; } void set_optimistic(bool optimistic) { optimistic_ = optimistic; }
void set_initial_value(float initial_value) { initial_value_ = initial_value; } void set_initial_value(float initial_value) { initial_value_ = initial_value; }
void set_restore_value(bool restore_value) { this->restore_value_ = restore_value; } void set_restore_value(bool restore_value) { this->restore_value_ = restore_value; }
@@ -27,7 +29,9 @@ class TemplateNumber final : public number::Number, public PollingComponent {
bool optimistic_{false}; bool optimistic_{false};
float initial_value_{NAN}; float initial_value_{NAN};
bool restore_value_{false}; bool restore_value_{false};
#ifdef USE_TEMPLATE_NUMBER_SET_TRIGGER
Trigger<float> *set_trigger_ = new Trigger<float>(); Trigger<float> *set_trigger_ = new Trigger<float>();
#endif
TemplateLambda<float> f_; TemplateLambda<float> f_;
ESPPreferenceObject pref_; ESPPreferenceObject pref_;

View File

@@ -87,6 +87,7 @@ async def to_code(config):
cg.add(var.set_restore_value(True)) cg.add(var.set_restore_value(True))
if CONF_SET_ACTION in config: if CONF_SET_ACTION in config:
cg.add_define("USE_TEMPLATE_SELECT_SET_TRIGGER")
await automation.build_automation( await automation.build_automation(
var.get_set_trigger(), [(cg.StringRef, "x")], config[CONF_SET_ACTION] var.get_set_trigger(), [(cg.StringRef, "x")], config[CONF_SET_ACTION]
) )

View File

@@ -41,7 +41,9 @@ void TemplateSelect::update() {
} }
void TemplateSelect::control(size_t index) { void TemplateSelect::control(size_t index) {
#ifdef USE_TEMPLATE_SELECT_SET_TRIGGER
this->set_trigger_->trigger(StringRef(this->option_at(index))); this->set_trigger_->trigger(StringRef(this->option_at(index)));
#endif
if (this->optimistic_) if (this->optimistic_)
this->publish_state(index); this->publish_state(index);

View File

@@ -18,7 +18,9 @@ class TemplateSelect final : public select::Select, public PollingComponent {
void dump_config() override; void dump_config() override;
float get_setup_priority() const override { return setup_priority::HARDWARE; } float get_setup_priority() const override { return setup_priority::HARDWARE; }
#ifdef USE_TEMPLATE_SELECT_SET_TRIGGER
Trigger<StringRef> *get_set_trigger() const { return this->set_trigger_; } Trigger<StringRef> *get_set_trigger() const { return this->set_trigger_; }
#endif
void set_optimistic(bool optimistic) { this->optimistic_ = optimistic; } void set_optimistic(bool optimistic) { this->optimistic_ = optimistic; }
void set_initial_option_index(size_t initial_option_index) { this->initial_option_index_ = initial_option_index; } void set_initial_option_index(size_t initial_option_index) { this->initial_option_index_ = initial_option_index; }
void set_restore_value(bool restore_value) { this->restore_value_ = restore_value; } void set_restore_value(bool restore_value) { this->restore_value_ = restore_value; }
@@ -28,7 +30,9 @@ class TemplateSelect final : public select::Select, public PollingComponent {
bool optimistic_ = false; bool optimistic_ = false;
size_t initial_option_index_{0}; size_t initial_option_index_{0};
bool restore_value_ = false; bool restore_value_ = false;
#ifdef USE_TEMPLATE_SELECT_SET_TRIGGER
Trigger<StringRef> *set_trigger_ = new Trigger<StringRef>(); Trigger<StringRef> *set_trigger_ = new Trigger<StringRef>();
#endif
TemplateLambda<std::string> f_; TemplateLambda<std::string> f_;
ESPPreferenceObject pref_; ESPPreferenceObject pref_;

View File

@@ -60,10 +60,12 @@ async def to_code(config):
) )
cg.add(var.set_state_lambda(template_)) cg.add(var.set_state_lambda(template_))
if CONF_TURN_OFF_ACTION in config: if CONF_TURN_OFF_ACTION in config:
cg.add_define("USE_TEMPLATE_SWITCH_TURN_OFF_TRIGGER")
await automation.build_automation( await automation.build_automation(
var.get_turn_off_trigger(), [], config[CONF_TURN_OFF_ACTION] var.get_turn_off_trigger(), [], config[CONF_TURN_OFF_ACTION]
) )
if CONF_TURN_ON_ACTION in config: if CONF_TURN_ON_ACTION in config:
cg.add_define("USE_TEMPLATE_SWITCH_TURN_ON_TRIGGER")
await automation.build_automation( await automation.build_automation(
var.get_turn_on_trigger(), [], config[CONF_TURN_ON_ACTION] var.get_turn_on_trigger(), [], config[CONF_TURN_ON_ACTION]
) )

View File

@@ -5,7 +5,18 @@ namespace esphome::template_ {
static const char *const TAG = "template.switch"; static const char *const TAG = "template.switch";
TemplateSwitch::TemplateSwitch() : turn_on_trigger_(new Trigger<>()), turn_off_trigger_(new Trigger<>()) {} TemplateSwitch::TemplateSwitch()
#ifdef USE_TEMPLATE_SWITCH_TURN_ON_TRIGGER
: turn_on_trigger_(new Trigger<>())
#ifdef USE_TEMPLATE_SWITCH_TURN_OFF_TRIGGER
,
turn_off_trigger_(new Trigger<>())
#endif
#elif defined(USE_TEMPLATE_SWITCH_TURN_OFF_TRIGGER)
: turn_off_trigger_(new Trigger<>())
#endif
{
}
void TemplateSwitch::loop() { void TemplateSwitch::loop() {
auto s = this->f_(); auto s = this->f_();
@@ -14,16 +25,22 @@ void TemplateSwitch::loop() {
} }
} }
void TemplateSwitch::write_state(bool state) { void TemplateSwitch::write_state(bool state) {
#if defined(USE_TEMPLATE_SWITCH_TURN_ON_TRIGGER) || defined(USE_TEMPLATE_SWITCH_TURN_OFF_TRIGGER)
if (this->prev_trigger_ != nullptr) { if (this->prev_trigger_ != nullptr) {
this->prev_trigger_->stop_action(); this->prev_trigger_->stop_action();
} }
#endif
if (state) { if (state) {
#ifdef USE_TEMPLATE_SWITCH_TURN_ON_TRIGGER
this->prev_trigger_ = this->turn_on_trigger_; this->prev_trigger_ = this->turn_on_trigger_;
this->turn_on_trigger_->trigger(); this->turn_on_trigger_->trigger();
#endif
} else { } else {
#ifdef USE_TEMPLATE_SWITCH_TURN_OFF_TRIGGER
this->prev_trigger_ = this->turn_off_trigger_; this->prev_trigger_ = this->turn_off_trigger_;
this->turn_off_trigger_->trigger(); this->turn_off_trigger_->trigger();
#endif
} }
if (this->optimistic_) if (this->optimistic_)
@@ -32,8 +49,12 @@ void TemplateSwitch::write_state(bool state) {
void TemplateSwitch::set_optimistic(bool optimistic) { this->optimistic_ = optimistic; } void TemplateSwitch::set_optimistic(bool optimistic) { this->optimistic_ = optimistic; }
bool TemplateSwitch::assumed_state() { return this->assumed_state_; } bool TemplateSwitch::assumed_state() { return this->assumed_state_; }
float TemplateSwitch::get_setup_priority() const { return setup_priority::HARDWARE - 2.0f; } float TemplateSwitch::get_setup_priority() const { return setup_priority::HARDWARE - 2.0f; }
#ifdef USE_TEMPLATE_SWITCH_TURN_ON_TRIGGER
Trigger<> *TemplateSwitch::get_turn_on_trigger() const { return this->turn_on_trigger_; } Trigger<> *TemplateSwitch::get_turn_on_trigger() const { return this->turn_on_trigger_; }
#endif
#ifdef USE_TEMPLATE_SWITCH_TURN_OFF_TRIGGER
Trigger<> *TemplateSwitch::get_turn_off_trigger() const { return this->turn_off_trigger_; } Trigger<> *TemplateSwitch::get_turn_off_trigger() const { return this->turn_off_trigger_; }
#endif
void TemplateSwitch::setup() { void TemplateSwitch::setup() {
if (!this->f_.has_value()) if (!this->f_.has_value())
this->disable_loop(); this->disable_loop();

View File

@@ -15,8 +15,12 @@ class TemplateSwitch final : public switch_::Switch, public Component {
void dump_config() override; void dump_config() override;
template<typename F> void set_state_lambda(F &&f) { this->f_.set(std::forward<F>(f)); } template<typename F> void set_state_lambda(F &&f) { this->f_.set(std::forward<F>(f)); }
#ifdef USE_TEMPLATE_SWITCH_TURN_ON_TRIGGER
Trigger<> *get_turn_on_trigger() const; Trigger<> *get_turn_on_trigger() const;
#endif
#ifdef USE_TEMPLATE_SWITCH_TURN_OFF_TRIGGER
Trigger<> *get_turn_off_trigger() const; Trigger<> *get_turn_off_trigger() const;
#endif
void set_optimistic(bool optimistic); void set_optimistic(bool optimistic);
void set_assumed_state(bool assumed_state); void set_assumed_state(bool assumed_state);
void loop() override; void loop() override;
@@ -31,9 +35,15 @@ class TemplateSwitch final : public switch_::Switch, public Component {
TemplateLambda<bool> f_; TemplateLambda<bool> f_;
bool optimistic_{false}; bool optimistic_{false};
bool assumed_state_{false}; bool assumed_state_{false};
#ifdef USE_TEMPLATE_SWITCH_TURN_ON_TRIGGER
Trigger<> *turn_on_trigger_; Trigger<> *turn_on_trigger_;
#endif
#ifdef USE_TEMPLATE_SWITCH_TURN_OFF_TRIGGER
Trigger<> *turn_off_trigger_; Trigger<> *turn_off_trigger_;
#endif
#if defined(USE_TEMPLATE_SWITCH_TURN_ON_TRIGGER) || defined(USE_TEMPLATE_SWITCH_TURN_OFF_TRIGGER)
Trigger<> *prev_trigger_{nullptr}; Trigger<> *prev_trigger_{nullptr};
#endif
}; };
} // namespace esphome::template_ } // namespace esphome::template_

View File

@@ -89,6 +89,7 @@ async def to_code(config):
cg.add(var.set_value_saver(saver)) cg.add(var.set_value_saver(saver))
if CONF_SET_ACTION in config: if CONF_SET_ACTION in config:
cg.add_define("USE_TEMPLATE_TEXT_SET_TRIGGER")
await automation.build_automation( await automation.build_automation(
var.get_set_trigger(), [(cg.std_string, "x")], config[CONF_SET_ACTION] var.get_set_trigger(), [(cg.std_string, "x")], config[CONF_SET_ACTION]
) )

View File

@@ -47,7 +47,9 @@ void TemplateText::update() {
} }
void TemplateText::control(const std::string &value) { void TemplateText::control(const std::string &value) {
#ifdef USE_TEMPLATE_TEXT_SET_TRIGGER
this->set_trigger_->trigger(value); this->set_trigger_->trigger(value);
#endif
if (this->optimistic_) if (this->optimistic_)
this->publish_state(value); this->publish_state(value);

View File

@@ -68,7 +68,9 @@ class TemplateText final : public text::Text, public PollingComponent {
void dump_config() override; void dump_config() override;
float get_setup_priority() const override { return setup_priority::HARDWARE; } float get_setup_priority() const override { return setup_priority::HARDWARE; }
#ifdef USE_TEMPLATE_TEXT_SET_TRIGGER
Trigger<std::string> *get_set_trigger() const { return this->set_trigger_; } Trigger<std::string> *get_set_trigger() const { return this->set_trigger_; }
#endif
void set_optimistic(bool optimistic) { this->optimistic_ = optimistic; } void set_optimistic(bool optimistic) { this->optimistic_ = optimistic; }
void set_initial_value(const char *initial_value) { this->initial_value_ = initial_value; } void set_initial_value(const char *initial_value) { this->initial_value_ = initial_value; }
/// Prevent accidental use of std::string which would dangle /// Prevent accidental use of std::string which would dangle
@@ -79,7 +81,9 @@ class TemplateText final : public text::Text, public PollingComponent {
void control(const std::string &value) override; void control(const std::string &value) override;
bool optimistic_ = false; bool optimistic_ = false;
const char *initial_value_{nullptr}; const char *initial_value_{nullptr};
#ifdef USE_TEMPLATE_TEXT_SET_TRIGGER
Trigger<std::string> *set_trigger_ = new Trigger<std::string>(); Trigger<std::string> *set_trigger_ = new Trigger<std::string>();
#endif
TemplateLambda<std::string> f_{}; TemplateLambda<std::string> f_{};
TemplateTextSaverBase *pref_ = nullptr; TemplateTextSaverBase *pref_ = nullptr;

View File

@@ -78,16 +78,19 @@ async def to_code(config):
var.get_close_trigger(), [], close_action_config var.get_close_trigger(), [], close_action_config
) )
if stop_action_config := config.get(CONF_STOP_ACTION): if stop_action_config := config.get(CONF_STOP_ACTION):
cg.add_define("USE_TEMPLATE_VALVE_STOP_TRIGGER")
await automation.build_automation( await automation.build_automation(
var.get_stop_trigger(), [], stop_action_config var.get_stop_trigger(), [], stop_action_config
) )
cg.add(var.set_has_stop(True)) cg.add(var.set_has_stop(True))
if toggle_action_config := config.get(CONF_TOGGLE_ACTION): if toggle_action_config := config.get(CONF_TOGGLE_ACTION):
cg.add_define("USE_TEMPLATE_VALVE_TOGGLE_TRIGGER")
await automation.build_automation( await automation.build_automation(
var.get_toggle_trigger(), [], toggle_action_config var.get_toggle_trigger(), [], toggle_action_config
) )
cg.add(var.set_has_toggle(True)) cg.add(var.set_has_toggle(True))
if position_action_config := config.get(CONF_POSITION_ACTION): if position_action_config := config.get(CONF_POSITION_ACTION):
cg.add_define("USE_TEMPLATE_VALVE_POSITION_TRIGGER")
await automation.build_automation( await automation.build_automation(
var.get_position_trigger(), [(float, "pos")], position_action_config var.get_position_trigger(), [(float, "pos")], position_action_config
) )

View File

@@ -9,10 +9,21 @@ static const char *const TAG = "template.valve";
TemplateValve::TemplateValve() TemplateValve::TemplateValve()
: open_trigger_(new Trigger<>()), : open_trigger_(new Trigger<>()),
close_trigger_(new Trigger<>), close_trigger_(new Trigger<>())
stop_trigger_(new Trigger<>()), #ifdef USE_TEMPLATE_VALVE_STOP_TRIGGER
toggle_trigger_(new Trigger<>()), ,
position_trigger_(new Trigger<float>()) {} stop_trigger_(new Trigger<>())
#endif
#ifdef USE_TEMPLATE_VALVE_TOGGLE_TRIGGER
,
toggle_trigger_(new Trigger<>())
#endif
#ifdef USE_TEMPLATE_VALVE_POSITION_TRIGGER
,
position_trigger_(new Trigger<float>())
#endif
{
}
void TemplateValve::setup() { void TemplateValve::setup() {
switch (this->restore_mode_) { switch (this->restore_mode_) {
@@ -58,8 +69,12 @@ float TemplateValve::get_setup_priority() const { return setup_priority::HARDWAR
Trigger<> *TemplateValve::get_open_trigger() const { return this->open_trigger_; } Trigger<> *TemplateValve::get_open_trigger() const { return this->open_trigger_; }
Trigger<> *TemplateValve::get_close_trigger() const { return this->close_trigger_; } Trigger<> *TemplateValve::get_close_trigger() const { return this->close_trigger_; }
#ifdef USE_TEMPLATE_VALVE_STOP_TRIGGER
Trigger<> *TemplateValve::get_stop_trigger() const { return this->stop_trigger_; } Trigger<> *TemplateValve::get_stop_trigger() const { return this->stop_trigger_; }
#endif
#ifdef USE_TEMPLATE_VALVE_TOGGLE_TRIGGER
Trigger<> *TemplateValve::get_toggle_trigger() const { return this->toggle_trigger_; } Trigger<> *TemplateValve::get_toggle_trigger() const { return this->toggle_trigger_; }
#endif
void TemplateValve::dump_config() { void TemplateValve::dump_config() {
LOG_VALVE("", "Template Valve", this); LOG_VALVE("", "Template Valve", this);
@@ -70,18 +85,22 @@ void TemplateValve::dump_config() {
} }
void TemplateValve::control(const ValveCall &call) { void TemplateValve::control(const ValveCall &call) {
#ifdef USE_TEMPLATE_VALVE_STOP_TRIGGER
if (call.get_stop()) { if (call.get_stop()) {
this->stop_prev_trigger_(); this->stop_prev_trigger_();
this->stop_trigger_->trigger(); this->stop_trigger_->trigger();
this->prev_command_trigger_ = this->stop_trigger_; this->prev_command_trigger_ = this->stop_trigger_;
this->publish_state(); this->publish_state();
} }
#endif
#ifdef USE_TEMPLATE_VALVE_TOGGLE_TRIGGER
if (call.get_toggle().has_value()) { if (call.get_toggle().has_value()) {
this->stop_prev_trigger_(); this->stop_prev_trigger_();
this->toggle_trigger_->trigger(); this->toggle_trigger_->trigger();
this->prev_command_trigger_ = this->toggle_trigger_; this->prev_command_trigger_ = this->toggle_trigger_;
this->publish_state(); this->publish_state();
} }
#endif
if (call.get_position().has_value()) { if (call.get_position().has_value()) {
auto pos = *call.get_position(); auto pos = *call.get_position();
this->stop_prev_trigger_(); this->stop_prev_trigger_();
@@ -92,9 +111,12 @@ void TemplateValve::control(const ValveCall &call) {
} else if (pos == VALVE_CLOSED) { } else if (pos == VALVE_CLOSED) {
this->close_trigger_->trigger(); this->close_trigger_->trigger();
this->prev_command_trigger_ = this->close_trigger_; this->prev_command_trigger_ = this->close_trigger_;
} else { }
#ifdef USE_TEMPLATE_VALVE_POSITION_TRIGGER
else {
this->position_trigger_->trigger(pos); this->position_trigger_->trigger(pos);
} }
#endif
if (this->optimistic_) { if (this->optimistic_) {
this->position = pos; this->position = pos;
@@ -107,17 +129,37 @@ void TemplateValve::control(const ValveCall &call) {
ValveTraits TemplateValve::get_traits() { ValveTraits TemplateValve::get_traits() {
auto traits = ValveTraits(); auto traits = ValveTraits();
traits.set_is_assumed_state(this->assumed_state_); traits.set_is_assumed_state(this->assumed_state_);
#ifdef USE_TEMPLATE_VALVE_STOP_TRIGGER
traits.set_supports_stop(this->has_stop_); traits.set_supports_stop(this->has_stop_);
#else
traits.set_supports_stop(false);
#endif
#ifdef USE_TEMPLATE_VALVE_TOGGLE_TRIGGER
traits.set_supports_toggle(this->has_toggle_); traits.set_supports_toggle(this->has_toggle_);
#else
traits.set_supports_toggle(false);
#endif
#ifdef USE_TEMPLATE_VALVE_POSITION_TRIGGER
traits.set_supports_position(this->has_position_); traits.set_supports_position(this->has_position_);
#else
traits.set_supports_position(false);
#endif
return traits; return traits;
} }
#ifdef USE_TEMPLATE_VALVE_POSITION_TRIGGER
Trigger<float> *TemplateValve::get_position_trigger() const { return this->position_trigger_; } Trigger<float> *TemplateValve::get_position_trigger() const { return this->position_trigger_; }
#endif
#ifdef USE_TEMPLATE_VALVE_STOP_TRIGGER
void TemplateValve::set_has_stop(bool has_stop) { this->has_stop_ = has_stop; } void TemplateValve::set_has_stop(bool has_stop) { this->has_stop_ = has_stop; }
#endif
#ifdef USE_TEMPLATE_VALVE_TOGGLE_TRIGGER
void TemplateValve::set_has_toggle(bool has_toggle) { this->has_toggle_ = has_toggle; } void TemplateValve::set_has_toggle(bool has_toggle) { this->has_toggle_ = has_toggle; }
#endif
#ifdef USE_TEMPLATE_VALVE_POSITION_TRIGGER
void TemplateValve::set_has_position(bool has_position) { this->has_position_ = has_position; } void TemplateValve::set_has_position(bool has_position) { this->has_position_ = has_position; }
#endif
void TemplateValve::stop_prev_trigger_() { void TemplateValve::stop_prev_trigger_() {
if (this->prev_command_trigger_ != nullptr) { if (this->prev_command_trigger_ != nullptr) {

View File

@@ -20,14 +20,26 @@ class TemplateValve final : public valve::Valve, public Component {
template<typename F> void set_state_lambda(F &&f) { this->state_f_.set(std::forward<F>(f)); } template<typename F> void set_state_lambda(F &&f) { this->state_f_.set(std::forward<F>(f)); }
Trigger<> *get_open_trigger() const; Trigger<> *get_open_trigger() const;
Trigger<> *get_close_trigger() const; Trigger<> *get_close_trigger() const;
#ifdef USE_TEMPLATE_VALVE_STOP_TRIGGER
Trigger<> *get_stop_trigger() const; Trigger<> *get_stop_trigger() const;
#endif
#ifdef USE_TEMPLATE_VALVE_TOGGLE_TRIGGER
Trigger<> *get_toggle_trigger() const; Trigger<> *get_toggle_trigger() const;
#endif
#ifdef USE_TEMPLATE_VALVE_POSITION_TRIGGER
Trigger<float> *get_position_trigger() const; Trigger<float> *get_position_trigger() const;
#endif
void set_optimistic(bool optimistic); void set_optimistic(bool optimistic);
void set_assumed_state(bool assumed_state); void set_assumed_state(bool assumed_state);
#ifdef USE_TEMPLATE_VALVE_STOP_TRIGGER
void set_has_stop(bool has_stop); void set_has_stop(bool has_stop);
#endif
#ifdef USE_TEMPLATE_VALVE_POSITION_TRIGGER
void set_has_position(bool has_position); void set_has_position(bool has_position);
#endif
#ifdef USE_TEMPLATE_VALVE_TOGGLE_TRIGGER
void set_has_toggle(bool has_toggle); void set_has_toggle(bool has_toggle);
#endif
void set_restore_mode(TemplateValveRestoreMode restore_mode) { restore_mode_ = restore_mode; } void set_restore_mode(TemplateValveRestoreMode restore_mode) { restore_mode_ = restore_mode; }
void setup() override; void setup() override;
@@ -41,19 +53,32 @@ class TemplateValve final : public valve::Valve, public Component {
valve::ValveTraits get_traits() override; valve::ValveTraits get_traits() override;
void stop_prev_trigger_(); void stop_prev_trigger_();
// Ordered to minimize padding on 32-bit: 4-byte members first, then smaller
TemplateValveRestoreMode restore_mode_{VALVE_NO_RESTORE}; TemplateValveRestoreMode restore_mode_{VALVE_NO_RESTORE};
TemplateLambda<float> state_f_; TemplateLambda<float> state_f_;
bool assumed_state_{false};
bool optimistic_{false};
Trigger<> *open_trigger_; Trigger<> *open_trigger_;
Trigger<> *close_trigger_; Trigger<> *close_trigger_;
bool has_stop_{false};
bool has_toggle_{false};
Trigger<> *stop_trigger_;
Trigger<> *toggle_trigger_;
Trigger<> *prev_command_trigger_{nullptr}; Trigger<> *prev_command_trigger_{nullptr};
#ifdef USE_TEMPLATE_VALVE_STOP_TRIGGER
Trigger<> *stop_trigger_;
#endif
#ifdef USE_TEMPLATE_VALVE_TOGGLE_TRIGGER
Trigger<> *toggle_trigger_;
#endif
#ifdef USE_TEMPLATE_VALVE_POSITION_TRIGGER
Trigger<float> *position_trigger_; Trigger<float> *position_trigger_;
#endif
bool assumed_state_{false};
bool optimistic_{false};
#ifdef USE_TEMPLATE_VALVE_STOP_TRIGGER
bool has_stop_{false};
#endif
#ifdef USE_TEMPLATE_VALVE_TOGGLE_TRIGGER
bool has_toggle_{false};
#endif
#ifdef USE_TEMPLATE_VALVE_POSITION_TRIGGER
bool has_position_{false}; bool has_position_{false};
#endif
}; };
} // namespace esphome::template_ } // namespace esphome::template_

View File

@@ -64,6 +64,7 @@ async def to_code(config: ConfigType) -> None:
cg.add(var.set_optimistic(config[CONF_OPTIMISTIC])) cg.add(var.set_optimistic(config[CONF_OPTIMISTIC]))
if CONF_SET_ACTION in config: if CONF_SET_ACTION in config:
cg.add_define("USE_TEMPLATE_WATER_HEATER_SET_TRIGGER")
await automation.build_automation( await automation.build_automation(
var.get_set_trigger(), [], config[CONF_SET_ACTION] var.get_set_trigger(), [], config[CONF_SET_ACTION]
) )

View File

@@ -5,7 +5,12 @@ namespace esphome::template_ {
static const char *const TAG = "template.water_heater"; static const char *const TAG = "template.water_heater";
TemplateWaterHeater::TemplateWaterHeater() : set_trigger_(new Trigger<>()) {} TemplateWaterHeater::TemplateWaterHeater()
#ifdef USE_TEMPLATE_WATER_HEATER_SET_TRIGGER
: set_trigger_(new Trigger<>())
#endif
{
}
void TemplateWaterHeater::setup() { void TemplateWaterHeater::setup() {
if (this->restore_mode_ == TemplateWaterHeaterRestoreMode::WATER_HEATER_RESTORE || if (this->restore_mode_ == TemplateWaterHeaterRestoreMode::WATER_HEATER_RESTORE ||
@@ -78,7 +83,9 @@ void TemplateWaterHeater::control(const water_heater::WaterHeaterCall &call) {
} }
} }
#ifdef USE_TEMPLATE_WATER_HEATER_SET_TRIGGER
this->set_trigger_->trigger(); this->set_trigger_->trigger();
#endif
if (this->optimistic_) { if (this->optimistic_) {
this->publish_state(); this->publish_state();

View File

@@ -28,7 +28,9 @@ class TemplateWaterHeater : public Component, public water_heater::WaterHeater {
this->supported_modes_ = modes; this->supported_modes_ = modes;
} }
#ifdef USE_TEMPLATE_WATER_HEATER_SET_TRIGGER
Trigger<> *get_set_trigger() const { return this->set_trigger_; } Trigger<> *get_set_trigger() const { return this->set_trigger_; }
#endif
void setup() override; void setup() override;
void loop() override; void loop() override;
@@ -42,7 +44,9 @@ class TemplateWaterHeater : public Component, public water_heater::WaterHeater {
water_heater::WaterHeaterTraits traits() override; water_heater::WaterHeaterTraits traits() override;
// Ordered to minimize padding on 32-bit: 4-byte members first, then smaller // Ordered to minimize padding on 32-bit: 4-byte members first, then smaller
#ifdef USE_TEMPLATE_WATER_HEATER_SET_TRIGGER
Trigger<> *set_trigger_; Trigger<> *set_trigger_;
#endif
TemplateLambda<float> current_temperature_f_; TemplateLambda<float> current_temperature_f_;
TemplateLambda<water_heater::WaterHeaterMode> mode_f_; TemplateLambda<water_heater::WaterHeaterMode> mode_f_;
TemplateWaterHeaterRestoreMode restore_mode_{WATER_HEATER_NO_RESTORE}; TemplateWaterHeaterRestoreMode restore_mode_{WATER_HEATER_NO_RESTORE};

View File

@@ -2,7 +2,7 @@
#include "esphome/core/helpers.h" #include "esphome/core/helpers.h"
#include "esphome/core/log.h" #include "esphome/core/log.h"
#include <vector> #include <array>
namespace esphome { namespace esphome {
namespace tx20 { namespace tx20 {
@@ -45,25 +45,25 @@ std::string Tx20Component::get_wind_cardinal_direction() const { return this->wi
void Tx20Component::decode_and_publish_() { void Tx20Component::decode_and_publish_() {
ESP_LOGVV(TAG, "Decode Tx20"); ESP_LOGVV(TAG, "Decode Tx20");
std::string string_buffer; std::array<bool, MAX_BUFFER_SIZE> bit_buffer{};
std::string string_buffer_2; size_t bit_pos = 0;
std::vector<bool> bit_buffer;
bool current_bit = true; bool current_bit = true;
// Cap at MAX_BUFFER_SIZE - 1 to prevent out-of-bounds access (buffer_index can exceed MAX_BUFFER_SIZE in ISR)
const int max_buffer_index =
std::min(static_cast<int>(this->store_.buffer_index), static_cast<int>(MAX_BUFFER_SIZE - 1));
for (int i = 1; i <= this->store_.buffer_index; i++) { for (int i = 1; i <= max_buffer_index; i++) {
string_buffer_2 += to_string(this->store_.buffer[i]) + ", ";
uint8_t repeat = this->store_.buffer[i] / TX20_BIT_TIME; uint8_t repeat = this->store_.buffer[i] / TX20_BIT_TIME;
// ignore segments at the end that were too short // ignore segments at the end that were too short
string_buffer.append(repeat, current_bit ? '1' : '0'); for (uint8_t j = 0; j < repeat && bit_pos < MAX_BUFFER_SIZE; j++) {
bit_buffer.insert(bit_buffer.end(), repeat, current_bit); bit_buffer[bit_pos++] = current_bit;
}
current_bit = !current_bit; current_bit = !current_bit;
} }
current_bit = !current_bit; current_bit = !current_bit;
if (string_buffer.length() < MAX_BUFFER_SIZE) { size_t bits_before_padding = bit_pos;
uint8_t remain = MAX_BUFFER_SIZE - string_buffer.length(); while (bit_pos < MAX_BUFFER_SIZE) {
string_buffer_2 += to_string(remain) + ", "; bit_buffer[bit_pos++] = current_bit;
string_buffer.append(remain, current_bit ? '1' : '0');
bit_buffer.insert(bit_buffer.end(), remain, current_bit);
} }
uint8_t tx20_sa = 0; uint8_t tx20_sa = 0;
@@ -108,8 +108,24 @@ void Tx20Component::decode_and_publish_() {
// 2. Check received checksum matches calculated checksum // 2. Check received checksum matches calculated checksum
// 3. Check that Wind Direction matches Wind Direction (Inverted) // 3. Check that Wind Direction matches Wind Direction (Inverted)
// 4. Check that Wind Speed matches Wind Speed (Inverted) // 4. Check that Wind Speed matches Wind Speed (Inverted)
ESP_LOGVV(TAG, "BUFFER %s", string_buffer_2.c_str()); #if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERY_VERBOSE
ESP_LOGVV(TAG, "Decoded bits %s", string_buffer.c_str()); // Build debug strings from completed data
char debug_buf[320]; // buffer values: max 40 entries * 7 chars each
size_t debug_pos = 0;
for (int i = 1; i <= max_buffer_index; i++) {
debug_pos = buf_append_printf(debug_buf, sizeof(debug_buf), debug_pos, "%u, ", this->store_.buffer[i]);
}
if (bits_before_padding < MAX_BUFFER_SIZE) {
buf_append_printf(debug_buf, sizeof(debug_buf), debug_pos, "%zu, ", MAX_BUFFER_SIZE - bits_before_padding);
}
char bits_buf[MAX_BUFFER_SIZE + 1];
for (size_t i = 0; i < MAX_BUFFER_SIZE; i++) {
bits_buf[i] = bit_buffer[i] ? '1' : '0';
}
bits_buf[MAX_BUFFER_SIZE] = '\0';
ESP_LOGVV(TAG, "BUFFER %s", debug_buf);
ESP_LOGVV(TAG, "Decoded bits %s", bits_buf);
#endif
if (tx20_sa == 4) { if (tx20_sa == 4) {
if (chk == tx20_sd) { if (chk == tx20_sd) {

View File

@@ -12,8 +12,8 @@ from esphome.components.packet_transport import (
) )
import esphome.config_validation as cv import esphome.config_validation as cv
from esphome.const import CONF_DATA, CONF_ID, CONF_PORT, CONF_TRIGGER_ID from esphome.const import CONF_DATA, CONF_ID, CONF_PORT, CONF_TRIGGER_ID
from esphome.core import ID, Lambda from esphome.core import ID
from esphome.cpp_generator import ExpressionStatement, MockObj from esphome.cpp_generator import literal
CODEOWNERS = ["@clydebarrow"] CODEOWNERS = ["@clydebarrow"]
DEPENDENCIES = ["network"] DEPENDENCIES = ["network"]
@@ -24,6 +24,8 @@ udp_ns = cg.esphome_ns.namespace("udp")
UDPComponent = udp_ns.class_("UDPComponent", cg.Component) UDPComponent = udp_ns.class_("UDPComponent", cg.Component)
UDPWriteAction = udp_ns.class_("UDPWriteAction", automation.Action) UDPWriteAction = udp_ns.class_("UDPWriteAction", automation.Action)
trigger_args = cg.std_vector.template(cg.uint8) trigger_args = cg.std_vector.template(cg.uint8)
trigger_argname = "data"
trigger_argtype = [(trigger_args, trigger_argname)]
CONF_ADDRESSES = "addresses" CONF_ADDRESSES = "addresses"
CONF_LISTEN_ADDRESS = "listen_address" CONF_LISTEN_ADDRESS = "listen_address"
@@ -111,13 +113,14 @@ async def to_code(config):
cg.add(var.set_addresses([str(addr) for addr in config[CONF_ADDRESSES]])) cg.add(var.set_addresses([str(addr) for addr in config[CONF_ADDRESSES]]))
if on_receive := config.get(CONF_ON_RECEIVE): if on_receive := config.get(CONF_ON_RECEIVE):
on_receive = on_receive[0] on_receive = on_receive[0]
trigger = cg.new_Pvariable(on_receive[CONF_TRIGGER_ID]) trigger_id = cg.new_Pvariable(on_receive[CONF_TRIGGER_ID])
trigger = await automation.build_automation( trigger = await automation.build_automation(
trigger, [(trigger_args, "data")], on_receive trigger_id, trigger_argtype, on_receive
) )
trigger = Lambda(str(ExpressionStatement(trigger.trigger(MockObj("data"))))) trigger_lambda = await cg.process_lambda(
trigger = await cg.process_lambda(trigger, [(trigger_args, "data")]) trigger.trigger(literal(trigger_argname)), trigger_argtype
cg.add(var.add_listener(trigger)) )
cg.add(var.add_listener(trigger_lambda))
cg.add(var.set_should_listen()) cg.add(var.set_should_listen())

View File

@@ -155,8 +155,9 @@ void USBCDCACMInstance::setup() {
return; return;
} }
// Use a larger stack size for (very) verbose logging // Use a larger stack size for very verbose logging
const size_t stack_size = esp_log_level_get(TAG) > ESP_LOG_DEBUG ? USB_TX_TASK_STACK_SIZE_VV : USB_TX_TASK_STACK_SIZE; constexpr size_t stack_size =
ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERY_VERBOSE ? USB_TX_TASK_STACK_SIZE_VV : USB_TX_TASK_STACK_SIZE;
// Create a simple, unique task name per interface // Create a simple, unique task name per interface
char task_name[] = "usb_tx_0"; char task_name[] = "usb_tx_0";

View File

@@ -53,4 +53,4 @@ async def to_code(config):
"lib_ignore", ["ESPAsyncTCP", "AsyncTCP", "AsyncTCP_RP2040W"] "lib_ignore", ["ESPAsyncTCP", "AsyncTCP", "AsyncTCP_RP2040W"]
) )
# https://github.com/ESP32Async/ESPAsyncWebServer/blob/main/library.json # https://github.com/ESP32Async/ESPAsyncWebServer/blob/main/library.json
cg.add_library("ESP32Async/ESPAsyncWebServer", "3.9.5") cg.add_library("ESP32Async/ESPAsyncWebServer", "3.9.6")

View File

@@ -24,7 +24,12 @@ from .const_zephyr import (
ZigbeeComponent, ZigbeeComponent,
zigbee_ns, zigbee_ns,
) )
from .zigbee_zephyr import zephyr_binary_sensor, zephyr_sensor, zephyr_switch from .zigbee_zephyr import (
zephyr_binary_sensor,
zephyr_number,
zephyr_sensor,
zephyr_switch,
)
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@@ -43,6 +48,7 @@ def zigbee_set_core_data(config: ConfigType) -> ConfigType:
BINARY_SENSOR_SCHEMA = cv.Schema({}).extend(zephyr_binary_sensor) BINARY_SENSOR_SCHEMA = cv.Schema({}).extend(zephyr_binary_sensor)
SENSOR_SCHEMA = cv.Schema({}).extend(zephyr_sensor) SENSOR_SCHEMA = cv.Schema({}).extend(zephyr_sensor)
SWITCH_SCHEMA = cv.Schema({}).extend(zephyr_switch) SWITCH_SCHEMA = cv.Schema({}).extend(zephyr_switch)
NUMBER_SCHEMA = cv.Schema({}).extend(zephyr_number)
CONFIG_SCHEMA = cv.All( CONFIG_SCHEMA = cv.All(
cv.Schema( cv.Schema(
@@ -125,6 +131,21 @@ async def setup_switch(entity: cg.MockObj, config: ConfigType) -> None:
await zephyr_setup_switch(entity, config) await zephyr_setup_switch(entity, config)
async def setup_number(
entity: cg.MockObj,
config: ConfigType,
min_value: float,
max_value: float,
step: float,
) -> None:
if not config.get(CONF_ZIGBEE_ID) or config.get(CONF_INTERNAL):
return
if CORE.using_zephyr:
from .zigbee_zephyr import zephyr_setup_number
await zephyr_setup_number(entity, config, min_value, max_value, step)
def consume_endpoint(config: ConfigType) -> ConfigType: def consume_endpoint(config: ConfigType) -> ConfigType:
if not config.get(CONF_ZIGBEE_ID) or config.get(CONF_INTERNAL): if not config.get(CONF_ZIGBEE_ID) or config.get(CONF_INTERNAL):
return config return config
@@ -152,6 +173,10 @@ def validate_switch(config: ConfigType) -> ConfigType:
return consume_endpoint(config) return consume_endpoint(config)
def validate_number(config: ConfigType) -> ConfigType:
return consume_endpoint(config)
ZIGBEE_ACTION_SCHEMA = automation.maybe_simple_id( ZIGBEE_ACTION_SCHEMA = automation.maybe_simple_id(
cv.Schema( cv.Schema(
{ {

View File

@@ -4,6 +4,7 @@ zigbee_ns = cg.esphome_ns.namespace("zigbee")
ZigbeeComponent = zigbee_ns.class_("ZigbeeComponent", cg.Component) ZigbeeComponent = zigbee_ns.class_("ZigbeeComponent", cg.Component)
BinaryAttrs = zigbee_ns.struct("BinaryAttrs") BinaryAttrs = zigbee_ns.struct("BinaryAttrs")
AnalogAttrs = zigbee_ns.struct("AnalogAttrs") AnalogAttrs = zigbee_ns.struct("AnalogAttrs")
AnalogAttrsOutput = zigbee_ns.struct("AnalogAttrsOutput")
CONF_MAX_EP_NUMBER = 8 CONF_MAX_EP_NUMBER = 8
CONF_ZIGBEE_ID = "zigbee_id" CONF_ZIGBEE_ID = "zigbee_id"
@@ -12,6 +13,7 @@ CONF_WIPE_ON_BOOT = "wipe_on_boot"
CONF_ZIGBEE_BINARY_SENSOR = "zigbee_binary_sensor" CONF_ZIGBEE_BINARY_SENSOR = "zigbee_binary_sensor"
CONF_ZIGBEE_SENSOR = "zigbee_sensor" CONF_ZIGBEE_SENSOR = "zigbee_sensor"
CONF_ZIGBEE_SWITCH = "zigbee_switch" CONF_ZIGBEE_SWITCH = "zigbee_switch"
CONF_ZIGBEE_NUMBER = "zigbee_number"
CONF_POWER_SOURCE = "power_source" CONF_POWER_SOURCE = "power_source"
POWER_SOURCE = { POWER_SOURCE = {
"UNKNOWN": "ZB_ZCL_BASIC_POWER_SOURCE_UNKNOWN", "UNKNOWN": "ZB_ZCL_BASIC_POWER_SOURCE_UNKNOWN",
@@ -38,3 +40,4 @@ ZB_ZCL_CLUSTER_ID_IDENTIFY = "ZB_ZCL_CLUSTER_ID_IDENTIFY"
ZB_ZCL_CLUSTER_ID_BINARY_INPUT = "ZB_ZCL_CLUSTER_ID_BINARY_INPUT" ZB_ZCL_CLUSTER_ID_BINARY_INPUT = "ZB_ZCL_CLUSTER_ID_BINARY_INPUT"
ZB_ZCL_CLUSTER_ID_ANALOG_INPUT = "ZB_ZCL_CLUSTER_ID_ANALOG_INPUT" ZB_ZCL_CLUSTER_ID_ANALOG_INPUT = "ZB_ZCL_CLUSTER_ID_ANALOG_INPUT"
ZB_ZCL_CLUSTER_ID_BINARY_OUTPUT = "ZB_ZCL_CLUSTER_ID_BINARY_OUTPUT" ZB_ZCL_CLUSTER_ID_BINARY_OUTPUT = "ZB_ZCL_CLUSTER_ID_BINARY_OUTPUT"
ZB_ZCL_CLUSTER_ID_ANALOG_OUTPUT = "ZB_ZCL_CLUSTER_ID_ANALOG_OUTPUT"

View File

@@ -0,0 +1,111 @@
#include "zigbee_number_zephyr.h"
#if defined(USE_ZIGBEE) && defined(USE_NRF52) && defined(USE_NUMBER)
#include "esphome/core/log.h"
extern "C" {
#include <zboss_api.h>
#include <zboss_api_addons.h>
#include <zb_nrf_platform.h>
#include <zigbee/zigbee_app_utils.h>
#include <zb_error_to_string.h>
}
namespace esphome::zigbee {
static const char *const TAG = "zigbee.number";
void ZigbeeNumber::setup() {
this->parent_->add_callback(this->endpoint_, [this](zb_bufid_t bufid) { this->zcl_device_cb_(bufid); });
this->number_->add_on_state_callback([this](float state) {
this->cluster_attributes_->present_value = state;
ESP_LOGD(TAG, "Set attribute endpoint: %d, present_value %f", this->endpoint_,
this->cluster_attributes_->present_value);
ZB_ZCL_SET_ATTRIBUTE(this->endpoint_, ZB_ZCL_CLUSTER_ID_ANALOG_OUTPUT, ZB_ZCL_CLUSTER_SERVER_ROLE,
ZB_ZCL_ATTR_ANALOG_OUTPUT_PRESENT_VALUE_ID, (zb_uint8_t *) &cluster_attributes_->present_value,
ZB_FALSE);
this->parent_->force_report();
});
}
void ZigbeeNumber::dump_config() {
ESP_LOGCONFIG(TAG,
"Zigbee Number\n"
" Endpoint: %d, present_value %f",
this->endpoint_, this->cluster_attributes_->present_value);
}
void ZigbeeNumber::zcl_device_cb_(zb_bufid_t bufid) {
zb_zcl_device_callback_param_t *p_device_cb_param = ZB_BUF_GET_PARAM(bufid, zb_zcl_device_callback_param_t);
zb_zcl_device_callback_id_t device_cb_id = p_device_cb_param->device_cb_id;
zb_uint16_t cluster_id = p_device_cb_param->cb_param.set_attr_value_param.cluster_id;
zb_uint16_t attr_id = p_device_cb_param->cb_param.set_attr_value_param.attr_id;
switch (device_cb_id) {
/* ZCL set attribute value */
case ZB_ZCL_SET_ATTR_VALUE_CB_ID:
if (cluster_id == ZB_ZCL_CLUSTER_ID_ANALOG_OUTPUT) {
ESP_LOGI(TAG, "Analog output attribute setting");
if (attr_id == ZB_ZCL_ATTR_ANALOG_OUTPUT_PRESENT_VALUE_ID) {
float value =
*reinterpret_cast<const float *>(&p_device_cb_param->cb_param.set_attr_value_param.values.data32);
this->defer([this, value]() {
this->cluster_attributes_->present_value = value;
auto call = this->number_->make_call();
call.set_value(value);
call.perform();
});
}
} else {
/* other clusters attribute handled here */
ESP_LOGI(TAG, "Unhandled cluster attribute id: %d", cluster_id);
p_device_cb_param->status = RET_NOT_IMPLEMENTED;
}
break;
default:
p_device_cb_param->status = RET_NOT_IMPLEMENTED;
break;
}
ESP_LOGD(TAG, "%s status: %hd", __func__, p_device_cb_param->status);
}
const zb_uint8_t ZB_ZCL_ANALOG_OUTPUT_STATUS_FLAG_MAX_VALUE = 0x0F;
static zb_ret_t check_value_analog_server(zb_uint16_t attr_id, zb_uint8_t endpoint,
zb_uint8_t *value) { // NOLINT(readability-non-const-parameter)
zb_ret_t ret = RET_OK;
ZVUNUSED(endpoint);
switch (attr_id) {
case ZB_ZCL_ATTR_ANALOG_OUTPUT_OUT_OF_SERVICE_ID:
ret = ZB_ZCL_CHECK_BOOL_VALUE(*value) ? RET_OK : RET_ERROR;
break;
case ZB_ZCL_ATTR_ANALOG_OUTPUT_PRESENT_VALUE_ID:
break;
case ZB_ZCL_ATTR_ANALOG_OUTPUT_STATUS_FLAG_ID:
if (*value > ZB_ZCL_ANALOG_OUTPUT_STATUS_FLAG_MAX_VALUE) {
ret = RET_ERROR;
}
break;
default:
break;
}
return ret;
}
} // namespace esphome::zigbee
void zb_zcl_analog_output_init_server() {
zb_zcl_add_cluster_handlers(ZB_ZCL_CLUSTER_ID_ANALOG_OUTPUT, ZB_ZCL_CLUSTER_SERVER_ROLE,
esphome::zigbee::check_value_analog_server, (zb_zcl_cluster_write_attr_hook_t) NULL,
(zb_zcl_cluster_handler_t) NULL);
}
void zb_zcl_analog_output_init_client() {
zb_zcl_add_cluster_handlers(ZB_ZCL_CLUSTER_ID_ANALOG_OUTPUT, ZB_ZCL_CLUSTER_CLIENT_ROLE,
(zb_zcl_cluster_check_value_t) NULL, (zb_zcl_cluster_write_attr_hook_t) NULL,
(zb_zcl_cluster_handler_t) NULL);
}
#endif

View File

@@ -0,0 +1,118 @@
#pragma once
#include "esphome/core/defines.h"
#if defined(USE_ZIGBEE) && defined(USE_NRF52) && defined(USE_NUMBER)
#include "esphome/components/zigbee/zigbee_zephyr.h"
#include "esphome/core/component.h"
#include "esphome/components/number/number.h"
extern "C" {
#include <zboss_api.h>
#include <zboss_api_addons.h>
}
enum {
ZB_ZCL_ATTR_ANALOG_OUTPUT_DESCRIPTION_ID = 0x001C,
ZB_ZCL_ATTR_ANALOG_OUTPUT_MAX_PRESENT_VALUE_ID = 0x0041,
ZB_ZCL_ATTR_ANALOG_OUTPUT_MIN_PRESENT_VALUE_ID = 0x0045,
ZB_ZCL_ATTR_ANALOG_OUTPUT_OUT_OF_SERVICE_ID = 0x0051,
ZB_ZCL_ATTR_ANALOG_OUTPUT_PRESENT_VALUE_ID = 0x0055,
ZB_ZCL_ATTR_ANALOG_OUTPUT_RESOLUTION_ID = 0x006A,
ZB_ZCL_ATTR_ANALOG_OUTPUT_STATUS_FLAG_ID = 0x006F,
ZB_ZCL_ATTR_ANALOG_OUTPUT_ENGINEERING_UNITS_ID = 0x0075,
};
#define ZB_ZCL_ANALOG_OUTPUT_CLUSTER_REVISION_DEFAULT ((zb_uint16_t) 0x0001u)
#define ZB_SET_ATTR_DESCR_WITH_ZB_ZCL_ATTR_ANALOG_OUTPUT_DESCRIPTION_ID(data_ptr) \
{ \
ZB_ZCL_ATTR_ANALOG_OUTPUT_DESCRIPTION_ID, ZB_ZCL_ATTR_TYPE_CHAR_STRING, ZB_ZCL_ATTR_ACCESS_READ_ONLY, \
(ZB_ZCL_NON_MANUFACTURER_SPECIFIC), (void *) (data_ptr) \
}
#define ZB_SET_ATTR_DESCR_WITH_ZB_ZCL_ATTR_ANALOG_OUTPUT_OUT_OF_SERVICE_ID(data_ptr) \
{ \
ZB_ZCL_ATTR_ANALOG_OUTPUT_OUT_OF_SERVICE_ID, ZB_ZCL_ATTR_TYPE_BOOL, \
ZB_ZCL_ATTR_ACCESS_READ_ONLY | ZB_ZCL_ATTR_ACCESS_WRITE_OPTIONAL, (ZB_ZCL_NON_MANUFACTURER_SPECIFIC), \
(void *) (data_ptr) \
}
// PresentValue
#define ZB_SET_ATTR_DESCR_WITH_ZB_ZCL_ATTR_ANALOG_OUTPUT_PRESENT_VALUE_ID(data_ptr) \
{ \
ZB_ZCL_ATTR_ANALOG_OUTPUT_PRESENT_VALUE_ID, ZB_ZCL_ATTR_TYPE_SINGLE, \
ZB_ZCL_ATTR_ACCESS_READ_WRITE | ZB_ZCL_ATTR_ACCESS_REPORTING, (ZB_ZCL_NON_MANUFACTURER_SPECIFIC), \
(void *) (data_ptr) \
}
// MaxPresentValue
#define ZB_SET_ATTR_DESCR_WITH_ZB_ZCL_ATTR_ANALOG_OUTPUT_MAX_PRESENT_VALUE_ID(data_ptr) \
{ \
ZB_ZCL_ATTR_ANALOG_OUTPUT_MAX_PRESENT_VALUE_ID, ZB_ZCL_ATTR_TYPE_SINGLE, \
ZB_ZCL_ATTR_ACCESS_READ_ONLY | ZB_ZCL_ATTR_ACCESS_WRITE_OPTIONAL, (ZB_ZCL_NON_MANUFACTURER_SPECIFIC), \
(void *) (data_ptr) \
}
// MinPresentValue
#define ZB_SET_ATTR_DESCR_WITH_ZB_ZCL_ATTR_ANALOG_OUTPUT_MIN_PRESENT_VALUE_ID(data_ptr) \
{ \
ZB_ZCL_ATTR_ANALOG_OUTPUT_MIN_PRESENT_VALUE_ID, ZB_ZCL_ATTR_TYPE_SINGLE, \
ZB_ZCL_ATTR_ACCESS_READ_ONLY | ZB_ZCL_ATTR_ACCESS_WRITE_OPTIONAL, (ZB_ZCL_NON_MANUFACTURER_SPECIFIC), \
(void *) (data_ptr) \
}
// Resolution
#define ZB_SET_ATTR_DESCR_WITH_ZB_ZCL_ATTR_ANALOG_OUTPUT_RESOLUTION_ID(data_ptr) \
{ \
ZB_ZCL_ATTR_ANALOG_OUTPUT_RESOLUTION_ID, ZB_ZCL_ATTR_TYPE_SINGLE, \
ZB_ZCL_ATTR_ACCESS_READ_ONLY | ZB_ZCL_ATTR_ACCESS_WRITE_OPTIONAL, (ZB_ZCL_NON_MANUFACTURER_SPECIFIC), \
(void *) (data_ptr) \
}
#define ZB_SET_ATTR_DESCR_WITH_ZB_ZCL_ATTR_ANALOG_OUTPUT_STATUS_FLAG_ID(data_ptr) \
{ \
ZB_ZCL_ATTR_ANALOG_OUTPUT_STATUS_FLAG_ID, ZB_ZCL_ATTR_TYPE_8BITMAP, \
ZB_ZCL_ATTR_ACCESS_READ_ONLY | ZB_ZCL_ATTR_ACCESS_REPORTING, (ZB_ZCL_NON_MANUFACTURER_SPECIFIC), \
(void *) (data_ptr) \
}
#define ZB_SET_ATTR_DESCR_WITH_ZB_ZCL_ATTR_ANALOG_OUTPUT_ENGINEERING_UNITS_ID(data_ptr) \
{ \
ZB_ZCL_ATTR_ANALOG_OUTPUT_ENGINEERING_UNITS_ID, ZB_ZCL_ATTR_TYPE_16BIT_ENUM, ZB_ZCL_ATTR_ACCESS_READ_ONLY, \
(ZB_ZCL_NON_MANUFACTURER_SPECIFIC), (void *) (data_ptr) \
}
#define ESPHOME_ZB_ZCL_DECLARE_ANALOG_OUTPUT_ATTRIB_LIST(attr_list, out_of_service, present_value, status_flag, \
max_present_value, min_present_value, resolution, \
engineering_units, description) \
ZB_ZCL_START_DECLARE_ATTRIB_LIST_CLUSTER_REVISION(attr_list, ZB_ZCL_ANALOG_OUTPUT) \
ZB_ZCL_SET_ATTR_DESC(ZB_ZCL_ATTR_ANALOG_OUTPUT_OUT_OF_SERVICE_ID, (out_of_service)) \
ZB_ZCL_SET_ATTR_DESC(ZB_ZCL_ATTR_ANALOG_OUTPUT_PRESENT_VALUE_ID, (present_value)) \
ZB_ZCL_SET_ATTR_DESC(ZB_ZCL_ATTR_ANALOG_OUTPUT_STATUS_FLAG_ID, (status_flag)) \
ZB_ZCL_SET_ATTR_DESC(ZB_ZCL_ATTR_ANALOG_OUTPUT_MAX_PRESENT_VALUE_ID, (max_present_value)) \
ZB_ZCL_SET_ATTR_DESC(ZB_ZCL_ATTR_ANALOG_OUTPUT_MIN_PRESENT_VALUE_ID, (min_present_value)) \
ZB_ZCL_SET_ATTR_DESC(ZB_ZCL_ATTR_ANALOG_OUTPUT_RESOLUTION_ID, (resolution)) \
ZB_ZCL_SET_ATTR_DESC(ZB_ZCL_ATTR_ANALOG_OUTPUT_ENGINEERING_UNITS_ID, (engineering_units)) \
ZB_ZCL_SET_ATTR_DESC(ZB_ZCL_ATTR_ANALOG_OUTPUT_DESCRIPTION_ID, (description)) \
ZB_ZCL_FINISH_DECLARE_ATTRIB_LIST
void zb_zcl_analog_output_init_server();
void zb_zcl_analog_output_init_client();
#define ZB_ZCL_CLUSTER_ID_ANALOG_OUTPUT_SERVER_ROLE_INIT zb_zcl_analog_output_init_server
#define ZB_ZCL_CLUSTER_ID_ANALOG_OUTPUT_CLIENT_ROLE_INIT zb_zcl_analog_output_init_client
namespace esphome::zigbee {
class ZigbeeNumber : public ZigbeeEntity, public Component {
public:
ZigbeeNumber(number::Number *n) : number_(n) {}
void set_cluster_attributes(AnalogAttrsOutput &cluster_attributes) {
this->cluster_attributes_ = &cluster_attributes;
}
void setup() override;
void dump_config() override;
protected:
number::Number *number_;
AnalogAttrsOutput *cluster_attributes_{nullptr};
void zcl_device_cb_(zb_bufid_t bufid);
};
} // namespace esphome::zigbee
#endif

View File

@@ -50,7 +50,7 @@ void ZigbeeSwitch::zcl_device_cb_(zb_bufid_t bufid) {
if (attr_id == ZB_ZCL_ATTR_BINARY_OUTPUT_PRESENT_VALUE_ID) { if (attr_id == ZB_ZCL_ATTR_BINARY_OUTPUT_PRESENT_VALUE_ID) {
this->defer([this, value]() { this->defer([this, value]() {
this->cluster_attributes_->present_value = value ? ZB_TRUE : ZB_FALSE; this->cluster_attributes_->present_value = value ? ZB_TRUE : ZB_FALSE;
this->switch_->publish_state(value); this->switch_->control(value);
}); });
} }
} else { } else {

View File

@@ -101,8 +101,8 @@ void ZigbeeComponent::zcl_device_cb(zb_bufid_t bufid) {
zb_uint16_t attr_id = p_device_cb_param->cb_param.set_attr_value_param.attr_id; zb_uint16_t attr_id = p_device_cb_param->cb_param.set_attr_value_param.attr_id;
auto endpoint = p_device_cb_param->endpoint; auto endpoint = p_device_cb_param->endpoint;
ESP_LOGI(TAG, "Zcl_device_cb %s id %hd, cluster_id %d, attr_id %d, endpoint: %d", __func__, device_cb_id, cluster_id, ESP_LOGI(TAG, "%s id %hd, cluster_id %d, attr_id %d, endpoint: %d", __func__, device_cb_id, cluster_id, attr_id,
attr_id, endpoint); endpoint);
/* Set default response value. */ /* Set default response value. */
p_device_cb_param->status = RET_OK; p_device_cb_param->status = RET_OK;

View File

@@ -60,6 +60,12 @@ struct AnalogAttrs {
zb_uchar_t description[ZB_ZCL_MAX_STRING_SIZE]; zb_uchar_t description[ZB_ZCL_MAX_STRING_SIZE];
}; };
struct AnalogAttrsOutput : AnalogAttrs {
float max_present_value;
float min_present_value;
float resolution;
};
class ZigbeeComponent : public Component { class ZigbeeComponent : public Component {
public: public:
void setup() override; void setup() override;

View File

@@ -55,6 +55,7 @@ from .const_zephyr import (
CONF_WIPE_ON_BOOT, CONF_WIPE_ON_BOOT,
CONF_ZIGBEE_BINARY_SENSOR, CONF_ZIGBEE_BINARY_SENSOR,
CONF_ZIGBEE_ID, CONF_ZIGBEE_ID,
CONF_ZIGBEE_NUMBER,
CONF_ZIGBEE_SENSOR, CONF_ZIGBEE_SENSOR,
CONF_ZIGBEE_SWITCH, CONF_ZIGBEE_SWITCH,
KEY_EP_NUMBER, KEY_EP_NUMBER,
@@ -62,12 +63,14 @@ from .const_zephyr import (
POWER_SOURCE, POWER_SOURCE,
ZB_ZCL_BASIC_ATTRS_EXT_T, ZB_ZCL_BASIC_ATTRS_EXT_T,
ZB_ZCL_CLUSTER_ID_ANALOG_INPUT, ZB_ZCL_CLUSTER_ID_ANALOG_INPUT,
ZB_ZCL_CLUSTER_ID_ANALOG_OUTPUT,
ZB_ZCL_CLUSTER_ID_BASIC, ZB_ZCL_CLUSTER_ID_BASIC,
ZB_ZCL_CLUSTER_ID_BINARY_INPUT, ZB_ZCL_CLUSTER_ID_BINARY_INPUT,
ZB_ZCL_CLUSTER_ID_BINARY_OUTPUT, ZB_ZCL_CLUSTER_ID_BINARY_OUTPUT,
ZB_ZCL_CLUSTER_ID_IDENTIFY, ZB_ZCL_CLUSTER_ID_IDENTIFY,
ZB_ZCL_IDENTIFY_ATTRS_T, ZB_ZCL_IDENTIFY_ATTRS_T,
AnalogAttrs, AnalogAttrs,
AnalogAttrsOutput,
BinaryAttrs, BinaryAttrs,
ZigbeeComponent, ZigbeeComponent,
zigbee_ns, zigbee_ns,
@@ -76,6 +79,7 @@ from .const_zephyr import (
ZigbeeBinarySensor = zigbee_ns.class_("ZigbeeBinarySensor", cg.Component) ZigbeeBinarySensor = zigbee_ns.class_("ZigbeeBinarySensor", cg.Component)
ZigbeeSensor = zigbee_ns.class_("ZigbeeSensor", cg.Component) ZigbeeSensor = zigbee_ns.class_("ZigbeeSensor", cg.Component)
ZigbeeSwitch = zigbee_ns.class_("ZigbeeSwitch", cg.Component) ZigbeeSwitch = zigbee_ns.class_("ZigbeeSwitch", cg.Component)
ZigbeeNumber = zigbee_ns.class_("ZigbeeNumber", cg.Component)
# BACnet engineering units mapping (ZCL uses BACnet unit codes) # BACnet engineering units mapping (ZCL uses BACnet unit codes)
# See: https://github.com/zigpy/zha/blob/dev/zha/application/platforms/number/bacnet.py # See: https://github.com/zigpy/zha/blob/dev/zha/application/platforms/number/bacnet.py
@@ -139,6 +143,15 @@ zephyr_switch = cv.Schema(
} }
) )
zephyr_number = cv.Schema(
{
cv.OnlyWith(CONF_ZIGBEE_ID, ["nrf52", "zigbee"]): cv.use_id(ZigbeeComponent),
cv.OnlyWith(CONF_ZIGBEE_NUMBER, ["nrf52", "zigbee"]): cv.declare_id(
ZigbeeNumber
),
}
)
async def zephyr_to_code(config: ConfigType) -> None: async def zephyr_to_code(config: ConfigType) -> None:
zephyr_add_prj_conf("ZIGBEE", True) zephyr_add_prj_conf("ZIGBEE", True)
@@ -344,6 +357,16 @@ async def zephyr_setup_switch(entity: cg.MockObj, config: ConfigType) -> None:
CORE.add_job(_add_switch, entity, config) CORE.add_job(_add_switch, entity, config)
async def zephyr_setup_number(
entity: cg.MockObj,
config: ConfigType,
min_value: float,
max_value: float,
step: float,
) -> None:
CORE.add_job(_add_number, entity, config, min_value, max_value, step)
def get_slot_index() -> int: def get_slot_index() -> int:
"""Find the next available endpoint slot.""" """Find the next available endpoint slot."""
slot = next( slot = next(
@@ -451,3 +474,31 @@ async def _add_switch(entity: cg.MockObj, config: ConfigType) -> None:
ZB_ZCL_CLUSTER_ID_BINARY_OUTPUT, ZB_ZCL_CLUSTER_ID_BINARY_OUTPUT,
"ZB_HA_CUSTOM_ATTR_DEVICE_ID", "ZB_HA_CUSTOM_ATTR_DEVICE_ID",
) )
async def _add_number(
entity: cg.MockObj,
config: ConfigType,
min_value: float,
max_value: float,
step: float,
) -> None:
# Get BACnet engineering unit from unit_of_measurement
unit = config.get(CONF_UNIT_OF_MEASUREMENT, "")
bacnet_unit = BACNET_UNITS.get(unit, BACNET_UNIT_NO_UNITS)
await _add_zigbee_ep(
entity,
config,
CONF_ZIGBEE_NUMBER,
AnalogAttrsOutput,
"ESPHOME_ZB_ZCL_DECLARE_ANALOG_OUTPUT_ATTRIB_LIST",
ZB_ZCL_CLUSTER_ID_ANALOG_OUTPUT,
"ZB_HA_CUSTOM_ATTR_DEVICE_ID",
extra_field_values={
"max_present_value": max_value,
"min_present_value": min_value,
"resolution": step,
"engineering_units": bacnet_unit,
},
)

View File

@@ -149,6 +149,7 @@ CONF_ASSUMED_STATE = "assumed_state"
CONF_AT = "at" CONF_AT = "at"
CONF_ATTENUATION = "attenuation" CONF_ATTENUATION = "attenuation"
CONF_ATTRIBUTE = "attribute" CONF_ATTRIBUTE = "attribute"
CONF_AUDIO_DAC = "audio_dac"
CONF_AUTH = "auth" CONF_AUTH = "auth"
CONF_AUTO_CLEAR_ENABLED = "auto_clear_enabled" CONF_AUTO_CLEAR_ENABLED = "auto_clear_enabled"
CONF_AUTO_MODE = "auto_mode" CONF_AUTO_MODE = "auto_mode"

View File

@@ -278,9 +278,13 @@ LAMBDA_PROG = re.compile(r"\bid\(\s*([a-zA-Z_][a-zA-Z0-9_]*)\s*\)(\.?)")
class Lambda: class Lambda:
def __init__(self, value): def __init__(self, value):
from esphome.cpp_generator import Expression, statement
# pylint: disable=protected-access # pylint: disable=protected-access
if isinstance(value, Lambda): if isinstance(value, Lambda):
self._value = value._value self._value = value._value
elif isinstance(value, Expression):
self._value = str(statement(value))
else: else:
self._value = value self._value = value
self._parts = None self._parts = None

View File

@@ -210,7 +210,7 @@ void Application::loop() {
#ifdef USE_ESP32 #ifdef USE_ESP32
esp_chip_info_t chip_info; esp_chip_info_t chip_info;
esp_chip_info(&chip_info); esp_chip_info(&chip_info);
ESP_LOGI(TAG, "ESP32 Chip: %s r%d.%d, %d core(s)", ESPHOME_VARIANT, chip_info.revision / 100, ESP_LOGI(TAG, "ESP32 Chip: %s rev%d.%d, %d core(s)", ESPHOME_VARIANT, chip_info.revision / 100,
chip_info.revision % 100, chip_info.cores); chip_info.revision % 100, chip_info.cores);
#if defined(USE_ESP32_VARIANT_ESP32) && !defined(USE_ESP32_MIN_CHIP_REVISION_SET) #if defined(USE_ESP32_VARIANT_ESP32) && !defined(USE_ESP32_MIN_CHIP_REVISION_SET)
// Suggest optimization for chips that don't need the PSRAM cache workaround // Suggest optimization for chips that don't need the PSRAM cache workaround

View File

@@ -232,6 +232,25 @@
#define ESPHOME_WIFI_CONNECT_STATE_LISTENERS 2 #define ESPHOME_WIFI_CONNECT_STATE_LISTENERS 2
#define ESPHOME_WIFI_POWER_SAVE_LISTENERS 2 #define ESPHOME_WIFI_POWER_SAVE_LISTENERS 2
#define USE_WIFI_RUNTIME_POWER_SAVE #define USE_WIFI_RUNTIME_POWER_SAVE
#define USE_TEMPLATE_NUMBER_SET_TRIGGER
#define USE_TEMPLATE_SELECT_SET_TRIGGER
#define USE_TEMPLATE_TEXT_SET_TRIGGER
#define USE_TEMPLATE_SWITCH_TURN_ON_TRIGGER
#define USE_TEMPLATE_SWITCH_TURN_OFF_TRIGGER
#define USE_TEMPLATE_DATE_SET_TRIGGER
#define USE_TEMPLATE_TIME_SET_TRIGGER
#define USE_TEMPLATE_DATETIME_SET_TRIGGER
#define USE_TEMPLATE_WATER_HEATER_SET_TRIGGER
#define USE_TEMPLATE_LOCK_LOCK_TRIGGER
#define USE_TEMPLATE_LOCK_UNLOCK_TRIGGER
#define USE_TEMPLATE_LOCK_OPEN_TRIGGER
#define USE_TEMPLATE_COVER_STOP_TRIGGER
#define USE_TEMPLATE_COVER_TOGGLE_TRIGGER
#define USE_TEMPLATE_COVER_POSITION_TRIGGER
#define USE_TEMPLATE_COVER_TILT_TRIGGER
#define USE_TEMPLATE_VALVE_STOP_TRIGGER
#define USE_TEMPLATE_VALVE_TOGGLE_TRIGGER
#define USE_TEMPLATE_VALVE_POSITION_TRIGGER
#define USB_HOST_MAX_REQUESTS 16 #define USB_HOST_MAX_REQUESTS 16
#ifdef USE_ARDUINO #ifdef USE_ARDUINO

View File

@@ -462,6 +462,16 @@ def statement(expression: Expression | Statement) -> Statement:
return ExpressionStatement(expression) return ExpressionStatement(expression)
def literal(name: str) -> "MockObj":
"""Create a literal name that will appear in the generated code
not surrounded by quotes.
:param name: The name of the literal.
:return: The literal as a MockObj.
"""
return MockObj(name, "")
def variable( def variable(
id_: ID, rhs: SafeExpType, type_: "MockObj" = None, register=True id_: ID, rhs: SafeExpType, type_: "MockObj" = None, register=True
) -> "MockObj": ) -> "MockObj":
@@ -665,7 +675,7 @@ async def get_variable_with_full_id(id_: ID) -> tuple[ID, "MockObj"]:
async def process_lambda( async def process_lambda(
value: Lambda, value: Lambda | Expression,
parameters: TemplateArgsType, parameters: TemplateArgsType,
capture: str = "", capture: str = "",
return_type: SafeExpType = None, return_type: SafeExpType = None,
@@ -689,6 +699,14 @@ async def process_lambda(
if value is None: if value is None:
return None return None
# Inadvertently passing a malformed parameters value will lead to the build process mysteriously hanging at the
# "Generating C++ source..." stage, so check here to save the developer's hair.
assert isinstance(parameters, list) and all(
isinstance(p, tuple) and len(p) == 2 for p in parameters
)
if isinstance(value, Expression):
value = Lambda(value)
parts = value.parts[:] parts = value.parts[:]
for i, id in enumerate(value.requires_ids): for i, id in enumerate(value.requires_ids):
full_id, var = await get_variable_with_full_id(id) full_id, var = await get_variable_with_full_id(id)

View File

@@ -114,7 +114,7 @@ lib_deps =
ESP8266WiFi ; wifi (Arduino built-in) ESP8266WiFi ; wifi (Arduino built-in)
Update ; ota (Arduino built-in) Update ; ota (Arduino built-in)
ESP32Async/ESPAsyncTCP@2.0.0 ; async_tcp ESP32Async/ESPAsyncTCP@2.0.0 ; async_tcp
ESP32Async/ESPAsyncWebServer@3.9.5 ; web_server_base ESP32Async/ESPAsyncWebServer@3.9.6 ; web_server_base
makuna/NeoPixelBus@2.7.3 ; neopixelbus makuna/NeoPixelBus@2.7.3 ; neopixelbus
ESP8266HTTPClient ; http_request (Arduino built-in) ESP8266HTTPClient ; http_request (Arduino built-in)
ESP8266mDNS ; mdns (Arduino built-in) ESP8266mDNS ; mdns (Arduino built-in)
@@ -202,7 +202,7 @@ lib_deps =
${common:arduino.lib_deps} ${common:arduino.lib_deps}
ayushsharma82/RPAsyncTCP@1.3.2 ; async_tcp ayushsharma82/RPAsyncTCP@1.3.2 ; async_tcp
bblanchon/ArduinoJson@7.4.2 ; json bblanchon/ArduinoJson@7.4.2 ; json
ESP32Async/ESPAsyncWebServer@3.9.5 ; web_server_base ESP32Async/ESPAsyncWebServer@3.9.6 ; web_server_base
build_flags = build_flags =
${common:arduino.build_flags} ${common:arduino.build_flags}
-DUSE_RP2040 -DUSE_RP2040
@@ -218,7 +218,7 @@ framework = arduino
lib_compat_mode = soft lib_compat_mode = soft
lib_deps = lib_deps =
bblanchon/ArduinoJson@7.4.2 ; json bblanchon/ArduinoJson@7.4.2 ; json
ESP32Async/ESPAsyncWebServer@3.9.5 ; web_server_base ESP32Async/ESPAsyncWebServer@3.9.6 ; web_server_base
droscy/esp_wireguard@0.4.2 ; wireguard droscy/esp_wireguard@0.4.2 ; wireguard
build_flags = build_flags =
${common:arduino.build_flags} ${common:arduino.build_flags}

Some files were not shown because too many files have changed in this diff Show More