diff --git a/.ai/instructions.md b/.ai/instructions.md index 3c24177827..86f554e9ce 100644 --- a/.ai/instructions.md +++ b/.ai/instructions.md @@ -124,6 +124,28 @@ This document provides essential context for AI models interacting with this pro * **Indentation:** Use spaces (two per indentation level), not tabs * **Type aliases:** Prefer `using type_t = int;` over `typedef int type_t;` * **Line length:** Wrap lines at no more than 120 characters + * **Constructor parameters vs setters:** Component properties that are both **required** and **invariant** + (never change after construction) should be constructor parameters rather than set via setter methods. + This makes the dependency explicit and prevents use of the object in an incompletely-initialized state. + In code generation, when calling `cg.new_Pvariable()` or the relevant helper function to create the component, pass these as arguments. + ```cpp + // Good - required invariant dependency as constructor parameter + class SourceTextSensor : public text_sensor::TextSensor, public Component { + public: + explicit SourceTextSensor(text::Text *source) : source_(source) {} + protected: + text::Text *source_; + }; + ``` + ```cpp + // Bad - required invariant dependency as setter + class SourceTextSensor : public text_sensor::TextSensor, public Component { + public: + void set_source(text::Text *source) { this->source_ = source; } + protected: + text::Text *source_{nullptr}; + }; + ``` * **Component Structure:** * **Standard Files:** @@ -217,6 +239,123 @@ This document provides essential context for AI models interacting with this pro var = await switch.new_switch(config) ``` +* **Automations (Triggers, Actions, Conditions):** + + Automations have three building blocks: **Triggers** (fire when something happens), **Actions** (do something), and **Conditions** (check if something is true). + + * **Triggers -- Callback method (preferred):** + + Use `build_callback_automation()` for simple triggers. This eliminates the need for a C++ Trigger class by using a lightweight pointer-sized forwarder struct registered directly as a callback. No `CONF_TRIGGER_ID` in the schema. + + **Python:** + ```python + from esphome import automation + + CONFIG_SCHEMA = cv.Schema({ + cv.GenerateID(): cv.declare_id(MyComponent), + cv.Optional(CONF_ON_STATE): automation.validate_automation({}), + }).extend(cv.COMPONENT_SCHEMA) + + async def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + await cg.register_component(var, config) + for conf in config.get(CONF_ON_STATE, []): + await automation.build_callback_automation( + var, "add_on_state_callback", [(bool, "x")], conf + ) + ``` + + `build_callback_automation` arguments: `parent`, `callback_method` (C++ method name), `args` (template args as `[(type, name)]` tuples), `config`, and optional `forwarder` (defaults to `TriggerForwarder`). + + For boolean filtering (e.g. `on_press`/`on_release`), use built-in forwarders with `args=[]`: + ```python + for conf_key, forwarder in ( + (CONF_ON_PRESS, automation.TriggerOnTrueForwarder), + (CONF_ON_RELEASE, automation.TriggerOnFalseForwarder), + ): + for conf in config.get(conf_key, []): + await automation.build_callback_automation( + var, "add_on_state_callback", [], conf, forwarder=forwarder + ) + ``` + + **C++ -- no trigger class needed.** The callback registration method must be templatized to accept both `std::function` and lightweight forwarder structs (which avoid heap allocation): + ```cpp + class MyComponent : public Component { + public: + // Must be a template -- accepts both std::function and pointer-sized forwarder structs + template void add_on_state_callback(F &&callback) { + this->state_callback_.add(std::forward(callback)); + } + protected: + // Use CallbackManager when callbacks are always registered (e.g. core components) + CallbackManager state_callback_; + // Use LazyCallbackManager when callbacks are often not registered -- saves 8 bytes + // (nullptr vs empty std::vector) per instance when no callbacks are added + // LazyCallbackManager state_callback_; + }; + ``` + + * **Triggers -- Trigger class method:** + + Use `build_automation()` with a `Trigger` subclass only when the forwarder needs **mutable state beyond a single `Automation*` pointer** (e.g. edge detection tracking previous state, timing logic). + + **Python:** + ```python + TurnOnTrigger = my_ns.class_("TurnOnTrigger", automation.Trigger.template()) + + CONFIG_SCHEMA = cv.Schema({ + cv.Optional(CONF_ON_TURN_ON): automation.validate_automation( + {cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(TurnOnTrigger)} + ), + }) + + async def to_code(config): + for conf in config.get(CONF_ON_TURN_ON, []): + trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) + await automation.build_automation(trigger, [], conf) + ``` + + **C++:** + ```cpp + class TurnOnTrigger : public Trigger<> { + public: + explicit TurnOnTrigger(MyComponent *parent) : last_on_{false} { + parent->add_on_state_callback([this](bool state) { + if (state && !this->last_on_) + this->trigger(); + this->last_on_ = state; + }); + } + protected: + bool last_on_; + }; + ``` + + * **Actions:** + ```cpp + template class MyAction : public Action { + public: + explicit MyAction(MyComponent *parent) : parent_(parent) {} + void play(const Ts &...) override { this->parent_->do_something(); } + protected: + MyComponent *parent_; + }; + ``` + Register with `@automation.register_action("my_component.do_something", MyAction, schema, synchronous=True)`. Use `synchronous=True` for actions that run to completion inside `play()` without deferring. Use `synchronous=False` if the action may suspend/defer execution (e.g. `delay`, `wait_until`, `script.wait`) or store trigger arguments for later use. + + * **Conditions:** + ```cpp + template class MyCondition : public Condition { + public: + explicit MyCondition(MyComponent *parent) : parent_(parent) {} + bool check(const Ts &...) override { return this->parent_->is_active(); } + protected: + MyComponent *parent_; + }; + ``` + Register with `@automation.register_condition("my_component.is_active", MyCondition, schema)`. + * **Configuration Validation:** * **Common Validators:** `cv.int_`, `cv.float_`, `cv.string`, `cv.boolean`, `cv.int_range(min=0, max=100)`, `cv.positive_int`, `cv.percentage`. * **Complex Validation:** `cv.All(cv.string, cv.Length(min=1, max=50))`, `cv.Any(cv.int_, cv.string)`. @@ -252,10 +391,39 @@ This document provides essential context for AI models interacting with this pro * **Component Tests:** YAML-based compilation tests are located in `tests/`. The structure is as follows: ``` tests/ - ├── test_build_components/ # Base test configurations - └── components/[component]/ # Component-specific tests + ├── test_build_components/ + │ └── common/ # Shared bus packages (uart, i2c, spi, etc.) + │ ├── uart/ # UART at default baud rate + │ ├── uart_115200/ # UART at 115200 baud + │ ├── i2c/ # I2C bus + │ └── spi/ # SPI bus + └── components/[component]/ + ├── common.yaml # Component-only config (no bus definitions) + ├── test.esp32-idf.yaml + ├── test.esp8266-ard.yaml + └── test.rp2040-ard.yaml ``` Run them using `script/test_build_components`. Use `-c ` to test specific components and `-t ` for specific platforms. + + * **Test Grouping with Packages:** Components that use shared bus packages can be grouped together in CI to reduce build count. **Never define buses (uart, i2c, spi, modbus) directly in test YAML files** — always use packages from `test_build_components/common/`: + ```yaml + # test.esp32-idf.yaml — use packages for buses + packages: + uart: !include ../../test_build_components/common/uart_115200/esp32-idf.yaml + + <<: !include common.yaml + ``` + ```yaml + # common.yaml — component config only, NO bus definitions + my_component: + id: my_instance + + sensor: + - platform: my_component + name: My Sensor + ``` + Components that define buses directly are flagged as "NEEDS MIGRATION" and cannot be grouped, increasing CI build time. + * **Testing All Components Together:** To verify that all components can be tested together without ID conflicts or configuration issues, use: ```bash ./script/test_component_grouping.py -e config --all @@ -286,6 +454,7 @@ This document provides essential context for AI models interacting with this pro * **Documentation Contributions:** * Documentation is hosted in the separate `esphome/esphome-docs` repository. * The contribution workflow is the same as for the codebase. + * When editing a component's documentation page, also update the corresponding component index page to ensure both pages remain in sync. * **Best Practices:** * **Component Development:** Keep dependencies minimal, provide clear error messages, and write comprehensive docstrings and tests. @@ -394,6 +563,30 @@ This document provides essential context for AI models interacting with this pro Note: Avoiding heap allocation after `setup()` is always required regardless of component type. The prioritization above is about the effort spent on container optimization (e.g., migrating from `std::vector` to `StaticVector`). + **Callback Managers:** + + ESPHome provides two callback manager types in `esphome/core/helpers.h` for the observer pattern. Both support `std::function`, lambdas, and lightweight forwarder structs via their templatized `add()` method. + + | Type | Idle overhead (32-bit) | When to use | + |------|----------------------|-------------| + | `CallbackManager` | 12 bytes (empty `std::vector`) | Callbacks are always or almost always registered | + | `LazyCallbackManager` | 4 bytes (`nullptr`) | Callbacks are often not registered (common case) | + + `LazyCallbackManager` is a drop-in replacement for `CallbackManager` that defers allocation until the first callback is added. Prefer it for entity-level callbacks where most instances have no subscribers. + + **Important:** Registration methods that add to a callback manager **must always be templatized** to accept both `std::function` and pointer-sized forwarder structs (used by `build_callback_automation`). Never use `std::function` in the method signature: + ```cpp + // Bad -- forces heap allocation for forwarder structs + void add_on_state_callback(std::function &&callback) { + this->state_callback_.add(std::move(callback)); + } + + // Good -- accepts any callable without forcing std::function wrapping + template void add_on_state_callback(F &&callback) { + this->state_callback_.add(std::forward(callback)); + } + ``` + * **State Management:** Use `CORE.data` for component state that needs to persist during configuration generation. Avoid module-level mutable globals. **Bad Pattern (Module-Level Globals):** diff --git a/.clang-tidy.hash b/.clang-tidy.hash index 1cb5f98c28..5c7eab517b 100644 --- a/.clang-tidy.hash +++ b/.clang-tidy.hash @@ -1 +1 @@ -cf3d341206b4184ec8b7fe85141aef4fe4696aa720c3f8a06d4e57930574bdab +f31f13994768b5b07e29624406c9b053bf4bb26e1623ac2bc1e9d4a9477502d6 diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index d1ef3bd822..72013e411e 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -6,8 +6,9 @@ - [ ] Bugfix (non-breaking change which fixes an issue) - [ ] New feature (non-breaking change which adds functionality) -- [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected) -- [ ] Developer breaking change (an API change that could break external components) +- [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected) — [policy](https://developers.esphome.io/contributing/code/#what-constitutes-a-c-breaking-change) +- [ ] Developer breaking change (an API change that could break external components) — [policy](https://developers.esphome.io/contributing/code/#what-is-considered-public-c-api) +- [ ] Undocumented C++ API change (removal or change of undocumented public methods that lambda users may depend on) — [policy](https://developers.esphome.io/contributing/code/#c-user-expectations) - [ ] Code quality improvements to existing code or addition of tests - [ ] Other @@ -24,7 +25,7 @@ - [ ] ESP32 - [ ] ESP32 IDF - [ ] ESP8266 -- [ ] RP2040 +- [ ] RP2040/RP2350 - [ ] BK72xx - [ ] RTL87xx - [ ] LN882x diff --git a/.github/actions/build-image/action.yaml b/.github/actions/build-image/action.yaml index 9c7f051e05..a895226030 100644 --- a/.github/actions/build-image/action.yaml +++ b/.github/actions/build-image/action.yaml @@ -47,7 +47,7 @@ runs: - name: Build and push to ghcr by digest id: build-ghcr - uses: docker/build-push-action@263435318d21b8e681c14492fe198d362a7d2c83 # v6.18.0 + uses: docker/build-push-action@d08e5c354a6adb9ed34480a06d141179aa583294 # v7.0.0 env: DOCKER_BUILD_SUMMARY: false DOCKER_BUILD_RECORD_UPLOAD: false @@ -73,7 +73,7 @@ runs: - name: Build and push to dockerhub by digest id: build-dockerhub - uses: docker/build-push-action@263435318d21b8e681c14492fe198d362a7d2c83 # v6.18.0 + uses: docker/build-push-action@d08e5c354a6adb9ed34480a06d141179aa583294 # v7.0.0 env: DOCKER_BUILD_SUMMARY: false DOCKER_BUILD_RECORD_UPLOAD: false diff --git a/.github/actions/restore-python/action.yml b/.github/actions/restore-python/action.yml index e178610a97..af54175c01 100644 --- a/.github/actions/restore-python/action.yml +++ b/.github/actions/restore-python/action.yml @@ -22,7 +22,7 @@ runs: python-version: ${{ inputs.python-version }} - name: Restore Python virtual environment id: cache-venv - uses: actions/cache/restore@8b402f58fbc84540c8b491a91e594a4576fec3d7 # v5.0.2 + uses: actions/cache/restore@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4 with: path: venv # yamllint disable-line rule:line-length diff --git a/.github/scripts/auto-label-pr/constants.js b/.github/scripts/auto-label-pr/constants.js index bd60d8c766..1c33772c4c 100644 --- a/.github/scripts/auto-label-pr/constants.js +++ b/.github/scripts/auto-label-pr/constants.js @@ -14,6 +14,7 @@ module.exports = { 'chained-pr', 'core', 'small-pr', + 'medium-pr', 'dashboard', 'github-actions', 'by-code-owner', @@ -27,6 +28,7 @@ module.exports = { 'new-feature', 'breaking-change', 'developer-breaking-change', + 'undocumented-api-change', 'code-quality', 'deprecated-component' ], diff --git a/.github/scripts/auto-label-pr/detectors.js b/.github/scripts/auto-label-pr/detectors.js index f502a85666..fb9dadc6a0 100644 --- a/.github/scripts/auto-label-pr/detectors.js +++ b/.github/scripts/auto-label-pr/detectors.js @@ -1,5 +1,13 @@ const fs = require('fs'); const { DOCS_PR_PATTERNS } = require('./constants'); +const { + COMPONENT_REGEX, + detectComponents, + hasCoreChanges, + hasDashboardChanges, + hasGitHubActionsChanges, +} = require('../detect-tags'); +const { loadCodeowners, getEffectiveOwners } = require('../codeowners'); // Strategy: Merge branch detection async function detectMergeBranch(context) { @@ -20,15 +28,13 @@ async function detectMergeBranch(context) { // Strategy: Component and platform labeling async function detectComponentPlatforms(changedFiles, apiData) { const labels = new Set(); - const componentRegex = /^esphome\/components\/([^\/]+)\//; const targetPlatformRegex = new RegExp(`^esphome\/components\/(${apiData.targetPlatforms.join('|')})/`); - for (const file of changedFiles) { - const componentMatch = file.match(componentRegex); - if (componentMatch) { - labels.add(`component: ${componentMatch[1]}`); - } + for (const comp of detectComponents(changedFiles)) { + labels.add(`component: ${comp}`); + } + for (const file of changedFiles) { const platformMatch = file.match(targetPlatformRegex); if (platformMatch) { labels.add(`platform: ${platformMatch[1]}`); @@ -90,20 +96,14 @@ async function detectNewPlatforms(prFiles, apiData) { // Strategy: Core files detection async function detectCoreChanges(changedFiles) { const labels = new Set(); - const coreFiles = changedFiles.filter(file => - file.startsWith('esphome/core/') || - (file.startsWith('esphome/') && file.split('/').length === 2) - ); - - if (coreFiles.length > 0) { + if (hasCoreChanges(changedFiles)) { labels.add('core'); } - return labels; } // Strategy: PR size detection -async function detectPRSize(prFiles, totalAdditions, totalDeletions, totalChanges, isMegaPR, SMALL_PR_THRESHOLD, TOO_BIG_THRESHOLD) { +async function detectPRSize(prFiles, totalAdditions, totalDeletions, totalChanges, isMegaPR, SMALL_PR_THRESHOLD, MEDIUM_PR_THRESHOLD, TOO_BIG_THRESHOLD) { const labels = new Set(); if (totalChanges <= SMALL_PR_THRESHOLD) { @@ -111,6 +111,11 @@ async function detectPRSize(prFiles, totalAdditions, totalDeletions, totalChange return labels; } + if (totalChanges <= MEDIUM_PR_THRESHOLD) { + labels.add('medium-pr'); + return labels; + } + const testAdditions = prFiles .filter(file => file.filename.startsWith('tests/')) .reduce((sum, file) => sum + (file.additions || 0), 0); @@ -131,80 +136,33 @@ async function detectPRSize(prFiles, totalAdditions, totalDeletions, totalChange // Strategy: Dashboard changes async function detectDashboardChanges(changedFiles) { const labels = new Set(); - const dashboardFiles = changedFiles.filter(file => - file.startsWith('esphome/dashboard/') || - file.startsWith('esphome/components/dashboard_import/') - ); - - if (dashboardFiles.length > 0) { + if (hasDashboardChanges(changedFiles)) { labels.add('dashboard'); } - return labels; } // Strategy: GitHub Actions changes async function detectGitHubActionsChanges(changedFiles) { const labels = new Set(); - const githubActionsFiles = changedFiles.filter(file => - file.startsWith('.github/workflows/') - ); - - if (githubActionsFiles.length > 0) { + if (hasGitHubActionsChanges(changedFiles)) { labels.add('github-actions'); } - return labels; } // Strategy: Code owner detection async function detectCodeOwner(github, context, changedFiles) { const labels = new Set(); - const { owner, repo } = context.repo; try { - const { data: codeownersFile } = await github.rest.repos.getContent({ - owner, - repo, - path: 'CODEOWNERS', - }); - - const codeownersContent = Buffer.from(codeownersFile.content, 'base64').toString('utf8'); + const codeownersPatterns = loadCodeowners(); const prAuthor = context.payload.pull_request.user.login; - const codeownersLines = codeownersContent.split('\n') - .map(line => line.trim()) - .filter(line => line && !line.startsWith('#')); - - const codeownersRegexes = codeownersLines.map(line => { - const parts = line.split(/\s+/); - const pattern = parts[0]; - const owners = parts.slice(1); - - let regex; - if (pattern.endsWith('*')) { - const dir = pattern.slice(0, -1); - regex = new RegExp(`^${dir.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}`); - } else if (pattern.includes('*')) { - // First escape all regex special chars except *, then replace * with .* - const regexPattern = pattern - .replace(/[.+?^${}()|[\]\\]/g, '\\$&') - .replace(/\*/g, '.*'); - regex = new RegExp(`^${regexPattern}$`); - } else { - regex = new RegExp(`^${pattern.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}$`); - } - - return { regex, owners }; - }); - - for (const file of changedFiles) { - for (const { regex, owners } of codeownersRegexes) { - if (regex.test(file) && owners.some(owner => owner === `@${prAuthor}`)) { - labels.add('by-code-owner'); - return labels; - } - } + // Check if PR author is a codeowner of any changed file + const effective = getEffectiveOwners(changedFiles, codeownersPatterns); + if (effective.users.has(prAuthor)) { + labels.add('by-code-owner'); } } catch (error) { console.log('Failed to read or parse CODEOWNERS file:', error.message); @@ -238,6 +196,7 @@ async function detectPRTemplateCheckboxes(context) { { pattern: /- \[x\] New feature \(non-breaking change which adds functionality\)/i, label: 'new-feature' }, { pattern: /- \[x\] Breaking change \(fix or feature that would cause existing functionality to not work as expected\)/i, label: 'breaking-change' }, { pattern: /- \[x\] Developer breaking change \(an API change that could break external components\)/i, label: 'developer-breaking-change' }, + { pattern: /- \[x\] Undocumented C\+\+ API change \(removal or change of undocumented public methods that lambda users may depend on\)/i, label: 'undocumented-api-change' }, { pattern: /- \[x\] Code quality improvements to existing code or addition of tests/i, label: 'code-quality' } ]; @@ -258,7 +217,7 @@ async function detectDeprecatedComponents(github, context, changedFiles) { const { owner, repo } = context.repo; // Compile regex once for better performance - const componentFileRegex = /^esphome\/components\/([^\/]+)\//; + const componentFileRegex = COMPONENT_REGEX; // Get files that are modified or added in components directory const componentFiles = changedFiles.filter(file => componentFileRegex.test(file)); @@ -276,19 +235,20 @@ async function detectDeprecatedComponents(github, context, changedFiles) { } } - // Get PR head to fetch files from the PR branch - const prNumber = context.payload.pull_request.number; + // Get base branch ref to check if deprecation already exists for the component + // This prevents flagging a PR that simply adds deprecation + const baseRef = context.payload.pull_request.base.ref; // 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 + // Fetch file content from base branch using GitHub API const { data: fileData } = await github.rest.repos.getContent({ owner, repo, path: initFile, - ref: `refs/pull/${prNumber}/head` + ref: baseRef }); // Decode base64 content diff --git a/.github/scripts/auto-label-pr/index.js b/.github/scripts/auto-label-pr/index.js index 483d2cb626..42588c0bc8 100644 --- a/.github/scripts/auto-label-pr/index.js +++ b/.github/scripts/auto-label-pr/index.js @@ -35,6 +35,7 @@ async function fetchApiData() { module.exports = async ({ github, context }) => { // Environment variables const SMALL_PR_THRESHOLD = parseInt(process.env.SMALL_PR_THRESHOLD); + const MEDIUM_PR_THRESHOLD = parseInt(process.env.MEDIUM_PR_THRESHOLD); const MAX_LABELS = parseInt(process.env.MAX_LABELS); const TOO_BIG_THRESHOLD = parseInt(process.env.TOO_BIG_THRESHOLD); const COMPONENT_LABEL_THRESHOLD = parseInt(process.env.COMPONENT_LABEL_THRESHOLD); @@ -120,7 +121,7 @@ module.exports = async ({ github, context }) => { detectNewComponents(prFiles), detectNewPlatforms(prFiles, apiData), detectCoreChanges(changedFiles), - detectPRSize(prFiles, totalAdditions, totalDeletions, totalChanges, isMegaPR, SMALL_PR_THRESHOLD, TOO_BIG_THRESHOLD), + detectPRSize(prFiles, totalAdditions, totalDeletions, totalChanges, isMegaPR, SMALL_PR_THRESHOLD, MEDIUM_PR_THRESHOLD, TOO_BIG_THRESHOLD), detectDashboardChanges(changedFiles), detectGitHubActionsChanges(changedFiles), detectCodeOwner(github, context, changedFiles), diff --git a/.github/scripts/codeowners.js b/.github/scripts/codeowners.js new file mode 100644 index 0000000000..9b2f2922c0 --- /dev/null +++ b/.github/scripts/codeowners.js @@ -0,0 +1,227 @@ +// Shared CODEOWNERS parsing and matching utilities. +// +// Used by: +// - codeowner-review-request.yml +// - codeowner-approved-label-update.yml +// - auto-label-pr/detectors.js (detectCodeOwner) + +/** + * Convert a CODEOWNERS glob pattern to a RegExp. + * + * Handles **, *, and ? wildcards after escaping regex-special characters. + */ +function globToRegex(pattern) { + let regexStr = pattern + .replace(/([.+^=!:${}()|[\]\\])/g, '\\$1') + .replace(/\*\*/g, '\x00GLOBSTAR\x00') // protect ** from next replace + .replace(/\*/g, '[^/]*') // single star + .replace(/\x00GLOBSTAR\x00/g, '.*') // restore globstar + .replace(/\?/g, '.'); + return new RegExp('^' + regexStr + '$'); +} + +/** + * Parse raw CODEOWNERS file content into an array of + * { pattern, regex, owners } objects. + * + * Each `owners` entry is the raw string from the file (e.g. "@user" or + * "@esphome/core"). + */ +function parseCodeowners(content) { + const lines = content + .split('\n') + .map(line => line.trim()) + .filter(line => line && !line.startsWith('#')); + + const patterns = []; + for (const line of lines) { + const parts = line.split(/\s+/); + if (parts.length < 2) continue; + + const pattern = parts[0]; + const owners = parts.slice(1); + const regex = globToRegex(pattern); + patterns.push({ pattern, regex, owners }); + } + return patterns; +} + +/** + * Fetch and parse the CODEOWNERS file via the GitHub API. + * + * @param {object} github - octokit instance from actions/github-script + * @param {string} owner - repo owner + * @param {string} repo - repo name + * @param {string} [ref] - git ref (SHA / branch) to read from + * @returns {Array<{pattern: string, regex: RegExp, owners: string[]}>} + */ +async function fetchCodeowners(github, owner, repo, ref) { + const params = { owner, repo, path: 'CODEOWNERS' }; + if (ref) params.ref = ref; + + const { data: file } = await github.rest.repos.getContent(params); + const content = Buffer.from(file.content, 'base64').toString('utf8'); + return parseCodeowners(content); +} + +/** + * Classify raw owner strings into individual users and teams. + * + * @param {string[]} rawOwners - e.g. ["@user1", "@esphome/core"] + * @returns {{ users: string[], teams: string[] }} + * users – login names without "@" + * teams – team slugs without the "org/" prefix + */ +function classifyOwners(rawOwners) { + const users = []; + const teams = []; + for (const o of rawOwners) { + const clean = o.startsWith('@') ? o.slice(1) : o; + if (clean.includes('/')) { + teams.push(clean.split('/')[1]); + } else { + users.push(clean); + } + } + return { users, teams }; +} + +/** + * For each file, find its effective codeowners using GitHub's + * "last match wins" semantics, then union across all files. + * + * @param {string[]} files - list of file paths + * @param {Array} codeownersPatterns - from parseCodeowners / fetchCodeowners + * @returns {{ users: Set, teams: Set, matchedFileCount: number }} + */ +function getEffectiveOwners(files, codeownersPatterns) { + const users = new Set(); + const teams = new Set(); + let matchedFileCount = 0; + + for (const file of files) { + // Last matching pattern wins for each file + let effectiveOwners = null; + for (const { regex, owners } of codeownersPatterns) { + if (regex.test(file)) { + effectiveOwners = owners; + } + } + if (effectiveOwners) { + matchedFileCount++; + const classified = classifyOwners(effectiveOwners); + for (const u of classified.users) users.add(u); + for (const t of classified.teams) teams.add(t); + } + } + + return { users, teams, matchedFileCount }; +} + +/** + * Read and parse the CODEOWNERS file from disk. + * + * Use this when the repo is already checked out (avoids an API call). + * + * @param {string} [repoRoot='.'] - path to the repo root + * @returns {Array<{pattern: string, regex: RegExp, owners: string[]}>} + */ +function loadCodeowners(repoRoot = '.') { + const fs = require('fs'); + const path = require('path'); + const content = fs.readFileSync(path.join(repoRoot, 'CODEOWNERS'), 'utf8'); + return parseCodeowners(content); +} + +/** Possible label actions returned by determineLabelAction. */ +const LabelAction = Object.freeze({ + ADD: 'add', + REMOVE: 'remove', + NONE: 'none', +}); + +/** + * Determine what label action is needed for a PR based on codeowner approvals. + * + * Checks changed files against CODEOWNERS patterns, reviews, and current labels + * to decide if the label should be added, removed, or left unchanged. + * + * @param {object} github - octokit instance from actions/github-script + * @param {string} owner - repo owner + * @param {string} repo - repo name + * @param {number} pr_number - pull request number + * @param {Array} codeownersPatterns - from loadCodeowners / fetchCodeowners + * @param {string} labelName - label to manage + * @returns {Promise} + */ +async function determineLabelAction(github, owner, repo, pr_number, codeownersPatterns, labelName) { + // Get the list of changed files in this PR + const prFiles = await github.paginate( + github.rest.pulls.listFiles, + { owner, repo, pull_number: pr_number } + ); + + const changedFiles = prFiles.map(file => file.filename); + console.log(`Found ${changedFiles.length} changed files`); + + if (changedFiles.length === 0) { + console.log('No changed files found'); + return LabelAction.NONE; + } + + // Get effective owners using last-match-wins semantics + const effective = getEffectiveOwners(changedFiles, codeownersPatterns); + const componentCodeowners = effective.users; + + console.log(`Component-specific codeowners: ${Array.from(componentCodeowners).join(', ') || '(none)'}`); + + // Get current labels + const { data: currentLabels } = await github.rest.issues.listLabelsOnIssue({ + owner, repo, issue_number: pr_number + }); + const hasLabel = currentLabels.some(label => label.name === labelName); + + if (componentCodeowners.size === 0) { + console.log('No component-specific codeowners found'); + return hasLabel ? LabelAction.REMOVE : LabelAction.NONE; + } + + // Get all reviews and find latest per user + const reviews = await github.paginate( + github.rest.pulls.listReviews, + { owner, repo, pull_number: pr_number } + ); + + const latestReviewByUser = new Map(); + for (const review of reviews) { + if (!review.user || review.user.type === 'Bot' || review.state === 'COMMENTED') continue; + latestReviewByUser.set(review.user.login, review); + } + + // Check if any component-specific codeowner has an active approval + let hasCodeownerApproval = false; + for (const [login, review] of latestReviewByUser) { + if (review.state === 'APPROVED' && componentCodeowners.has(login)) { + console.log(`Codeowner '${login}' has approved`); + hasCodeownerApproval = true; + break; + } + } + + if (hasCodeownerApproval && !hasLabel) return LabelAction.ADD; + if (!hasCodeownerApproval && hasLabel) return LabelAction.REMOVE; + + console.log(`Label already ${hasLabel ? 'present' : 'absent'}, no change needed`); + return LabelAction.NONE; +} + +module.exports = { + globToRegex, + parseCodeowners, + fetchCodeowners, + loadCodeowners, + classifyOwners, + getEffectiveOwners, + LabelAction, + determineLabelAction +}; diff --git a/.github/scripts/detect-tags.js b/.github/scripts/detect-tags.js new file mode 100644 index 0000000000..3933776c61 --- /dev/null +++ b/.github/scripts/detect-tags.js @@ -0,0 +1,66 @@ +/** + * Shared tag detection from changed file paths. + * Used by pr-title-check and auto-label-pr workflows. + */ + +const COMPONENT_REGEX = /^esphome\/components\/([^\/]+)\//; + +/** + * Detect component names from changed files. + * @param {string[]} changedFiles - List of changed file paths + * @returns {Set} Set of component names + */ +function detectComponents(changedFiles) { + const components = new Set(); + for (const file of changedFiles) { + const match = file.match(COMPONENT_REGEX); + if (match) { + components.add(match[1]); + } + } + return components; +} + +/** + * Detect if core files were changed. + * Core files are in esphome/core/ or top-level esphome/ directory. + * @param {string[]} changedFiles - List of changed file paths + * @returns {boolean} + */ +function hasCoreChanges(changedFiles) { + return changedFiles.some(file => + file.startsWith('esphome/core/') || + (file.startsWith('esphome/') && file.split('/').length === 2) + ); +} + +/** + * Detect if dashboard files were changed. + * @param {string[]} changedFiles - List of changed file paths + * @returns {boolean} + */ +function hasDashboardChanges(changedFiles) { + return changedFiles.some(file => + file.startsWith('esphome/dashboard/') || + file.startsWith('esphome/components/dashboard_import/') + ); +} + +/** + * Detect if GitHub Actions files were changed. + * @param {string[]} changedFiles - List of changed file paths + * @returns {boolean} + */ +function hasGitHubActionsChanges(changedFiles) { + return changedFiles.some(file => + file.startsWith('.github/workflows/') + ); +} + +module.exports = { + COMPONENT_REGEX, + detectComponents, + hasCoreChanges, + hasDashboardChanges, + hasGitHubActionsChanges, +}; diff --git a/.github/workflows/auto-label-pr.yml b/.github/workflows/auto-label-pr.yml index 6fcb50b70a..3b5e9f0d15 100644 --- a/.github/workflows/auto-label-pr.yml +++ b/.github/workflows/auto-label-pr.yml @@ -12,6 +12,7 @@ permissions: env: SMALL_PR_THRESHOLD: 30 + MEDIUM_PR_THRESHOLD: 100 MAX_LABELS: 15 TOO_BIG_THRESHOLD: 1000 COMPONENT_LABEL_THRESHOLD: 10 @@ -26,7 +27,7 @@ jobs: - name: Generate a token id: generate-token - uses: actions/create-github-app-token@29824e69f54612133e76f7eaac726eef6c875baf # v2 + uses: actions/create-github-app-token@f8d387b68d61c58ab83c6c016672934102569859 # v2 with: app-id: ${{ secrets.ESPHOME_GITHUB_APP_ID }} private-key: ${{ secrets.ESPHOME_GITHUB_APP_PRIVATE_KEY }} diff --git a/.github/workflows/ci-api-proto.yml b/.github/workflows/ci-api-proto.yml index 0328611f5c..6d200956e9 100644 --- a/.github/workflows/ci-api-proto.yml +++ b/.github/workflows/ci-api-proto.yml @@ -62,7 +62,7 @@ jobs: run: git diff - if: failure() name: Archive artifacts - uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0 + uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 with: name: generated-proto-files path: | diff --git a/.github/workflows/ci-clang-tidy-hash.yml b/.github/workflows/ci-clang-tidy-hash.yml index 5054a62207..7905739b15 100644 --- a/.github/workflows/ci-clang-tidy-hash.yml +++ b/.github/workflows/ci-clang-tidy-hash.yml @@ -40,7 +40,7 @@ jobs: echo "You have modified clang-tidy configuration but have not updated the hash." | tee -a $GITHUB_STEP_SUMMARY echo "Please run 'script/clang_tidy_hash.py --update' and commit the changes." | tee -a $GITHUB_STEP_SUMMARY - - if: failure() + - if: failure() && github.event.pull_request.head.repo.full_name == github.repository name: Request changes uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 with: @@ -53,7 +53,7 @@ jobs: body: 'You have modified clang-tidy configuration but have not updated the hash.\nPlease run `script/clang_tidy_hash.py --update` and commit the changes.' }) - - if: success() + - if: success() && github.event.pull_request.head.repo.full_name == github.repository name: Dismiss review uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 with: diff --git a/.github/workflows/ci-docker.yml b/.github/workflows/ci-docker.yml index a83bcae0b0..4009ac1e17 100644 --- a/.github/workflows/ci-docker.yml +++ b/.github/workflows/ci-docker.yml @@ -49,7 +49,7 @@ jobs: with: python-version: "3.11" - name: Set up Docker Buildx - uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3.12.0 + uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4.0.0 - name: Set TAG run: | diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index f521b07bae..06e8189f54 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -47,7 +47,7 @@ jobs: python-version: ${{ env.DEFAULT_PYTHON }} - name: Restore Python virtual environment id: cache-venv - uses: actions/cache@8b402f58fbc84540c8b491a91e594a4576fec3d7 # v5.0.2 + uses: actions/cache@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4 with: path: venv # yamllint disable-line rule:line-length @@ -106,6 +106,7 @@ jobs: script/build_codeowners.py --check script/build_language_schema.py --check script/generate-esp32-boards.py --check + script/generate-rp2040-boards.py --check pytest: name: Run pytest @@ -115,6 +116,7 @@ jobs: python-version: - "3.11" - "3.13" + - "3.14" os: - ubuntu-latest - macOS-latest @@ -152,12 +154,12 @@ jobs: . venv/bin/activate pytest -vv --cov-report=xml --tb=native -n auto tests --ignore=tests/integration/ - name: Upload coverage to Codecov - uses: codecov/codecov-action@671740ac38dd9b0130fbe1cec585b89eea48d3de # v5.5.2 + uses: codecov/codecov-action@57e3a136b779b570ffcdbf80b3bdc90e7fab3de2 # v6.0.0 with: token: ${{ secrets.CODECOV_TOKEN }} - name: Save Python virtual environment cache if: github.ref == 'refs/heads/dev' - uses: actions/cache/save@8b402f58fbc84540c8b491a91e594a4576fec3d7 # v5.0.2 + uses: actions/cache/save@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4 with: path: venv key: ${{ runner.os }}-${{ steps.restore-python.outputs.python-version }}-venv-${{ needs.common.outputs.cache-key }} @@ -169,6 +171,8 @@ jobs: - common outputs: integration-tests: ${{ steps.determine.outputs.integration-tests }} + integration-tests-run-all: ${{ steps.determine.outputs.integration-tests-run-all }} + integration-test-files: ${{ steps.determine.outputs.integration-test-files }} clang-tidy: ${{ steps.determine.outputs.clang-tidy }} clang-tidy-mode: ${{ steps.determine.outputs.clang-tidy-mode }} python-linters: ${{ steps.determine.outputs.python-linters }} @@ -181,6 +185,7 @@ jobs: cpp-unit-tests-run-all: ${{ steps.determine.outputs.cpp-unit-tests-run-all }} cpp-unit-tests-components: ${{ steps.determine.outputs.cpp-unit-tests-components }} component-test-batches: ${{ steps.determine.outputs.component-test-batches }} + benchmarks: ${{ steps.determine.outputs.benchmarks }} steps: - name: Check out code from GitHub uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 @@ -193,7 +198,7 @@ jobs: python-version: ${{ env.DEFAULT_PYTHON }} cache-key: ${{ needs.common.outputs.cache-key }} - name: Restore components graph cache - uses: actions/cache/restore@8b402f58fbc84540c8b491a91e594a4576fec3d7 # v5.0.2 + uses: actions/cache/restore@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4 with: path: .temp/components_graph.json key: components-graph-${{ hashFiles('esphome/components/**/*.py') }} @@ -209,6 +214,8 @@ jobs: # Extract individual fields echo "integration-tests=$(echo "$output" | jq -r '.integration_tests')" >> $GITHUB_OUTPUT + echo "integration-tests-run-all=$(echo "$output" | jq -r '.integration_tests_run_all')" >> $GITHUB_OUTPUT + echo "integration-test-files=$(echo "$output" | jq -c '.integration_test_files')" >> $GITHUB_OUTPUT echo "clang-tidy=$(echo "$output" | jq -r '.clang_tidy')" >> $GITHUB_OUTPUT echo "clang-tidy-mode=$(echo "$output" | jq -r '.clang_tidy_mode')" >> $GITHUB_OUTPUT echo "python-linters=$(echo "$output" | jq -r '.python_linters')" >> $GITHUB_OUTPUT @@ -221,9 +228,10 @@ jobs: echo "cpp-unit-tests-run-all=$(echo "$output" | jq -r '.cpp_unit_tests_run_all')" >> $GITHUB_OUTPUT echo "cpp-unit-tests-components=$(echo "$output" | jq -c '.cpp_unit_tests_components')" >> $GITHUB_OUTPUT echo "component-test-batches=$(echo "$output" | jq -c '.component_test_batches')" >> $GITHUB_OUTPUT + echo "benchmarks=$(echo "$output" | jq -r '.benchmarks')" >> $GITHUB_OUTPUT - name: Save components graph cache if: github.ref == 'refs/heads/dev' - uses: actions/cache/save@8b402f58fbc84540c8b491a91e594a4576fec3d7 # v5.0.2 + uses: actions/cache/save@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4 with: path: .temp/components_graph.json key: components-graph-${{ hashFiles('esphome/components/**/*.py') }} @@ -245,7 +253,7 @@ jobs: python-version: "3.13" - name: Restore Python virtual environment id: cache-venv - uses: actions/cache@8b402f58fbc84540c8b491a91e594a4576fec3d7 # v5.0.2 + uses: actions/cache@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4 with: path: venv key: ${{ runner.os }}-${{ steps.python.outputs.python-version }}-venv-${{ needs.common.outputs.cache-key }} @@ -260,9 +268,20 @@ jobs: - name: Register matcher run: echo "::add-matcher::.github/workflows/matchers/pytest.json" - name: Run integration tests + env: + INTEGRATION_TEST_FILES: ${{ needs.determine-jobs.outputs.integration-test-files }} + INTEGRATION_TESTS_RUN_ALL: ${{ needs.determine-jobs.outputs.integration-tests-run-all }} run: | . venv/bin/activate - pytest -vv --no-cov --tb=native -n auto tests/integration/ + if [[ "$INTEGRATION_TESTS_RUN_ALL" == "true" ]]; then + echo "Running all integration tests" + pytest -vv --no-cov --tb=native -n auto tests/integration/ + else + # Parse JSON array into bash array to avoid shell expansion issues + mapfile -t test_files < <(echo "$INTEGRATION_TEST_FILES" | jq -r '.[]') + echo "Running ${#test_files[@]} specific integration tests" + pytest -vv --no-cov --tb=native -n auto "${test_files[@]}" + fi cpp-unit-tests: name: Run C++ unit tests @@ -291,6 +310,40 @@ jobs: script/cpp_unit_test.py $ARGS fi + benchmarks: + name: Run CodSpeed benchmarks + runs-on: ubuntu-24.04 + needs: + - common + - determine-jobs + if: >- + (github.event_name == 'push' && github.ref_name == 'dev') || + (github.event_name == 'pull_request' && needs.determine-jobs.outputs.benchmarks == 'true') + steps: + - name: Check out code from GitHub + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + + - name: Restore Python + uses: ./.github/actions/restore-python + with: + python-version: ${{ env.DEFAULT_PYTHON }} + cache-key: ${{ needs.common.outputs.cache-key }} + + - name: Build benchmarks + id: build + run: | + . venv/bin/activate + export BENCHMARK_LIB_CONFIG=$(python script/setup_codspeed_lib.py) + # --build-only prints BUILD_BINARY= to stdout + BINARY=$(script/cpp_benchmark.py --all --build-only | grep '^BUILD_BINARY=' | tail -1 | cut -d= -f2-) + echo "binary=$BINARY" >> $GITHUB_OUTPUT + + - name: Run CodSpeed benchmarks + uses: CodSpeedHQ/action@d872884a306dd4853acf0f584f4b706cf0cc72a2 # v4 + with: + run: ${{ steps.build.outputs.binary }} + mode: simulation + clang-tidy-single: name: ${{ matrix.name }} runs-on: ubuntu-24.04 @@ -334,14 +387,14 @@ jobs: - name: Cache platformio if: github.ref == 'refs/heads/dev' - uses: actions/cache@8b402f58fbc84540c8b491a91e594a4576fec3d7 # v5.0.2 + uses: actions/cache@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4 with: path: ~/.platformio key: platformio-${{ matrix.pio_cache_key }}-${{ hashFiles('platformio.ini') }} - name: Cache platformio if: github.ref != 'refs/heads/dev' - uses: actions/cache/restore@8b402f58fbc84540c8b491a91e594a4576fec3d7 # v5.0.2 + uses: actions/cache/restore@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4 with: path: ~/.platformio key: platformio-${{ matrix.pio_cache_key }}-${{ hashFiles('platformio.ini') }} @@ -413,14 +466,14 @@ jobs: - name: Cache platformio if: github.ref == 'refs/heads/dev' - uses: actions/cache@8b402f58fbc84540c8b491a91e594a4576fec3d7 # v5.0.2 + uses: actions/cache@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4 with: path: ~/.platformio key: platformio-tidyesp32-${{ hashFiles('platformio.ini') }} - name: Cache platformio if: github.ref != 'refs/heads/dev' - uses: actions/cache/restore@8b402f58fbc84540c8b491a91e594a4576fec3d7 # v5.0.2 + uses: actions/cache/restore@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4 with: path: ~/.platformio key: platformio-tidyesp32-${{ hashFiles('platformio.ini') }} @@ -502,14 +555,14 @@ jobs: - name: Cache platformio if: github.ref == 'refs/heads/dev' - uses: actions/cache@8b402f58fbc84540c8b491a91e594a4576fec3d7 # v5.0.2 + uses: actions/cache@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4 with: path: ~/.platformio key: platformio-tidyesp32-${{ hashFiles('platformio.ini') }} - name: Cache platformio if: github.ref != 'refs/heads/dev' - uses: actions/cache/restore@8b402f58fbc84540c8b491a91e594a4576fec3d7 # v5.0.2 + uses: actions/cache/restore@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4 with: path: ~/.platformio key: platformio-tidyesp32-${{ hashFiles('platformio.ini') }} @@ -670,7 +723,7 @@ jobs: cache-key: ${{ needs.common.outputs.cache-key }} - uses: esphome/pre-commit-action@43cd1109c09c544d97196f7730ee5b2e0cc6d81e # v3.0.1 fork with pinned actions/cache env: - SKIP: pylint,clang-tidy-hash + SKIP: pylint,clang-tidy-hash,ci-custom - uses: pre-commit-ci/lite-action@5d6cc0eb514c891a40562a58a8e71576c5c7fb43 # v1.1.0 if: always() @@ -685,7 +738,7 @@ jobs: ram_usage: ${{ steps.extract.outputs.ram_usage }} flash_usage: ${{ steps.extract.outputs.flash_usage }} cache_hit: ${{ steps.cache-memory-analysis.outputs.cache-hit }} - skip: ${{ steps.check-script.outputs.skip }} + skip: ${{ steps.check-script.outputs.skip || steps.check-tests.outputs.skip }} steps: - name: Check out target branch uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 @@ -704,10 +757,39 @@ jobs: echo "::warning::ci_memory_impact_extract.py not found on target branch, skipping memory impact analysis" fi - # All remaining steps only run if script exists + # Check if test files exist on the target branch for the requested + # components and platform. When a PR adds new test files for a platform, + # the target branch won't have them yet, so skip instead of failing. + # This check must be done here (not in determine-jobs.py) because + # determine-jobs runs on the PR branch and cannot see what the target + # branch has. + - name: Check for test files on target branch + id: check-tests + if: steps.check-script.outputs.skip != 'true' + run: | + components='${{ toJSON(fromJSON(needs.determine-jobs.outputs.memory_impact).components) }}' + platform="${{ fromJSON(needs.determine-jobs.outputs.memory_impact).platform }}" + found=false + for component in $(echo "$components" | jq -r '.[]'); do + # Check for test files matching the platform (test.platform.yaml or test-*.platform.yaml) + for f in tests/components/${component}/test*.${platform}.yaml; do + if [ -f "$f" ]; then + found=true + break 2 + fi + done + done + if [ "$found" = false ]; then + echo "skip=true" >> $GITHUB_OUTPUT + echo "::warning::No test files found on target branch for platform ${platform}, skipping memory impact analysis" + else + echo "skip=false" >> $GITHUB_OUTPUT + fi + + # All remaining steps only run if script and tests exist - name: Generate cache key id: cache-key - if: steps.check-script.outputs.skip != 'true' + if: steps.check-script.outputs.skip != 'true' && steps.check-tests.outputs.skip != 'true' run: | # Get the commit SHA of the target branch target_sha=$(git rev-parse HEAD) @@ -734,14 +816,14 @@ jobs: - name: Restore cached memory analysis id: cache-memory-analysis - if: steps.check-script.outputs.skip != 'true' - uses: actions/cache/restore@8b402f58fbc84540c8b491a91e594a4576fec3d7 # v5.0.2 + if: steps.check-script.outputs.skip != 'true' && steps.check-tests.outputs.skip != 'true' + uses: actions/cache/restore@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4 with: path: memory-analysis-target.json key: ${{ steps.cache-key.outputs.cache-key }} - name: Cache status - if: steps.check-script.outputs.skip != 'true' + if: steps.check-script.outputs.skip != 'true' && steps.check-tests.outputs.skip != 'true' run: | if [ "${{ steps.cache-memory-analysis.outputs.cache-hit }}" == "true" ]; then echo "✓ Cache hit! Using cached memory analysis results." @@ -751,21 +833,21 @@ jobs: fi - name: Restore Python - if: steps.check-script.outputs.skip != 'true' && steps.cache-memory-analysis.outputs.cache-hit != 'true' + if: steps.check-script.outputs.skip != 'true' && steps.check-tests.outputs.skip != 'true' && steps.cache-memory-analysis.outputs.cache-hit != 'true' uses: ./.github/actions/restore-python with: python-version: ${{ env.DEFAULT_PYTHON }} cache-key: ${{ needs.common.outputs.cache-key }} - name: Cache platformio - if: steps.check-script.outputs.skip != 'true' && steps.cache-memory-analysis.outputs.cache-hit != 'true' - uses: actions/cache/restore@8b402f58fbc84540c8b491a91e594a4576fec3d7 # v5.0.2 + if: steps.check-script.outputs.skip != 'true' && steps.check-tests.outputs.skip != 'true' && steps.cache-memory-analysis.outputs.cache-hit != 'true' + uses: actions/cache/restore@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4 with: path: ~/.platformio key: platformio-memory-${{ fromJSON(needs.determine-jobs.outputs.memory_impact).platform }}-${{ hashFiles('platformio.ini') }} - name: Build, compile, and analyze memory - if: steps.check-script.outputs.skip != 'true' && steps.cache-memory-analysis.outputs.cache-hit != 'true' + if: steps.check-script.outputs.skip != 'true' && steps.check-tests.outputs.skip != 'true' && steps.cache-memory-analysis.outputs.cache-hit != 'true' id: build run: | . venv/bin/activate @@ -799,15 +881,15 @@ jobs: --platform "$platform" - 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' - uses: actions/cache/save@8b402f58fbc84540c8b491a91e594a4576fec3d7 # v5.0.2 + if: steps.check-script.outputs.skip != 'true' && steps.check-tests.outputs.skip != 'true' && steps.cache-memory-analysis.outputs.cache-hit != 'true' && steps.build.outcome == 'success' + uses: actions/cache/save@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4 with: path: memory-analysis-target.json key: ${{ steps.cache-key.outputs.cache-key }} - name: Extract memory usage for outputs id: extract - if: steps.check-script.outputs.skip != 'true' + if: steps.check-script.outputs.skip != 'true' && steps.check-tests.outputs.skip != 'true' run: | if [ -f memory-analysis-target.json ]; then ram=$(jq -r '.ram_bytes' memory-analysis-target.json) @@ -821,7 +903,7 @@ jobs: fi - name: Upload memory analysis JSON - uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0 + uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 with: name: memory-analysis-target path: memory-analysis-target.json @@ -847,7 +929,7 @@ jobs: python-version: ${{ env.DEFAULT_PYTHON }} cache-key: ${{ needs.common.outputs.cache-key }} - name: Cache platformio - uses: actions/cache/restore@8b402f58fbc84540c8b491a91e594a4576fec3d7 # v5.0.2 + uses: actions/cache/restore@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4 with: path: ~/.platformio key: platformio-memory-${{ fromJSON(needs.determine-jobs.outputs.memory_impact).platform }}-${{ hashFiles('platformio.ini') }} @@ -885,7 +967,7 @@ jobs: --platform "$platform" - name: Upload memory analysis JSON - uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0 + uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 with: name: memory-analysis-pr path: memory-analysis-pr.json @@ -915,13 +997,13 @@ jobs: python-version: ${{ env.DEFAULT_PYTHON }} cache-key: ${{ needs.common.outputs.cache-key }} - name: Download target analysis JSON - uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0 + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 with: name: memory-analysis-target path: ./memory-analysis continue-on-error: true - name: Download PR analysis JSON - uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0 + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 with: name: memory-analysis-pr path: ./memory-analysis diff --git a/.github/workflows/codeowner-approved-label-update.yml b/.github/workflows/codeowner-approved-label-update.yml new file mode 100644 index 0000000000..34ff934b77 --- /dev/null +++ b/.github/workflows/codeowner-approved-label-update.yml @@ -0,0 +1,81 @@ +# Adds/removes a 'code-owner-approved' label when a component-specific +# codeowner approves (or dismisses) a PR. +# +# Uses pull_request_target so that fork PRs do not require workflow approval. +# The label is reconciled on every PR update; for review events specifically, +# this means the label is applied on the next push after a codeowner review. + +name: Codeowner Approved Label + +on: + pull_request_target: + types: [opened, synchronize, reopened, ready_for_review] + branches-ignore: + - release + - beta + +permissions: + issues: write + pull-requests: read + contents: read + +jobs: + codeowner-approved: + name: Run + if: ${{ github.repository == 'esphome/esphome' }} + runs-on: ubuntu-latest + steps: + - name: Checkout base branch + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + ref: ${{ github.event.pull_request.base.sha }} + sparse-checkout: | + .github/scripts/codeowners.js + CODEOWNERS + + - name: Check codeowner approval and update label + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 + env: + PR_NUMBER: ${{ github.event.pull_request.number }} + with: + script: | + const { loadCodeowners, determineLabelAction, LabelAction } = require('./.github/scripts/codeowners.js'); + + const owner = context.repo.owner; + const repo = context.repo.repo; + const pr_number = parseInt(process.env.PR_NUMBER, 10); + const LABEL_NAME = 'code-owner-approved'; + + console.log(`Processing PR #${pr_number} for codeowner approval label`); + + const codeownersPatterns = loadCodeowners(); + const action = await determineLabelAction( + github, owner, repo, pr_number, codeownersPatterns, LABEL_NAME + ); + + if (action === LabelAction.NONE) { + console.log('No label change needed'); + return; + } + + try { + if (action === LabelAction.ADD) { + await github.rest.issues.addLabels({ + owner, repo, issue_number: pr_number, labels: [LABEL_NAME] + }); + console.log(`Added '${LABEL_NAME}' label`); + } else if (action === LabelAction.REMOVE) { + await github.rest.issues.removeLabel({ + owner, repo, issue_number: pr_number, name: LABEL_NAME + }); + console.log(`Removed '${LABEL_NAME}' label`); + } + } catch (error) { + if (error.status === 403) { + console.log(`Warning: insufficient permissions to update label (expected for fork PRs)`); + } else if (error.status === 404) { + console.log(`Label '${LABEL_NAME}' not present, nothing to remove`); + } else { + throw error; + } + } diff --git a/.github/workflows/codeowner-review-request.yml b/.github/workflows/codeowner-review-request.yml index 6f4351b298..a89c03ba04 100644 --- a/.github/workflows/codeowner-review-request.yml +++ b/.github/workflows/codeowner-review-request.yml @@ -13,6 +13,9 @@ on: # Needs to be pull_request_target to get write permissions pull_request_target: types: [opened, reopened, synchronize, ready_for_review] + branches-ignore: + - release + - beta permissions: pull-requests: write @@ -24,10 +27,17 @@ jobs: if: ${{ github.repository == 'esphome/esphome' && !github.event.pull_request.draft }} runs-on: ubuntu-latest steps: + - name: Checkout base branch + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + ref: ${{ github.event.pull_request.base.sha }} + - name: Request reviews from component codeowners uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 with: script: | + const { loadCodeowners, getEffectiveOwners } = require('./.github/scripts/codeowners.js'); + const owner = context.repo.owner; const repo = context.repo.repo; const pr_number = context.payload.pull_request.number; @@ -38,12 +48,15 @@ jobs: const BOT_COMMENT_MARKER = ''; try { - // Get the list of changed files in this PR - const { data: files } = await github.rest.pulls.listFiles({ - owner, - repo, - pull_number: pr_number - }); + // Get the list of changed files in this PR (with pagination) + const files = await github.paginate( + github.rest.pulls.listFiles, + { + owner, + repo, + pull_number: pr_number + } + ); const changedFiles = files.map(file => file.filename); console.log(`Found ${changedFiles.length} changed files`); @@ -53,32 +66,10 @@ jobs: return; } - // Fetch CODEOWNERS file from root - const { data: codeownersFile } = await github.rest.repos.getContent({ - owner, - repo, - path: 'CODEOWNERS', - ref: context.payload.pull_request.base.sha - }); - const codeownersContent = Buffer.from(codeownersFile.content, 'base64').toString('utf8'); + // Parse CODEOWNERS from the checked-out base branch + const codeownersPatterns = loadCodeowners(); - // Parse CODEOWNERS file to extract all patterns and their owners - const codeownersLines = codeownersContent.split('\n') - .map(line => line.trim()) - .filter(line => line && !line.startsWith('#')); - - const codeownersPatterns = []; - - // Convert CODEOWNERS pattern to regex (robust glob handling) - function globToRegex(pattern) { - // Escape regex special characters except for glob wildcards - let regexStr = pattern - .replace(/([.+^=!:${}()|[\]\\])/g, '\\$1') // escape regex chars - .replace(/\*\*/g, '.*') // globstar - .replace(/\*/g, '[^/]*') // single star - .replace(/\?/g, '.'); // question mark - return new RegExp('^' + regexStr + '$'); - } + console.log(`Parsed ${codeownersPatterns.length} codeowner patterns`); // Helper function to create comment body function createCommentBody(reviewersList, teamsList, matchedFileCount, isSuccessful = true) { @@ -93,50 +84,11 @@ jobs: } } - for (const line of codeownersLines) { - const parts = line.split(/\s+/); - if (parts.length < 2) continue; - - const pattern = parts[0]; - const owners = parts.slice(1); - - // Use robust glob-to-regex conversion - const regex = globToRegex(pattern); - codeownersPatterns.push({ pattern, regex, owners }); - } - - console.log(`Parsed ${codeownersPatterns.length} codeowner patterns`); - - // Match changed files against CODEOWNERS patterns - const matchedOwners = new Set(); - const matchedTeams = new Set(); - const fileMatches = new Map(); // Track which files matched which patterns - - for (const file of changedFiles) { - for (const { pattern, regex, owners } of codeownersPatterns) { - if (regex.test(file)) { - console.log(`File '${file}' matches pattern '${pattern}' with owners: ${owners.join(', ')}`); - - if (!fileMatches.has(file)) { - fileMatches.set(file, []); - } - fileMatches.get(file).push({ pattern, owners }); - - // Add owners to the appropriate set (remove @ prefix) - for (const owner of owners) { - const cleanOwner = owner.startsWith('@') ? owner.slice(1) : owner; - if (cleanOwner.includes('/')) { - // Team mention (org/team-name) - const teamName = cleanOwner.split('/')[1]; - matchedTeams.add(teamName); - } else { - // Individual user - matchedOwners.add(cleanOwner); - } - } - } - } - } + // Match changed files against CODEOWNERS patterns using last-match-wins semantics + const effective = getEffectiveOwners(changedFiles, codeownersPatterns); + const matchedOwners = effective.users; + const matchedTeams = effective.teams; + const matchedFileCount = effective.matchedFileCount; if (matchedOwners.size === 0 && matchedTeams.size === 0) { console.log('No codeowners found for any changed files'); @@ -170,11 +122,14 @@ jobs: } // Check for completed reviews to avoid re-requesting users who have already reviewed - const { data: reviews } = await github.rest.pulls.listReviews({ - owner, - repo, - pull_number: pr_number - }); + const reviews = await github.paginate( + github.rest.pulls.listReviews, + { + owner, + repo, + pull_number: pr_number + } + ); const reviewedUsers = new Set(); reviews.forEach(review => { @@ -247,7 +202,7 @@ jobs: } const totalReviewers = reviewersList.length + teamsList.length; - console.log(`Requesting reviews from ${reviewersList.length} users and ${teamsList.length} teams for ${fileMatches.size} matched files`); + console.log(`Requesting reviews from ${reviewersList.length} users and ${teamsList.length} teams for ${matchedFileCount} matched files`); // Request reviews try { @@ -279,7 +234,7 @@ jobs: // Only add a comment if there are new codeowners to mention (not previously pinged) if (reviewersList.length > 0 || teamsList.length > 0) { - const commentBody = createCommentBody(reviewersList, teamsList, fileMatches.size, true); + const commentBody = createCommentBody(reviewersList, teamsList, matchedFileCount, true); await github.rest.issues.createComment({ owner, @@ -297,7 +252,7 @@ jobs: // Only try to add a comment if there are new codeowners to mention if (reviewersList.length > 0 || teamsList.length > 0) { - const commentBody = createCommentBody(reviewersList, teamsList, fileMatches.size, false); + const commentBody = createCommentBody(reviewersList, teamsList, matchedFileCount, false); try { await github.rest.issues.createComment({ diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index be761cee3d..67f4690ac9 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -58,7 +58,7 @@ jobs: # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL - uses: github/codeql-action/init@b20883b0cd1f46c72ae0ba6d1090936928f9fa30 # v4.32.0 + uses: github/codeql-action/init@c10b8064de6f491fea524254123dbe5e09572f13 # v4.35.1 with: languages: ${{ matrix.language }} build-mode: ${{ matrix.build-mode }} @@ -86,6 +86,6 @@ jobs: exit 1 - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@b20883b0cd1f46c72ae0ba6d1090936928f9fa30 # v4.32.0 + uses: github/codeql-action/analyze@c10b8064de6f491fea524254123dbe5e09572f13 # v4.35.1 with: category: "/language:${{matrix.language}}" diff --git a/.github/workflows/pr-title-check.yml b/.github/workflows/pr-title-check.yml new file mode 100644 index 0000000000..2ad023ed1b --- /dev/null +++ b/.github/workflows/pr-title-check.yml @@ -0,0 +1,93 @@ +name: PR Title Check + +on: + pull_request: + types: [opened, edited, synchronize, reopened] + +permissions: + contents: read + pull-requests: read + +jobs: + check: + name: Validate PR title + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + + - uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 + with: + script: | + const { + detectComponents, + hasCoreChanges, + hasDashboardChanges, + hasGitHubActionsChanges, + } = require('./.github/scripts/detect-tags.js'); + + const title = context.payload.pull_request.title; + const author = context.payload.pull_request.user.login; + + // Skip bot PRs (e.g. dependabot) - they have their own title format + if (author === 'dependabot[bot]') { + return; + } + + // Block titles starting with "word:" or "word(scope):" patterns + const commitStylePattern = /^\w+(\(.*?\))?[!]?\s*:/; + if (commitStylePattern.test(title)) { + core.setFailed( + `PR title should not start with a "prefix:" style format.\n` + + `Please use the format: [component] Brief description\n` + ); + return; + } + + // Get changed files to detect tags + const files = await github.paginate(github.rest.pulls.listFiles, { + owner: context.repo.owner, + repo: context.repo.repo, + pull_number: context.issue.number, + }); + const filenames = files.map(f => f.filename); + + // Detect tags from changed files using shared logic + const tags = new Set(); + + for (const comp of detectComponents(filenames)) { + tags.add(comp); + } + if (hasCoreChanges(filenames)) tags.add('core'); + if (hasDashboardChanges(filenames)) tags.add('dashboard'); + if (hasGitHubActionsChanges(filenames)) tags.add('ci'); + + if (tags.size === 0) { + return; + } + + // Check for angle brackets not wrapped in backticks. + // Astro docs MDX treats bare < as JSX component opening tags. + const stripped = title.replace(/`[^`]*`/g, ''); + if (/[<>]/.test(stripped)) { + core.setFailed( + 'PR title contains `<` or `>` not wrapped in backticks.\n' + + 'Astro docs MDX interprets bare `<` as JSX components.\n' + + 'Please wrap angle brackets with backticks, e.g.: [component] Add `` support' + ); + return; + } + + // Check title starts with [tag] prefix + const bracketPattern = /^\[\w+\]/; + if (!bracketPattern.test(title)) { + const suggestion = [...tags].map(c => `[${c}]`).join(''); + // Skip if the suggested prefix would be too long for a readable title + if (suggestion.length > 40) { + return; + } + core.setFailed( + `PR modifies: ${[...tags].join(', ')}\n` + + `Title must start with a [tag] prefix.\n` + + `Suggested: ${suggestion} ` + ); + } diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 479b01ee37..ba6db99b84 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -99,15 +99,15 @@ jobs: python-version: "3.11" - name: Set up Docker Buildx - uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3.12.0 + uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4.0.0 - name: Log in to docker hub - uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3.7.0 + uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0 with: username: ${{ secrets.DOCKER_USER }} password: ${{ secrets.DOCKER_PASSWORD }} - name: Log in to the GitHub container registry - uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3.7.0 + uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0 with: registry: ghcr.io username: ${{ github.actor }} @@ -138,7 +138,7 @@ jobs: # version: ${{ needs.init.outputs.tag }} - name: Upload digests - uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0 + uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 with: name: digests-${{ matrix.platform.arch }} path: /tmp/digests @@ -171,24 +171,24 @@ jobs: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - name: Download digests - uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0 + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 with: pattern: digests-* path: /tmp/digests merge-multiple: true - name: Set up Docker Buildx - uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3.12.0 + uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4.0.0 - name: Log in to docker hub if: matrix.registry == 'dockerhub' - uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3.7.0 + uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0 with: username: ${{ secrets.DOCKER_USER }} password: ${{ secrets.DOCKER_PASSWORD }} - name: Log in to the GitHub container registry if: matrix.registry == 'ghcr' - uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3.7.0 + uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0 with: registry: ghcr.io username: ${{ github.actor }} @@ -221,7 +221,7 @@ jobs: steps: - name: Generate a token id: generate-token - uses: actions/create-github-app-token@29824e69f54612133e76f7eaac726eef6c875baf # v2.2.1 + uses: actions/create-github-app-token@f8d387b68d61c58ab83c6c016672934102569859 # v3.0.0 with: app-id: ${{ secrets.ESPHOME_GITHUB_APP_ID }} private-key: ${{ secrets.ESPHOME_GITHUB_APP_PRIVATE_KEY }} @@ -256,7 +256,7 @@ jobs: steps: - name: Generate a token id: generate-token - uses: actions/create-github-app-token@29824e69f54612133e76f7eaac726eef6c875baf # v2.2.1 + uses: actions/create-github-app-token@f8d387b68d61c58ab83c6c016672934102569859 # v3.0.0 with: app-id: ${{ secrets.ESPHOME_GITHUB_APP_ID }} private-key: ${{ secrets.ESPHOME_GITHUB_APP_PRIVATE_KEY }} @@ -287,7 +287,7 @@ jobs: steps: - name: Generate a token id: generate-token - uses: actions/create-github-app-token@29824e69f54612133e76f7eaac726eef6c875baf # v2.2.1 + uses: actions/create-github-app-token@f8d387b68d61c58ab83c6c016672934102569859 # v3.0.0 with: app-id: ${{ secrets.ESPHOME_GITHUB_APP_ID }} private-key: ${{ secrets.ESPHOME_GITHUB_APP_PRIVATE_KEY }} diff --git a/.github/workflows/stale.yml b/.github/workflows/stale.yml index 7e03e2a5f9..ba5c32e016 100644 --- a/.github/workflows/stale.yml +++ b/.github/workflows/stale.yml @@ -19,7 +19,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Stale - uses: actions/stale@997185467fa4f803885201cee163a9f38240193d # v10.1.1 + uses: actions/stale@b5d41d4e1d5dceea10e7104786b73624c18a190f # v10.2.0 with: debug-only: ${{ github.ref != 'refs/heads/dev' }} # Dry-run when not run on dev branch remove-stale-when-updated: true diff --git a/.github/workflows/sync-device-classes.yml b/.github/workflows/sync-device-classes.yml index b0d966555b..a71e5ef4ca 100644 --- a/.github/workflows/sync-device-classes.yml +++ b/.github/workflows/sync-device-classes.yml @@ -24,7 +24,7 @@ jobs: - name: Setup Python uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 with: - python-version: 3.13 + python-version: "3.14" - name: Install Home Assistant run: | diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index b068673ecf..f8c21aad36 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -11,7 +11,7 @@ ci: repos: - repo: https://github.com/astral-sh/ruff-pre-commit # Ruff version. - rev: v0.14.14 + rev: v0.15.9 hooks: # Run the linter. - id: ruff @@ -37,7 +37,7 @@ repos: - id: end-of-file-fixer - id: trailing-whitespace - repo: https://github.com/asottile/pyupgrade - rev: v3.20.0 + rev: v3.21.2 hooks: - id: pyupgrade args: [--py311-plus] @@ -65,3 +65,7 @@ repos: files: ^(\.clang-tidy|platformio\.ini|requirements_dev\.txt)$ pass_filenames: false additional_dependencies: [] + - id: ci-custom + name: ci-custom + entry: python3 script/run-in-env.py script/ci-custom.py + language: system diff --git a/CODEOWNERS b/CODEOWNERS index 00a22fed7c..c466204b66 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -54,6 +54,8 @@ esphome/components/atm90e32/* @circuitsetup @descipher esphome/components/audio/* @kahrendt esphome/components/audio_adc/* @kbx81 esphome/components/audio_dac/* @kbx81 +esphome/components/audio_file/* @kahrendt +esphome/components/audio_file/media_source/* @kahrendt esphome/components/axs15231/* @clydebarrow esphome/components/b_parasite/* @rbaron esphome/components/ballu/* @bazuchan @@ -90,6 +92,7 @@ esphome/components/bmp3xx_i2c/* @latonita esphome/components/bmp3xx_spi/* @latonita esphome/components/bmp581_base/* @danielkent-net @kahrendt esphome/components/bmp581_i2c/* @danielkent-net @kahrendt +esphome/components/bmp581_spi/* @danielkent-net @kahrendt esphome/components/bp1658cj/* @Cossid esphome/components/bp5758d/* @Cossid esphome/components/bthome_mithermometer/* @nagyrobi @@ -104,6 +107,7 @@ esphome/components/cc1101/* @gabest11 @lygris esphome/components/ccs811/* @habbie esphome/components/cd74hc4067/* @asoehlke esphome/components/ch422g/* @clydebarrow @jesterret +esphome/components/ch423/* @dwmw2 esphome/components/chsc6x/* @kkosik20 esphome/components/climate/* @esphome/core esphome/components/climate_ir/* @glmnet @@ -129,14 +133,16 @@ esphome/components/dashboard_import/* @esphome/core esphome/components/datetime/* @jesserockz @rfdarter esphome/components/debug/* @esphome/core esphome/components/delonghi/* @grob6000 +esphome/components/dew_point/* @CFlix esphome/components/dfplayer/* @glmnet esphome/components/dfrobot_sen0395/* @niklasweber esphome/components/dht/* @OttoWinter esphome/components/display_menu_base/* @numo68 +esphome/components/dlms_meter/* @SimonFischer04 esphome/components/dps310/* @kbx81 esphome/components/ds1307/* @badbadc0ffee esphome/components/ds2484/* @mrk-its -esphome/components/dsmr/* @glmnet @PolarGoose @zuidwijk +esphome/components/dsmr/* @glmnet @PolarGoose esphome/components/duty_time/* @dudanov esphome/components/ee895/* @Stock-M esphome/components/ektf2232/touchscreen/* @jesserockz @@ -211,6 +217,8 @@ esphome/components/hbridge/light/* @DotNetDann esphome/components/hbridge/switch/* @dwmw2 esphome/components/hc8/* @omartijn esphome/components/hdc2010/* @optimusprimespace @ssieb +esphome/components/hdc2080/* @G-Pereira @jesserockz +esphome/components/hdc302x/* @joshuasing esphome/components/he60r/* @clydebarrow esphome/components/heatpumpir/* @rob-deutsch esphome/components/hitachi_ac424/* @sourabhjaiswal @@ -238,7 +246,6 @@ esphome/components/hyt271/* @Philippe12 esphome/components/i2c/* @esphome/core esphome/components/i2c_device/* @gabest11 esphome/components/i2s_audio/* @jesserockz -esphome/components/i2s_audio/media_player/* @jesserockz esphome/components/i2s_audio/microphone/* @jesserockz esphome/components/i2s_audio/speaker/* @jesserockz @kahrendt esphome/components/iaqcore/* @yozik04 @@ -313,6 +320,7 @@ esphome/components/mcp9808/* @k7hpn esphome/components/md5/* @esphome/core esphome/components/mdns/* @esphome/core esphome/components/media_player/* @jesserockz +esphome/components/media_source/* @kahrendt esphome/components/micro_wake_word/* @jesserockz @kahrendt esphome/components/micronova/* @edenhaus @jorre05 esphome/components/microphone/* @jesserockz @kahrendt @@ -323,6 +331,7 @@ esphome/components/mipi_dsi/* @clydebarrow esphome/components/mipi_rgb/* @clydebarrow esphome/components/mipi_spi/* @clydebarrow esphome/components/mitsubishi/* @RubyBailey +esphome/components/mitsubishi_cn105/* @crnjan esphome/components/mixer/speaker/* @kahrendt esphome/components/mlx90393/* @functionpointer esphome/components/mlx90614/* @jesserockz @@ -404,11 +413,13 @@ esphome/components/restart/* @esphome/core esphome/components/rf_bridge/* @jesserockz esphome/components/rgbct/* @jesserockz esphome/components/rp2040/* @jesserockz +esphome/components/rp2040_ble/* @bdraco esphome/components/rp2040_pio_led_strip/* @Papa-DMan esphome/components/rp2040_pwm/* @jesserockz esphome/components/rpi_dpi_rgb/* @clydebarrow esphome/components/rtl87xx/* @kuba2k2 -esphome/components/rtttl/* @glmnet +esphome/components/rtttl/* @glmnet @ximex +esphome/components/runtime_image/* @clydebarrow @guillempages @kahrendt esphome/components/runtime_stats/* @bdraco esphome/components/rx8130/* @beormund esphome/components/safe_mode/* @jsuanet @kbx81 @paulmonigatti @@ -425,8 +436,10 @@ esphome/components/select/* @esphome/core esphome/components/sen0321/* @notjj esphome/components/sen21231/* @shreyaskarnik esphome/components/sen5x/* @martgras +esphome/components/sen6x/* @martgras @mebner86 @mikelawrence @tuct esphome/components/sensirion_common/* @martgras esphome/components/sensor/* @esphome/core +esphome/components/serial_proxy/* @kbx81 esphome/components/sfa30/* @ghsensdev esphome/components/sgp40/* @SenexCrenshaw esphome/components/sgp4x/* @martgras @SenexCrenshaw @@ -447,8 +460,12 @@ esphome/components/sn74hc165/* @jesserockz esphome/components/socket/* @esphome/core esphome/components/sonoff_d1/* @anatoly-savchenkov esphome/components/sound_level/* @kahrendt +esphome/components/spa06_base/* @danielkent-net +esphome/components/spa06_i2c/* @danielkent-net +esphome/components/spa06_spi/* @danielkent-net esphome/components/speaker/* @jesserockz @kahrendt esphome/components/speaker/media_player/* @kahrendt @synesthesiam +esphome/components/speaker_source/* @kahrendt esphome/components/spi/* @clydebarrow @esphome/core esphome/components/spi_device/* @clydebarrow esphome/components/spi_led_strip/* @clydebarrow @@ -530,7 +547,7 @@ esphome/components/uart/packet_transport/* @clydebarrow esphome/components/udp/* @clydebarrow esphome/components/ufire_ec/* @pvizeli esphome/components/ufire_ise/* @pvizeli -esphome/components/ultrasonic/* @OttoWinter +esphome/components/ultrasonic/* @ssieb @swoboda1337 esphome/components/update/* @jesserockz esphome/components/uponor_smatrix/* @kroimon esphome/components/usb_cdc_acm/* @kbx81 @@ -579,6 +596,7 @@ esphome/components/xl9535/* @mreditor97 esphome/components/xpt2046/touchscreen/* @nielsnl68 @numo68 esphome/components/xxtea/* @clydebarrow esphome/components/zephyr/* @tomaszduda23 +esphome/components/zephyr_mcumgr/ota/* @tomaszduda23 esphome/components/zhlt01/* @cfeenstra1024 esphome/components/zigbee/* @tomaszduda23 esphome/components/zio_ultrasonic/* @kahrendt diff --git a/Doxyfile b/Doxyfile index efc2f464e3..cfdb74bd19 100644 --- a/Doxyfile +++ b/Doxyfile @@ -48,7 +48,7 @@ PROJECT_NAME = ESPHome # could be handy for archiving the generated documentation or if some version # control system is used. -PROJECT_NUMBER = 2026.2.0-dev +PROJECT_NUMBER = 2026.4.0-dev # Using the PROJECT_BRIEF tag one can provide an optional one line description # for a project that appears at the top of each page and should give viewer a diff --git a/README.md b/README.md index b8ce8d091d..16497ee0be 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# ESPHome [![Discord Chat](https://img.shields.io/discord/429907082951524364.svg)](https://discord.gg/KhAMKrd) [![GitHub release](https://img.shields.io/github/release/esphome/esphome.svg)](https://GitHub.com/esphome/esphome/releases/) +# ESPHome [![Discord Chat](https://img.shields.io/discord/429907082951524364.svg)](https://discord.gg/KhAMKrd) [![GitHub release](https://img.shields.io/github/release/esphome/esphome.svg)](https://GitHub.com/esphome/esphome/releases/) [![CodSpeed](https://img.shields.io/endpoint?url=https://codspeed.io/badge.json)](https://codspeed.io/esphome/esphome) diff --git a/docker/Dockerfile b/docker/Dockerfile index 348a503bc8..540d28be7f 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -9,7 +9,8 @@ FROM ghcr.io/esphome/docker-base:${BUILD_OS}-ha-addon-${BUILD_BASE_VERSION} AS b ARG BUILD_TYPE FROM base-source-${BUILD_TYPE} AS base -RUN git config --system --add safe.directory "*" +RUN git config --system --add safe.directory "*" \ + && git config --system advice.detachedHead false # Install build tools for Python packages that require compilation # (e.g., ruamel.yaml.clibz used by ESP-IDF's idf-component-manager) @@ -23,7 +24,7 @@ RUN if command -v apk > /dev/null; then \ ENV PIP_DISABLE_PIP_VERSION_CHECK=1 -RUN pip install --no-cache-dir -U pip uv==0.6.14 +RUN pip install --no-cache-dir -U pip uv==0.10.1 COPY requirements.txt / diff --git a/esphome/__main__.py b/esphome/__main__.py index 55297e8d9b..87abd7f796 100644 --- a/esphome/__main__.py +++ b/esphome/__main__.py @@ -1,6 +1,7 @@ # PYTHON_ARGCOMPLETE_OK import argparse from collections.abc import Callable +from contextlib import suppress from datetime import datetime import functools import getpass @@ -9,6 +10,8 @@ import logging import os from pathlib import Path import re +import shutil +import subprocess import sys import time from typing import Protocol @@ -23,6 +26,7 @@ import esphome.codegen as cg from esphome.config import iter_component_configs, read_config, strip_default_ids from esphome.const import ( ALLOWED_NAME_CHARS, + ARGUMENT_HELP_DEVICE, CONF_API, CONF_BAUD_RATE, CONF_BROKER, @@ -43,7 +47,9 @@ from esphome.const import ( CONF_SUBSTITUTIONS, CONF_TOPIC, ENV_NOGITIGNORE, + KEY_CORE, KEY_NATIVE_IDF, + KEY_TARGET_PLATFORM, PLATFORM_ESP32, PLATFORM_ESP8266, PLATFORM_RP2040, @@ -55,7 +61,11 @@ from esphome.helpers import get_bool_env, indent, is_ip_address from esphome.log import AnsiFore, color, setup_log from esphome.types import ConfigType from esphome.util import ( + PICOTOOL_PACKAGE, + detect_rp2040_bootsel, + get_picotool_path, get_serial_ports, + is_picotool_usb_permission_error, list_yaml_files, run_external_command, run_external_process, @@ -64,9 +74,26 @@ from esphome.util import ( _LOGGER = logging.getLogger(__name__) +ESPHOME_COMMAND = [sys.executable, "-m", "esphome"] + # Maximum buffer size for serial log reading to prevent unbounded memory growth SERIAL_BUFFER_MAX_SIZE = 65536 +_RP2040_BOOTSEL_INSTRUCTIONS = ( + "To enter BOOTSEL mode:\n" + " 1. Unplug the device\n" + " 2. Hold the BOOT/BOOTSEL button\n" + " 3. Plug in the USB cable while holding the button\n" + " 4. Release the button - the device should appear as a USB drive (RPI-RP2)\n" + "Then run the upload command again." +) + +_RP2040_UDEV_HINT = ( + "You may need to add a udev rule for RP2040 devices. " + "See: https://github.com/raspberrypi/picotool" + "/blob/master/udev/60-picotool.rules" +) + # Special non-component keys that appear in configs _NON_COMPONENT_KEYS = frozenset( { @@ -162,6 +189,7 @@ class PortType(StrEnum): NETWORK = "NETWORK" MQTT = "MQTT" MQTTIP = "MQTTIP" + BOOTSEL = "BOOTSEL" # Magic MQTT port types that require special handling @@ -240,6 +268,19 @@ def choose_upload_log_host( (f"{port.path} ({port.description})", port.path) for port in get_serial_ports() ] + # Add RP2040 BOOTSEL device option when uploading + bootsel_permission_error = False + if ( + purpose == Purpose.UPLOADING + and CORE.data.get(KEY_CORE, {}).get(KEY_TARGET_PLATFORM) == PLATFORM_RP2040 + and (picotool := _find_picotool()) is not None + ): + bootsel = detect_rp2040_bootsel(picotool) + if bootsel.device_count > 0: + options.append(("RP2040 BOOTSEL (via picotool)", "BOOTSEL")) + elif bootsel.permission_error: + bootsel_permission_error = True + if purpose == Purpose.LOGGING: if has_mqtt_logging(): mqtt_config = CORE.config[CONF_MQTT] @@ -257,6 +298,25 @@ def choose_upload_log_host( if has_mqtt_ip_lookup(): options.append(("Over The Air (MQTT IP lookup)", "MQTTIP")) + # Show helpful BOOTSEL instructions for RP2040 when no BOOTSEL device is found + if ( + purpose == Purpose.UPLOADING + and CORE.data.get(KEY_CORE, {}).get(KEY_TARGET_PLATFORM) == PLATFORM_RP2040 + and not any(get_port_type(opt[1]) == PortType.BOOTSEL for opt in options) + ): + if bootsel_permission_error: + _LOGGER.warning( + "An RP2040 device in BOOTSEL mode was detected but could " + "not be accessed due to USB permissions." + ) + if sys.platform.startswith("linux"): + _LOGGER.warning(_RP2040_UDEV_HINT) + if not options: + raise EsphomeError( + f"No RP2040 device found. {_RP2040_BOOTSEL_INSTRUCTIONS}" + ) + _LOGGER.info("Tip: %s", _RP2040_BOOTSEL_INSTRUCTIONS) + if check_default is not None and check_default in [opt[1] for opt in options]: return [check_default] return [choose_prompt(options, purpose=purpose)] @@ -294,8 +354,13 @@ def has_api() -> bool: def has_ota() -> bool: - """Check if OTA is available.""" - return CONF_OTA in CORE.config + """Check if OTA upload is available (requires platform: esphome).""" + if CONF_OTA not in CORE.config: + return False + return any( + ota_item.get(CONF_PLATFORM) == CONF_ESPHOME + for ota_item in CORE.config[CONF_OTA] + ) def has_mqtt_ip_lookup() -> bool: @@ -398,10 +463,13 @@ def get_port_type(port: str) -> PortType: Returns: PortType.SERIAL for serial ports (/dev/ttyUSB0, COM1, etc.) + PortType.BOOTSEL for RP2040 BOOTSEL upload via picotool PortType.MQTT for MQTT logging PortType.MQTTIP for MQTT IP lookup PortType.NETWORK for IP addresses, hostnames, or mDNS names """ + if port == "BOOTSEL": + return PortType.BOOTSEL if port.startswith("/") or port.startswith("COM"): return PortType.SERIAL if port == "MQTT": @@ -426,6 +494,14 @@ def run_miniterm(config: ConfigType, port: str, args) -> int: return 1 _LOGGER.info("Starting log output from %s with baud rate %s", port, baud_rate) + process_stacktrace = None + + try: + module = importlib.import_module("esphome.components." + CORE.target_platform) + process_stacktrace = getattr(module, "process_stacktrace") + except AttributeError: + pass + backtrace_state = False ser = serial.Serial() ser.baudrate = baud_rate @@ -467,9 +543,14 @@ def run_miniterm(config: ConfigType, port: str, args) -> int: ) safe_print(parser.parse_line(line, time_str)) - backtrace_state = platformio_api.process_stacktrace( - config, line, backtrace_state=backtrace_state - ) + if process_stacktrace: + backtrace_state = process_stacktrace( + config, line, backtrace_state + ) + else: + backtrace_state = platformio_api.process_stacktrace( + config, line, backtrace_state=backtrace_state + ) except serial.SerialException: _LOGGER.error("Serial port closed!") return 0 @@ -609,6 +690,47 @@ def _check_and_emit_build_info() -> None: ) +def _get_configured_xtal_freq() -> int | None: + """Read the configured crystal frequency from the sdkconfig file.""" + sdkconfig_path = CORE.relative_build_path(f"sdkconfig.{CORE.name}") + if not sdkconfig_path.is_file(): + return None + with suppress(OSError, ValueError): + content = sdkconfig_path.read_text() + for line in content.splitlines(): + if line.startswith("CONFIG_XTAL_FREQ="): + return int(line.split("=", 1)[1]) + return None + + +def _make_crystal_freq_callback( + configured_freq: int, +) -> Callable[[str], str | None]: + """Create a callback that checks esptool crystal frequency output.""" + crystal_re = re.compile(r"Crystal frequency:\s+(\d+)\s*MHz") + + def check_crystal_line(line: str) -> str | None: + if not (match := crystal_re.search(line)): + return None + detected = int(match.group(1)) + if detected == configured_freq: + return None + return ( + f"\n\033[33mWARNING: Crystal frequency mismatch! " + f"Device reports {detected}MHz but firmware is configured " + f"for {configured_freq}MHz.\n" + f"UART logging and other clock-dependent features will not " + f"work correctly.\n" + f"Set the correct crystal frequency with sdkconfig_options:\n" + f" esp32:\n" + f" framework:\n" + f" sdkconfig_options:\n" + f" CONFIG_XTAL_FREQ_{detected}: 'y'\033[0m\n\n" + ) + + return check_crystal_line + + def upload_using_esptool( config: ConfigType, port: str, file: str, speed: int ) -> str | int: @@ -637,6 +759,14 @@ def upload_using_esptool( mcu = get_esp32_variant().lower() + line_callbacks: list[Callable[[str], str | None]] = [] + if ( + CORE.is_esp32 + and file is None + and (configured_freq := _get_configured_xtal_freq()) is not None + ): + line_callbacks.append(_make_crystal_freq_callback(configured_freq)) + def run_esptool(baud_rate): cmd = [ "esptool", @@ -661,9 +791,13 @@ def upload_using_esptool( if os.environ.get("ESPHOME_USE_SUBPROCESS") is None: import esptool - return run_external_command(esptool.main, *cmd) # pylint: disable=no-member + return run_external_command( + esptool.main, # pylint: disable=no-member + *cmd, + line_callbacks=line_callbacks, + ) - return run_external_process(*cmd) + return run_external_process(*cmd, line_callbacks=line_callbacks) rc = run_esptool(first_baudrate) if rc == 0 or first_baudrate == 115200: @@ -676,15 +810,140 @@ def upload_using_esptool( return run_esptool(115200) -def upload_using_platformio(config: ConfigType, port: str): +def upload_using_platformio(config: ConfigType, port: str) -> int: from esphome import platformio_api + # RP2040 platform-raspberrypi build recipe expects firmware.bin.signed for + # the upload target, but 'nobuild' skips the build phase that creates it. + # Create it here so the upload doesn't fail. + if CORE.data.get(KEY_CORE, {}).get(KEY_TARGET_PLATFORM) == PLATFORM_RP2040: + idedata = platformio_api.get_idedata(config) + build_dir = Path(idedata.firmware_elf_path).parent + firmware_bin = build_dir / "firmware.bin" + signed_bin = build_dir / "firmware.bin.signed" + if firmware_bin.is_file() and not signed_bin.is_file(): + shutil.copy2(firmware_bin, signed_bin) + upload_args = ["-t", "upload", "-t", "nobuild"] if port is not None: upload_args += ["--upload-port", port] return platformio_api.run_platformio_cli_run(config, CORE.verbose, *upload_args) +def _find_picotool() -> Path | None: + """Find the picotool binary from PlatformIO packages.""" + from esphome import platformio_api + + try: + idedata = platformio_api.get_idedata(CORE.config) + except Exception: # noqa: BLE001 # pylint: disable=broad-except + return None + return get_picotool_path(idedata.cc_path) + + +def upload_using_picotool(config: ConfigType) -> int: + """Upload firmware to RP2040 in BOOTSEL mode using picotool. + + Uses picotool to load the ELF firmware directly via USB, avoiding + the mass storage copy approach that causes "disk not ejected properly" + warnings on macOS. + """ + from esphome import platformio_api + + idedata = platformio_api.get_idedata(config) + firmware_elf = Path(idedata.firmware_elf_path) + + if not firmware_elf.is_file(): + _LOGGER.error( + "Firmware ELF file not found at %s. " + "Make sure the project has been compiled first.", + firmware_elf, + ) + return 1 + + picotool = get_picotool_path(idedata.cc_path) + if picotool is None: + _LOGGER.error( + "picotool not found. Ensure the RP2040 PlatformIO platform " + "is installed (%s).", + PICOTOOL_PACKAGE, + ) + return 1 + + _LOGGER.info("Uploading firmware to RP2040 via picotool...") + try: + # Don't capture stdout — let picotool write directly to the terminal + # so progress bars display in real-time with \r updates. + # Capture stderr only so we can detect permission errors. + result = subprocess.run( + [str(picotool), "load", "-v", "-x", str(firmware_elf)], + stderr=subprocess.PIPE, + timeout=60, + check=False, + ) + except subprocess.TimeoutExpired: + _LOGGER.error("picotool upload timed out after 60 seconds.") + return 1 + except OSError as err: + _LOGGER.error("Failed to run picotool: %s", err) + return 1 + + if result.returncode != 0: + stderr = result.stderr.decode("utf-8", errors="replace").strip() + if stderr: + for line in stderr.splitlines(): + safe_print(line) + if is_picotool_usb_permission_error(stderr): + msg = "Permission denied accessing USB device." + if sys.platform.startswith("linux"): + msg += f" {_RP2040_UDEV_HINT}" + _LOGGER.error(msg) + else: + _LOGGER.error("picotool upload failed (exit code %d).", result.returncode) + return 1 + + return 0 + + +def _wait_for_serial_port( + port: str | None = None, + timeout: float = 30.0, + known_ports: set[str] | None = None, +) -> None: + """Wait for a serial port to appear, e.g. after a device reboot. + + USB-CDC devices disappear briefly after flashing while the device + reboots and re-enumerates on the USB bus. + + If port is given, wait for that specific path. If known_ports is + given, wait for a new port that wasn't in the set. Otherwise wait + for any serial port to appear. + """ + + def _port_found() -> bool: + if port is not None: + if os.name == "posix": + return os.path.exists(port) + return any(p.path == port for p in get_serial_ports()) + ports = get_serial_ports() + if known_ports is not None: + return any(p.path not in known_ports for p in ports) + return bool(ports) + + if _port_found(): + return + if port is not None: + _LOGGER.info("Waiting for %s to come online...", port) + else: + _LOGGER.info("Waiting for device to reboot...") + start = time.monotonic() + while time.monotonic() - start < timeout: + time.sleep(0.05) + if _port_found(): + time.sleep(0.05) + return + + def check_permissions(port: str): if os.name == "posix" and get_port_type(port) == PortType.SERIAL: # Check if we can open selected serial port @@ -714,7 +973,15 @@ def upload_program( except AttributeError: pass - if get_port_type(host) == PortType.SERIAL: + port_type = get_port_type(host) + + if port_type == PortType.BOOTSEL: + exit_code = upload_using_picotool(config) + # Return None for device - BOOTSEL can't be used for logging, + # so command_run will show the interactive chooser for log source + return exit_code, None + + if port_type == PortType.SERIAL: check_permissions(host) exit_code = 1 @@ -768,6 +1035,7 @@ def show_logs(config: ConfigType, args: ArgsProtocol, devices: list[str]) -> int port_type = get_port_type(port) if port_type == PortType.SERIAL: + _wait_for_serial_port(port) check_permissions(port) return run_miniterm(config, port, args) @@ -778,7 +1046,11 @@ def show_logs(config: ConfigType, args: ArgsProtocol, devices: list[str]) -> int ): from esphome.components.api.client import run_logs - return run_logs(config, network_devices) + return run_logs( + config, + network_devices, + subscribe_states=not getattr(args, "no_states", False), + ) if port_type in (PortType.NETWORK, PortType.MQTT) and has_mqtt_logging(): from esphome import mqtt @@ -906,6 +1178,9 @@ def command_run(args: ArgsProtocol, config: ConfigType) -> int | None: purpose=Purpose.UPLOADING, ) + # Snapshot current serial ports before upload so we can detect new ones + pre_upload_ports = {p.path for p in get_serial_ports()} + exit_code, successful_device = upload_program(config, args, devices) if exit_code == 0: _LOGGER.info("Successfully uploaded program.") @@ -916,6 +1191,19 @@ def command_run(args: ArgsProtocol, config: ConfigType) -> int | None: if args.no_logs: return 0 + # After BOOTSEL upload, wait for a new serial port to appear + # so it shows up in the log chooser + if ( + successful_device is None + and CORE.data.get(KEY_CORE, {}).get(KEY_TARGET_PLATFORM) == PLATFORM_RP2040 + ): + _wait_for_serial_port(known_ports=pre_upload_ports) + # If exactly one new serial port appeared, use it directly + serial_ports = get_serial_ports() + new_ports = [p for p in serial_ports if p.path not in pre_upload_ports] + if len(new_ports) == 1: + successful_device = new_ports[0].path + # For logs, prefer the device we successfully uploaded to devices = choose_upload_log_host( default=successful_device, @@ -939,12 +1227,6 @@ def command_clean_all(args: ArgsProtocol) -> int | None: return 0 -def command_mqtt_fingerprint(args: ArgsProtocol, config: ConfigType) -> int | None: - from esphome import mqtt - - return mqtt.get_fingerprint(config) - - def command_version(args: ArgsProtocol) -> int | None: safe_print(f"Version: {const.__version__}") return 0 @@ -1031,9 +1313,8 @@ def command_update_all(args: ArgsProtocol) -> int | None: files = list_yaml_files(args.configuration) def build_command(f): - if CORE.dashboard: - return ["esphome", "--dashboard", "run", f, "--no-logs", "--device", "OTA"] - return ["esphome", "run", f, "--no-logs", "--device", "OTA"] + dashboard = ["--dashboard"] if CORE.dashboard else [] + return [*ESPHOME_COMMAND, *dashboard, "run", f, "--no-logs", "--device", "OTA"] return run_multiple_configs(files, build_command) @@ -1182,7 +1463,7 @@ def command_rename(args: ArgsProtocol, config: ConfigType) -> int | None: new_path.write_text(new_raw, encoding="utf-8") - rc = run_external_process("esphome", "config", str(new_path)) + rc = run_external_process(*ESPHOME_COMMAND, "config", str(new_path)) if rc != 0: print(color(AnsiFore.BOLD_RED, "Rename failed. Reverting changes.")) new_path.unlink() @@ -1200,7 +1481,7 @@ def command_rename(args: ArgsProtocol, config: ConfigType) -> int | None: cli_args.insert(0, "--dashboard") try: - rc = run_external_process("esphome", *cli_args) + rc = run_external_process(*ESPHOME_COMMAND, *cli_args) except KeyboardInterrupt: rc = 1 if rc != 0: @@ -1232,7 +1513,6 @@ POST_CONFIG_ACTIONS = { "run": command_run, "clean": command_clean, "clean-mqtt": command_clean_mqtt, - "mqtt-fingerprint": command_mqtt_fingerprint, "idedata": command_idedata, "rename": command_rename, "discover": command_discover, @@ -1356,7 +1636,7 @@ def parse_args(argv): parser_upload.add_argument( "--device", action="append", - help="Manually specify the serial port/address to use, for example /dev/ttyUSB0. Can be specified multiple times for fallback addresses.", + help=ARGUMENT_HELP_DEVICE, ) parser_upload.add_argument( "--upload_speed", @@ -1379,7 +1659,7 @@ def parse_args(argv): parser_logs.add_argument( "--device", action="append", - help="Manually specify the serial port/address to use, for example /dev/ttyUSB0. Can be specified multiple times for fallback addresses.", + help=ARGUMENT_HELP_DEVICE, ) parser_logs.add_argument( "--reset", @@ -1388,6 +1668,11 @@ def parse_args(argv): help="Reset the device before starting serial logs.", default=os.getenv("ESPHOME_SERIAL_LOGGING_RESET"), ) + parser_logs.add_argument( + "--no-states", + action="store_true", + help="Do not show entity state changes in log output.", + ) parser_discover = subparsers.add_parser( "discover", @@ -1409,7 +1694,7 @@ def parse_args(argv): parser_run.add_argument( "--device", action="append", - help="Manually specify the serial port/address to use, for example /dev/ttyUSB0. Can be specified multiple times for fallback addresses.", + help=ARGUMENT_HELP_DEVICE, ) parser_run.add_argument( "--upload_speed", @@ -1446,13 +1731,6 @@ def parse_args(argv): ) parser_wizard.add_argument("configuration", help="Your YAML configuration file.") - parser_fingerprint = subparsers.add_parser( - "mqtt-fingerprint", help="Get the SSL fingerprint from a MQTT broker." - ) - parser_fingerprint.add_argument( - "configuration", help="Your YAML configuration file(s).", nargs="+" - ) - subparsers.add_parser("version", help="Print the ESPHome version and exit.") parser_clean = subparsers.add_parser( @@ -1605,7 +1883,7 @@ def run_esphome(argv): # argv[0] is the program path, skip it since we prefix with "esphome" def build_command(f): return ( - ["esphome"] + [*ESPHOME_COMMAND] + [arg for arg in argv[1:] if arg not in args.configuration] + [str(f)] ) diff --git a/esphome/analyze_memory/__init__.py b/esphome/analyze_memory/__init__.py index 63ef0e74ed..f56d720ec2 100644 --- a/esphome/analyze_memory/__init__.py +++ b/esphome/analyze_memory/__init__.py @@ -1,6 +1,6 @@ """Memory usage analyzer for ESPHome compiled binaries.""" -from collections import defaultdict +from collections import Counter, defaultdict from dataclasses import dataclass, field import logging from pathlib import Path @@ -12,7 +12,6 @@ from .const import ( CORE_SUBCATEGORY_PATTERNS, DEMANGLED_PATTERNS, ESPHOME_COMPONENT_PATTERN, - SECTION_TO_ATTR, SYMBOL_PATTERNS, ) from .demangle import batch_demangle @@ -41,12 +40,26 @@ _READELF_SECTION_PATTERN = re.compile( r"\s*\[\s*\d+\]\s+([\.\w]+)\s+\w+\s+[\da-fA-F]+\s+[\da-fA-F]+\s+([\da-fA-F]+)" ) +# Regex for extracting call targets from objdump disassembly +# Matches direct call instructions across architectures: +# Xtensa: call0/call4/call8/call12/callx0/callx4/callx8/callx12 +# ARM: bl/blx +# Captures the mangled symbol name inside angle brackets. +_CALL_TARGET_PATTERN = re.compile( + r"\t(?:call(?:0|4|8|12)|callx(?:0|4|8|12)|blx?)\s+[\da-fA-F]+ <([^>]+)>" +) + # Component category prefixes _COMPONENT_PREFIX_ESPHOME = "[esphome]" _COMPONENT_PREFIX_EXTERNAL = "[external]" +_COMPONENT_PREFIX_LIB = "[lib]" _COMPONENT_CORE = f"{_COMPONENT_PREFIX_ESPHOME}core" _COMPONENT_API = f"{_COMPONENT_PREFIX_ESPHOME}api" +# Placement new storage suffix (generated by codegen Pvariable) +_PSTORAGE_SUFFIX = "__pstorage" + + # C++ namespace prefixes _NAMESPACE_ESPHOME = "esphome::" _NAMESPACE_STD = "std::" @@ -57,6 +70,16 @@ SymbolInfoType = tuple[str, int, str] # RAM sections - symbols in these sections consume RAM RAM_SECTIONS = frozenset([".data", ".bss"]) +# nm symbol types for global/weak defined symbols (used for library symbol mapping) +# Only global (uppercase) and weak symbols are safe to use - local symbols (lowercase) +# can have name collisions across compilation units +_NM_DEFINED_GLOBAL_TYPES = frozenset({"T", "D", "B", "R", "W", "V"}) + +# Pattern matching compiler-generated local names that can collide across compilation +# units (e.g., packet$19, buf$20, flag$5261). These are unsafe for name-based lookup. +# Does NOT match mangled C++ names with optimization suffixes (e.g., func$isra$0). +_COMPILER_LOCAL_PATTERN = re.compile(r"^[a-zA-Z_]\w*\$\d+$") + @dataclass class MemorySection: @@ -91,6 +114,17 @@ class ComponentMemory: bss_size: int = 0 # Uninitialized data (ram only) symbol_count: int = 0 + def add_section_size(self, section_name: str, size: int) -> None: + """Add size to the appropriate attribute for a section.""" + if section_name == ".text": + self.text_size += size + elif section_name == ".rodata": + self.rodata_size += size + elif section_name == ".data": + self.data_size += size + elif section_name == ".bss": + self.bss_size += size + @property def flash_total(self) -> int: """Total flash usage (text + rodata + data).""" @@ -167,13 +201,31 @@ class MemoryAnalyzer: self._elf_symbol_names: set[str] = set() # SDK symbols not in ELF (static/local symbols from closed-source libs) self._sdk_symbols: list[SDKSymbol] = [] + # CSWTCH symbols: list of (name, size, source_file, component) + self._cswtch_symbols: list[tuple[str, int, str, str]] = [] + # Library symbol mapping: symbol_name -> library_name + self._lib_symbol_map: dict[str, str] = {} + # Source file symbol mapping: symbol_name -> component_name + # Used for extern "C" and other symbols without C++ namespace + self._source_symbol_map: dict[str, str] = {} + # Library dir to name mapping: "lib641" -> "espsoftwareserial", + # "espressif__mdns" -> "mdns" + self._lib_hash_to_name: dict[str, str] = {} + # Heuristic category to library redirect: "mdns_lib" -> "[lib]mdns" + self._heuristic_to_lib: dict[str, str] = {} + # Function call counts: mangled_name -> call_count + self._function_call_counts: Counter[str] = Counter() def analyze(self) -> dict[str, ComponentMemory]: """Analyze the ELF file and return component memory usage.""" self._parse_sections() self._parse_symbols() + self._scan_libraries() + self._scan_source_symbols() self._categorize_symbols() + self._analyze_cswtch_symbols() self._analyze_sdk_libraries() + self._analyze_function_calls() return dict(self.components) def _parse_sections(self) -> None: @@ -255,8 +307,7 @@ class MemoryAnalyzer: comp_mem.symbol_count += 1 # Update the appropriate size attribute based on section - if attr_name := SECTION_TO_ATTR.get(section_name): - setattr(comp_mem, attr_name, getattr(comp_mem, attr_name) + size) + comp_mem.add_section_size(section_name, size) # Track uncategorized symbols if component == "other" and size > 0: @@ -285,6 +336,13 @@ class MemoryAnalyzer: # Demangle C++ names if needed demangled = self._demangle_symbol(symbol_name) + # Check for placement new storage symbols (generated by codegen) + # Format: {component}__{id}__pstorage + if demangled.endswith(_PSTORAGE_SUFFIX) and ( + component := self._match_pstorage_component(demangled) + ): + return component + # Check for special component classes first (before namespace pattern) # This handles cases like esphome::ESPHomeOTAComponent which should map to ota if _NAMESPACE_ESPHOME in demangled: @@ -316,15 +374,24 @@ class MemoryAnalyzer: # If no component match found, it's core return _COMPONENT_CORE + # Check library symbol map (more accurate than heuristic patterns) + if lib_name := self._lib_symbol_map.get(symbol_name): + return f"{_COMPONENT_PREFIX_LIB}{lib_name}" + + # Check source file mapping (catches extern "C" functions in ESPHome sources) + # Must be before heuristic patterns since source attribution is authoritative + if component := self._source_symbol_map.get(symbol_name): + return component + # Check against symbol patterns for component, patterns in SYMBOL_PATTERNS.items(): if any(pattern in symbol_name for pattern in patterns): - return component + return self._heuristic_to_lib.get(component, component) # Check against demangled patterns for component, patterns in DEMANGLED_PATTERNS.items(): if any(pattern in demangled for pattern in patterns): - return component + return self._heuristic_to_lib.get(component, component) # Special cases that need more complex logic @@ -343,14 +410,33 @@ class MemoryAnalyzer: # Track uncategorized symbols for analysis return "other" + def _match_pstorage_component(self, symbol_name: str) -> str | None: + """Match a __pstorage symbol to its ESPHome component. + + Symbol format: {component}__{id}__pstorage + The component namespace is embedded by codegen before the double underscore. + """ + prefix = symbol_name[: -len(_PSTORAGE_SUFFIX)] + # Extract component namespace before the first double underscore + dunder_pos = prefix.find("__") + if dunder_pos == -1: + return None + component_name = prefix[:dunder_pos] + if component_name in get_esphome_components(): + return f"{_COMPONENT_PREFIX_ESPHOME}{component_name}" + if component_name in self.external_components: + return f"{_COMPONENT_PREFIX_EXTERNAL}{component_name}" + return None + def _batch_demangle_symbols(self, symbols: list[str]) -> None: """Batch demangle C++ symbol names for efficiency.""" if not symbols: return _LOGGER.info("Demangling %d symbols", len(symbols)) - self._demangle_cache = batch_demangle(symbols, objdump_path=self.objdump_path) - _LOGGER.info("Successfully demangled %d symbols", len(self._demangle_cache)) + demangled = batch_demangle(symbols, objdump_path=self.objdump_path) + self._demangle_cache.update(demangled) + _LOGGER.info("Successfully demangled %d symbols", len(demangled)) def _demangle_symbol(self, symbol: str) -> str: """Get demangled C++ symbol name from cache.""" @@ -372,6 +458,763 @@ class MemoryAnalyzer: return "Other Core" + def _discover_pio_libraries( + self, + libraries: dict[str, list[Path]], + hash_to_name: dict[str, str], + ) -> None: + """Discover PlatformIO third-party libraries from the build directory. + + Scans ``lib/`` directories under ``.pioenvs//`` to find + library names and their ``.a`` archive or ``.o`` file paths. + + Args: + libraries: Dict to populate with library name -> file path list mappings. + Prefers ``.a`` archives when available, falls back to ``.o`` files + (e.g., pioarduino ESP32 Arduino builds only produce ``.o`` files). + hash_to_name: Dict to populate with dir name -> library name mappings + for CSWTCH attribution (e.g., ``lib641`` -> ``espsoftwareserial``). + """ + build_dir = self.elf_path.parent + + for entry in build_dir.iterdir(): + if not entry.is_dir() or not entry.name.startswith("lib"): + continue + # Validate that the suffix after "lib" is a hex hash + hex_part = entry.name[3:] + if not hex_part: + continue + try: + int(hex_part, 16) + except ValueError: + continue + + # Each lib/ directory contains a subdirectory named after the library + for lib_subdir in entry.iterdir(): + if not lib_subdir.is_dir(): + continue + lib_name = lib_subdir.name.lower() + + # Prefer .a archive (lib.a), fall back to .o files + # e.g., lib72a/ESPAsyncTCP/... has lib72a/libESPAsyncTCP.a + archive = entry / f"lib{lib_subdir.name}.a" + if archive.exists(): + file_paths = [archive] + elif archives := list(entry.glob("*.a")): + # Case-insensitive fallback + file_paths = [archives[0]] + else: + # No .a archive (e.g., pioarduino CMake builds) - use .o files + file_paths = sorted(lib_subdir.rglob("*.o")) + + if file_paths: + libraries[lib_name] = file_paths + hash_to_name[entry.name] = lib_name + _LOGGER.debug( + "Discovered PlatformIO library: %s -> %s", + lib_subdir.name, + file_paths[0], + ) + + def _discover_idf_managed_components( + self, + libraries: dict[str, list[Path]], + hash_to_name: dict[str, str], + ) -> None: + """Discover ESP-IDF managed component libraries from the build directory. + + ESP-IDF managed components (from the IDF component registry) use a + ``__`` naming convention. Source files live under + ``managed_components/__/`` and the compiled archives are at + ``esp-idf/__/lib__.a``. + + Args: + libraries: Dict to populate with library name -> file path list mappings. + hash_to_name: Dict to populate with dir name -> library name mappings + for CSWTCH attribution (e.g., ``espressif__mdns`` -> ``mdns``). + """ + build_dir = self.elf_path.parent + + managed_dir = build_dir / "managed_components" + if not managed_dir.is_dir(): + return + + espidf_dir = build_dir / "esp-idf" + + for entry in managed_dir.iterdir(): + if not entry.is_dir() or "__" not in entry.name: + continue + + # Extract the short name: espressif__mdns -> mdns + full_name = entry.name # e.g., espressif__mdns + short_name = full_name.split("__", 1)[1].lower() + + # Find the .a archive under esp-idf/__/ + archive = espidf_dir / full_name / f"lib{full_name}.a" + if archive.exists(): + libraries[short_name] = [archive] + hash_to_name[full_name] = short_name + _LOGGER.debug( + "Discovered IDF managed component: %s -> %s", + short_name, + archive, + ) + + def _build_library_symbol_map( + self, libraries: dict[str, list[Path]] + ) -> dict[str, str]: + """Build a symbol-to-library mapping from library archives or object files. + + Runs ``nm --defined-only`` on each ``.a`` or ``.o`` file to collect + global and weak defined symbols. + + Args: + libraries: Dictionary mapping library name to list of file paths + (``.a`` archives or ``.o`` object files). + + Returns: + Dictionary mapping symbol name to library name. + """ + symbol_map: dict[str, str] = {} + + if not self.nm_path: + return symbol_map + + for lib_name, file_paths in libraries.items(): + result = run_tool( + [self.nm_path, "--defined-only", *(str(p) for p in file_paths)], + timeout=10, + ) + if result is None or result.returncode != 0: + continue + + for line in result.stdout.splitlines(): + parts = line.split() + if len(parts) < 3: + continue + + sym_type = parts[-2] + sym_name = parts[-1] + + # Include global defined symbols (uppercase) and weak symbols (W/V) + if sym_type in _NM_DEFINED_GLOBAL_TYPES: + symbol_map[sym_name] = lib_name + + return symbol_map + + @staticmethod + def _build_heuristic_to_lib_mapping( + library_names: set[str], + ) -> dict[str, str]: + """Build mapping from heuristic pattern categories to discovered libraries. + + Heuristic categories like ``mdns_lib``, ``web_server_lib``, ``async_tcp`` + exist as approximations for library attribution. When we discover the + actual library, symbols matching those heuristics should be redirected + to the ``[lib]`` category instead. + + The mapping is built by checking if the normalized category name + (stripped of ``_lib`` suffix and underscores) appears as a substring + of any discovered library name. + + Examples:: + + mdns_lib -> mdns -> in "mdns" or "esp8266mdns" -> [lib]mdns + web_server_lib -> webserver -> in "espasyncwebserver" -> [lib]espasyncwebserver + async_tcp -> asynctcp -> in "espasynctcp" -> [lib]espasynctcp + + Args: + library_names: Set of discovered library names (lowercase). + + Returns: + Dictionary mapping heuristic category to ``[lib]`` string. + """ + mapping: dict[str, str] = {} + all_categories = set(SYMBOL_PATTERNS) | set(DEMANGLED_PATTERNS) + + for category in all_categories: + base = category.removesuffix("_lib").replace("_", "") + # Collect all libraries whose name contains the base string + candidates = [lib_name for lib_name in library_names if base in lib_name] + if not candidates: + continue + + # Choose a deterministic "best" match: + # 1. Prefer exact name matches over substring matches. + # 2. Among non-exact matches, prefer the shortest library name. + # 3. Break remaining ties lexicographically. + best_lib = min( + candidates, + key=lambda lib_name, _base=base: ( + lib_name != _base, + len(lib_name), + lib_name, + ), + ) + mapping[category] = f"{_COMPONENT_PREFIX_LIB}{best_lib}" + + if mapping: + _LOGGER.debug( + "Heuristic-to-library redirects: %s", + ", ".join(f"{k} -> {v}" for k, v in sorted(mapping.items())), + ) + + return mapping + + def _parse_map_file(self) -> dict[str, str] | None: + """Parse linker map file to build authoritative symbol-to-library mapping. + + The linker map file contains the definitive source attribution for every + symbol, including local/static ones that ``nm`` cannot safely export. + + Map file format (GNU ld):: + + .text._mdns_service_task + 0x400e9fdc 0x65c .pioenvs/env/esp-idf/espressif__mdns/libespressif__mdns.a(mdns.c.o) + + Each section entry has a ``.section.symbol_name`` line followed by an + indented line with address, size, and source path. + + Returns: + Symbol-to-library dict, or ``None`` if no usable map file exists. + """ + map_path = self.elf_path.with_suffix(".map") + if not map_path.exists() or map_path.stat().st_size < 10000: + return None + + _LOGGER.info("Parsing linker map file: %s", map_path.name) + + try: + map_text = map_path.read_text(encoding="utf-8", errors="replace") + except OSError as err: + _LOGGER.warning("Failed to read map file: %s", err) + return None + + symbol_map: dict[str, str] = {} + source_symbol_map: dict[str, str] = {} + current_symbol: str | None = None + section_prefixes = (".text.", ".rodata.", ".data.", ".bss.", ".literal.") + + for line in map_text.splitlines(): + # Match section.symbol line: " .text.symbol_name" + # Single space indent, starts with dot + if len(line) > 2 and line[0] == " " and line[1] == ".": + stripped = line.strip() + for prefix in section_prefixes: + if stripped.startswith(prefix): + current_symbol = stripped[len(prefix) :] + break + else: + current_symbol = None + continue + + # Match source attribution line: " 0xADDR 0xSIZE source_path" + if current_symbol is None: + continue + + fields = line.split() + # Skip compiler-generated local names (e.g., packet$19, buf$20) + # that can collide across compilation units + if ( + len(fields) >= 3 + and fields[0].startswith("0x") + and fields[1].startswith("0x") + and not _COMPILER_LOCAL_PATTERN.match(current_symbol) + ): + source_path = fields[2] + # Check if source path contains a known library directory + for dir_key, lib_name in self._lib_hash_to_name.items(): + if dir_key in source_path: + symbol_map[current_symbol] = lib_name + break + else: + # Map ESPHome source files to components for extern "C" + # and other symbols without C++ namespace + component = self._source_file_to_component(source_path) + if component.startswith( + (_COMPONENT_PREFIX_ESPHOME, _COMPONENT_PREFIX_EXTERNAL) + ): + source_symbol_map[current_symbol] = component + + current_symbol = None + + self._source_symbol_map = source_symbol_map + return symbol_map or None + + def _scan_libraries(self) -> None: + """Discover third-party libraries and build symbol mapping. + + Scans both PlatformIO ``lib/`` directories (Arduino builds) and + ESP-IDF ``managed_components/`` (IDF builds) to find library archives. + + Uses the linker map file for authoritative symbol attribution when + available, falling back to ``nm`` scanning with heuristic redirects. + """ + libraries: dict[str, list[Path]] = {} + self._discover_pio_libraries(libraries, self._lib_hash_to_name) + self._discover_idf_managed_components(libraries, self._lib_hash_to_name) + + if not libraries: + _LOGGER.debug("No third-party libraries found") + return + + _LOGGER.info( + "Scanning %d libraries: %s", + len(libraries), + ", ".join(sorted(libraries)), + ) + + # Heuristic redirect catches local symbols (e.g., mdns_task_buffer$14) + # that can't be safely added to the symbol map due to name collisions + self._heuristic_to_lib = self._build_heuristic_to_lib_mapping( + set(libraries.keys()) + ) + + # Try linker map file first (authoritative, includes local symbols) + map_symbols = self._parse_map_file() + if map_symbols is not None: + self._lib_symbol_map = map_symbols + _LOGGER.info( + "Built library symbol map from linker map: %d symbols", + len(self._lib_symbol_map), + ) + return + + # Fall back to nm scanning (global symbols only) + self._lib_symbol_map = self._build_library_symbol_map(libraries) + + _LOGGER.info( + "Built library symbol map from nm: %d symbols from %d libraries", + len(self._lib_symbol_map), + len(libraries), + ) + + def _scan_source_symbols(self) -> None: + """Scan ESPHome source object files to map extern "C" symbols to components. + + When no linker map file is available, this uses ``nm`` to scan ``.o`` files + under ``src/esphome/`` and build a symbol-to-component mapping. This catches + ``extern "C"`` functions and other symbols that lack C++ namespace prefixes. + + Skips scanning if ``_source_symbol_map`` was already populated by + ``_parse_map_file()``. + """ + if self._source_symbol_map or not self.nm_path: + return + + obj_dir = self._find_object_files_dir() + if obj_dir is None: + return + + # Find ESPHome source object files + esphome_src_dir = obj_dir / "src" / "esphome" + if not esphome_src_dir.is_dir(): + return + + obj_files = sorted(esphome_src_dir.rglob("*.o")) + if not obj_files: + return + + # Run nm with --print-file-name to get file:symbol mapping + result = run_tool( + [self.nm_path, "--print-file-name", "-g", "--defined-only"] + + [str(f) for f in obj_files], + ) + if result is None or result.returncode != 0: + _LOGGER.debug("nm scan of source objects failed") + return + + self._source_symbol_map = self._parse_nm_source_output(result.stdout, obj_dir) + if self._source_symbol_map: + _LOGGER.info( + "Built source symbol map from nm: %d symbols", + len(self._source_symbol_map), + ) + + def _parse_nm_source_output(self, output: str, base_dir: Path) -> dict[str, str]: + """Parse nm output to map non-namespaced symbols to ESPHome components. + + Extracts global defined symbols from ESPHome source object files that + don't use C++ namespacing (e.g. ``extern "C"`` functions). + + Args: + output: Raw stdout from ``nm --print-file-name -g --defined-only`` + or ``nm --print-file-name -S``. + base_dir: Build directory for computing relative paths. + + Returns: + Dict mapping symbol names to component names. + """ + source_map: dict[str, str] = {} + for line in output.splitlines(): + # Format: /path/to/file.o: addr type name + # or: /path/to/file.o: addr size type name (with -S) + colon_idx = line.rfind(".o:") + if colon_idx == -1: + continue + + file_path = line[: colon_idx + 2] + fields = line[colon_idx + 3 :].split() + if len(fields) < 3: + continue + + # With -S flag, format is: addr size type name + # Without -S flag: addr type name + # type is a single char; size is hex digits + # Detect by checking if fields[1] is a single uppercase letter (type) + if len(fields[1]) == 1 and fields[1].isalpha(): + # addr type name + sym_type = fields[1] + symbol_name = fields[2] + elif len(fields) >= 4: + # addr size type name + sym_type = fields[2] + symbol_name = fields[3] + else: + continue + + # Only global defined symbols (uppercase type) + if not sym_type.isupper() or sym_type == "U": + continue + + # Skip symbols already in esphome:: namespace + if symbol_name.startswith("_ZN7esphome"): + continue + + # Make path relative to base_dir for _source_file_to_component + try: + rel_path = str(Path(file_path).relative_to(base_dir)) + except ValueError: + continue + + component = self._source_file_to_component(rel_path) + if component.startswith( + (_COMPONENT_PREFIX_ESPHOME, _COMPONENT_PREFIX_EXTERNAL) + ): + source_map[symbol_name] = component + + return source_map + + def _find_object_files_dir(self) -> Path | None: + """Find the directory containing object files for this build. + + Returns: + Path to the directory containing .o files, or None if not found. + """ + # The ELF is typically at .pioenvs//firmware.elf + # Object files are in .pioenvs//src/ and .pioenvs//lib*/ + pioenvs_dir = self.elf_path.parent + if pioenvs_dir.exists() and any(pioenvs_dir.glob("src/*.o")): + return pioenvs_dir + return None + + @staticmethod + def _parse_nm_cswtch_output( + output: str, + base_dir: Path | None, + cswtch_map: dict[str, list[tuple[str, int]]], + ) -> None: + """Parse nm output for CSWTCH symbols and add to cswtch_map. + + Handles both ``.o`` files and ``.a`` archives. + + nm output formats:: + + .o files: /path/file.o:hex_addr hex_size type name + .a files: /path/lib.a:member.o:hex_addr hex_size type name + + For ``.o`` files, paths are made relative to *base_dir* when possible. + For ``.a`` archives (detected by ``:`` in the file portion), paths are + formatted as ``archive_stem/member.o`` (e.g. ``liblwip2-536-feat/lwip-esp.o``). + + Args: + output: Raw stdout from ``nm --print-file-name -S``. + base_dir: Base directory for computing relative paths of ``.o`` files. + Pass ``None`` when scanning archives outside the build tree. + cswtch_map: Dict to populate, mapping ``"CSWTCH$N:size"`` to source list. + """ + for line in output.splitlines(): + if "CSWTCH$" not in line: + continue + + # Split on last ":" that precedes a hex address. + # For .o: "filepath.o" : "hex_addr hex_size type name" + # For .a: "filepath.a:member.o" : "hex_addr hex_size type name" + parts_after_colon = line.rsplit(":", 1) + if len(parts_after_colon) != 2: + continue + + file_path = parts_after_colon[0] + fields = parts_after_colon[1].split() + # fields: [address, size, type, name] + if len(fields) < 4: + continue + + sym_name = fields[3] + if not sym_name.startswith("CSWTCH$"): + continue + + try: + size = int(fields[1], 16) + except ValueError: + continue + + # Determine readable source path + # Use ".a:" to detect archive format (not bare ":" which matches + # Windows drive letters like "C:\...\file.o"). + if ".a:" in file_path: + # Archive format: "archive.a:member.o" → "archive_stem/member.o" + archive_part, member = file_path.rsplit(":", 1) + archive_name = Path(archive_part).stem + rel_path = f"{archive_name}/{member}" + elif base_dir is not None: + try: + rel_path = str(Path(file_path).relative_to(base_dir)) + except ValueError: + rel_path = file_path + else: + rel_path = file_path + + key = f"{sym_name}:{size}" + cswtch_map[key].append((rel_path, size)) + + def _run_nm_cswtch_scan( + self, + files: list[Path], + base_dir: Path | None, + cswtch_map: dict[str, list[tuple[str, int]]], + ) -> None: + """Run nm on *files* and add any CSWTCH symbols to *cswtch_map*. + + Args: + files: Object (``.o``) or archive (``.a``) files to scan. + base_dir: Base directory for relative path computation (see + :meth:`_parse_nm_cswtch_output`). + cswtch_map: Dict to populate with results. + """ + if not self.nm_path or not files: + return + + _LOGGER.debug("Scanning %d files for CSWTCH symbols", len(files)) + + result = run_tool( + [self.nm_path, "--print-file-name", "-S"] + [str(f) for f in files], + timeout=30, + ) + if result is None or result.returncode != 0: + _LOGGER.debug( + "nm failed or timed out scanning %d files for CSWTCH symbols", + len(files), + ) + return + + self._parse_nm_cswtch_output(result.stdout, base_dir, cswtch_map) + + def _scan_cswtch_in_sdk_archives( + self, cswtch_map: dict[str, list[tuple[str, int]]] + ) -> None: + """Scan SDK library archives (.a) for CSWTCH symbols. + + Prebuilt SDK libraries (e.g. lwip, bearssl) are not compiled from source, + so their CSWTCH symbols only exist inside ``.a`` archives. Results are + merged into *cswtch_map* for keys not already found in ``.o`` files. + + The same source file (e.g. ``lwip-esp.o``) often appears in multiple + library variants (``liblwip2-536.a``, ``liblwip2-1460-feat.a``, etc.), + so results are deduplicated by member name. + """ + sdk_dirs = self._find_sdk_library_dirs() + if not sdk_dirs: + return + + sdk_archives = sorted(a for sdk_dir in sdk_dirs for a in sdk_dir.glob("*.a")) + + sdk_map: dict[str, list[tuple[str, int]]] = defaultdict(list) + self._run_nm_cswtch_scan(sdk_archives, None, sdk_map) + + # Merge SDK results, deduplicating by member name. + for key, sources in sdk_map.items(): + if key in cswtch_map: + continue + seen: dict[str, tuple[str, int]] = {} + for path, sz in sources: + member = Path(path).name + if member not in seen: + seen[member] = (path, sz) + cswtch_map[key] = list(seen.values()) + + def _source_file_to_component(self, source_file: str) -> str: + """Map a source object file path to its component name. + + Args: + source_file: Relative path like 'src/esphome/components/wifi/wifi_component.cpp.o' + + Returns: + Component name like '[esphome]wifi' or the source file if unknown. + """ + parts = Path(source_file).parts + + # ESPHome component: src/esphome/components//... + if "components" in parts: + idx = parts.index("components") + if idx + 1 < len(parts): + component_name = parts[idx + 1] + if component_name in get_esphome_components(): + return f"{_COMPONENT_PREFIX_ESPHOME}{component_name}" + if component_name in self.external_components: + return f"{_COMPONENT_PREFIX_EXTERNAL}{component_name}" + + # ESPHome core: src/esphome/core/... or src/esphome/... + if "core" in parts and "esphome" in parts: + return _COMPONENT_CORE + if "esphome" in parts and "components" not in parts: + return _COMPONENT_CORE + + # Framework/library files - check for PlatformIO library hash dirs + # e.g., lib65b/ESPAsyncTCP/... -> [lib]espasynctcp + if parts and parts[0] in self._lib_hash_to_name: + return f"{_COMPONENT_PREFIX_LIB}{self._lib_hash_to_name[parts[0]]}" + + # ESP-IDF managed components: managed_components/espressif__mdns/... -> [lib]mdns + if ( + len(parts) >= 2 + and parts[0] == "managed_components" + and parts[1] in self._lib_hash_to_name + ): + return f"{_COMPONENT_PREFIX_LIB}{self._lib_hash_to_name[parts[1]]}" + + # Other framework/library files - return the first path component + # e.g., FrameworkArduino/... -> FrameworkArduino + return parts[0] if parts else source_file + + def _analyze_cswtch_symbols(self) -> None: + """Analyze CSWTCH (GCC switch table) symbols by tracing to source objects. + + CSWTCH symbols are compiler-generated lookup tables for switch statements. + They are local symbols, so the same name can appear in different object files. + This method scans .o files and SDK archives to attribute them to their + source components. + """ + obj_dir = self._find_object_files_dir() + if obj_dir is None: + _LOGGER.debug("No object files directory found, skipping CSWTCH analysis") + return + + # Scan build-dir object files for CSWTCH symbols + cswtch_map: dict[str, list[tuple[str, int]]] = defaultdict(list) + self._run_nm_cswtch_scan(sorted(obj_dir.rglob("*.o")), obj_dir, cswtch_map) + + # Also scan SDK library archives (.a) for CSWTCH symbols. + # Prebuilt SDK libraries (e.g. lwip, bearssl) are not compiled from source + # so their symbols only exist inside .a archives, not as loose .o files. + self._scan_cswtch_in_sdk_archives(cswtch_map) + + if not cswtch_map: + _LOGGER.debug("No CSWTCH symbols found in object files or SDK archives") + return + + # Collect CSWTCH symbols from the ELF (already parsed in sections) + # Include section_name for re-attribution of component totals + elf_cswtch = [ + (symbol_name, size, section_name) + for section_name, section in self.sections.items() + for symbol_name, size, _ in section.symbols + if symbol_name.startswith("CSWTCH$") + ] + + _LOGGER.debug( + "Found %d CSWTCH symbols in ELF, %d unique in object files", + len(elf_cswtch), + len(cswtch_map), + ) + + # Match ELF CSWTCH symbols to source files and re-attribute component totals. + # _categorize_symbols() already ran and put these into "other" since CSWTCH$ + # names don't match any component pattern. We move the bytes to the correct + # component based on the object file mapping. + other_mem = self.components.get("other") + + for sym_name, size, section_name in elf_cswtch: + key = f"{sym_name}:{size}" + sources = cswtch_map.get(key, []) + + if len(sources) == 1: + source_file = sources[0][0] + component = self._source_file_to_component(source_file) + elif len(sources) > 1: + # Ambiguous - multiple object files have same CSWTCH name+size + source_file = "ambiguous" + component = "ambiguous" + _LOGGER.debug( + "Ambiguous CSWTCH %s (%d B) found in %d files: %s", + sym_name, + size, + len(sources), + ", ".join(src for src, _ in sources), + ) + else: + source_file = "unknown" + component = "unknown" + + self._cswtch_symbols.append((sym_name, size, source_file, component)) + + # Re-attribute from "other" to the correct component + if ( + component not in ("other", "unknown", "ambiguous") + and other_mem is not None + ): + other_mem.add_section_size(section_name, -size) + if component not in self.components: + self.components[component] = ComponentMemory(component) + self.components[component].add_section_size(section_name, size) + + # Sort by size descending + self._cswtch_symbols.sort(key=lambda x: x[1], reverse=True) + + total_size = sum(size for _, size, _, _ in self._cswtch_symbols) + _LOGGER.debug( + "CSWTCH analysis: %d symbols, %d bytes total", + len(self._cswtch_symbols), + total_size, + ) + + def _analyze_function_calls(self) -> None: + """Count function call sites by parsing disassembly output. + + Parses direct call instructions (call0/call8/bl/blx) from objdump -d + to count how many times each function is called. This helps identify + inlining candidates — frequently called small functions benefit most + from inlining. + """ + result = run_tool( + [self.objdump_path, "-d", str(self.elf_path)], + timeout=60, + ) + if result is None or result.returncode != 0: + _LOGGER.debug("Failed to disassemble ELF for function call analysis") + return + + self._function_call_counts = Counter( + match.group(1) + for line in result.stdout.splitlines() + if (match := _CALL_TARGET_PATTERN.search(line)) + ) + + # Demangle any call targets not already in the cache + missing = [ + name + for name in self._function_call_counts + if name not in self._demangle_cache + ] + if missing: + self._batch_demangle_symbols(missing) + + _LOGGER.debug( + "Function call analysis: %d unique targets, %d total calls", + len(self._function_call_counts), + sum(self._function_call_counts.values()), + ) + def get_unattributed_ram(self) -> tuple[int, int, int]: """Get unattributed RAM sizes (SDK/framework overhead). diff --git a/esphome/analyze_memory/cli.py b/esphome/analyze_memory/cli.py index a77e17afce..b7561e8ffc 100644 --- a/esphome/analyze_memory/cli.py +++ b/esphome/analyze_memory/cli.py @@ -4,6 +4,8 @@ from __future__ import annotations from collections import defaultdict from collections.abc import Callable +import heapq +from operator import itemgetter import sys from typing import TYPE_CHECKING @@ -12,6 +14,8 @@ from . import ( _COMPONENT_CORE, _COMPONENT_PREFIX_ESPHOME, _COMPONENT_PREFIX_EXTERNAL, + _COMPONENT_PREFIX_LIB, + _PSTORAGE_SUFFIX, RAM_SECTIONS, MemoryAnalyzer, ) @@ -20,6 +24,17 @@ if TYPE_CHECKING: from . import ComponentMemory +def _format_pstorage_name(name: str) -> str: + """Format a __pstorage symbol as 'storage for {id}'.""" + if not name.endswith(_PSTORAGE_SUFFIX): + return name + prefix = name[: -len(_PSTORAGE_SUFFIX)] + # Strip component namespace prefix: {component}__{id} -> {id} + dunder_pos = prefix.find("__") + var_id = prefix[dunder_pos + 2 :] if dunder_pos != -1 else prefix + return f"storage for {var_id}" + + class MemoryAnalyzerCLI(MemoryAnalyzer): """Memory analyzer with CLI-specific report generation.""" @@ -29,6 +44,10 @@ class MemoryAnalyzerCLI(MemoryAnalyzer): ) # Lower threshold for RAM symbols (RAM is more constrained) RAM_SYMBOL_SIZE_THRESHOLD: int = 24 + # Number of top symbols to show in the largest symbols report + TOP_SYMBOLS_LIMIT: int = 30 + # Width for symbol name display in top symbols report + COL_TOP_SYMBOL_NAME: int = 55 # Column width constants COL_COMPONENT: int = 29 @@ -141,11 +160,197 @@ class MemoryAnalyzerCLI(MemoryAnalyzer): If section is one of the RAM sections (.data or .bss), a label like " [data]" or " [bss]" is appended. For non-RAM sections or when section is None, no section label is added. + + Placement new storage symbols are formatted as "storage for {id}". """ + display_name = _format_pstorage_name(demangled) section_label = "" if section in RAM_SECTIONS: section_label = f" [{section[1:]}]" # .data -> [data], .bss -> [bss] - return f"{demangled} ({size:,} B){section_label}" + return f"{display_name} ({size:,} B){section_label}" + + def _add_top_symbols(self, lines: list[str]) -> None: + """Add a section showing the top largest symbols in the binary.""" + # Collect all symbols from all components: (symbol, demangled, size, section, component) + all_symbols = [ + (symbol, demangled, size, section, component) + for component, symbols in self._component_symbols.items() + for symbol, demangled, size, section in symbols + ] + + # Get top N symbols by size using heapq for efficiency + top_symbols = heapq.nlargest( + self.TOP_SYMBOLS_LIMIT, all_symbols, key=itemgetter(2) + ) + + lines.append("") + lines.append(f"Top {self.TOP_SYMBOLS_LIMIT} Largest Symbols:") + # Calculate truncation limit from column width (leaving room for "...") + truncate_limit = self.COL_TOP_SYMBOL_NAME - 3 + for i, (_, demangled, size, section, component) in enumerate(top_symbols): + # Format section label + section_label = f"[{section[1:]}]" if section else "" + # Format storage symbols readably + display_name = _format_pstorage_name(demangled) + # Truncate if too long + demangled_display = ( + f"{display_name[:truncate_limit]}..." + if len(display_name) > self.COL_TOP_SYMBOL_NAME + else display_name + ) + lines.append( + f"{i + 1:>2}. {size:>7,} B {section_label:<8} {demangled_display:<{self.COL_TOP_SYMBOL_NAME}} {component}" + ) + + def _add_cswtch_analysis(self, lines: list[str]) -> None: + """Add CSWTCH (GCC switch table lookup) analysis section.""" + self._add_section_header(lines, "CSWTCH Analysis (GCC Switch Table Lookups)") + + total_size = sum(size for _, size, _, _ in self._cswtch_symbols) + lines.append( + f"Total: {len(self._cswtch_symbols)} switch table(s), {total_size:,} B" + ) + lines.append("") + + # Group by component + by_component: dict[str, list[tuple[str, int, str]]] = defaultdict(list) + for sym_name, size, source_file, component in self._cswtch_symbols: + by_component[component].append((sym_name, size, source_file)) + + # Sort components by total size descending + sorted_components = sorted( + by_component.items(), + key=lambda x: sum(s[1] for s in x[1]), + reverse=True, + ) + + for component, symbols in sorted_components: + comp_total = sum(s[1] for s in symbols) + lines.append(f"{component} ({comp_total:,} B, {len(symbols)} tables):") + + # Group by source file within component + by_file: dict[str, list[tuple[str, int]]] = defaultdict(list) + for sym_name, size, source_file in symbols: + by_file[source_file].append((sym_name, size)) + + for source_file, file_symbols in sorted( + by_file.items(), + key=lambda x: sum(s[1] for s in x[1]), + reverse=True, + ): + file_total = sum(s[1] for s in file_symbols) + lines.append( + f" {source_file} ({file_total:,} B, {len(file_symbols)} tables)" + ) + for sym_name, size in sorted( + file_symbols, key=lambda x: x[1], reverse=True + ): + lines.append(f" {size:>6,} B {sym_name}") + lines.append("") + + # Number of top called functions to show + TOP_CALLS_LIMIT: int = 50 + # Number of inlining candidates to show + INLINE_CANDIDATES_LIMIT: int = 25 + # Maximum function size in bytes to consider for inlining + INLINE_SIZE_THRESHOLD: int = 16 + + def _build_symbol_sizes(self) -> dict[str, int]: + """Build a size lookup from all component symbols: mangled_name -> size.""" + return { + symbol: size + for symbols in self._component_symbols.values() + for symbol, _, size, _ in symbols + } + + def _format_call_row( + self, index: int, mangled: str, count: int, symbol_sizes: dict[str, int] + ) -> str: + """Format a single row for call frequency tables.""" + demangled = self._demangle_cache.get(mangled, mangled) + if len(demangled) > 80: + demangled = f"{demangled[:77]}..." + size = symbol_sizes.get(mangled) + size_str = f"{size:>5,} B" if size is not None else " ?" + return f"{index:>3} {count:>5} {size_str} {demangled}" + + def _add_call_table_header(self, lines: list[str]) -> None: + """Add the header row for call frequency tables.""" + lines.append(f"{'#':>3} {'Calls':>5} {'Size':>7} Function") + lines.append(f"{'---':>3} {'-----':>5} {'-------':>7} {'-' * 60}") + + def _add_function_call_analysis(self, lines: list[str]) -> None: + """Add function call frequency analysis section. + + Shows the most frequently called functions by call site count. + """ + self._add_section_header(lines, "Top Called Functions") + + symbol_sizes = self._build_symbol_sizes() + + # Sort by call count descending + sorted_calls = sorted( + self._function_call_counts.items(), key=lambda x: x[1], reverse=True + ) + + self._add_call_table_header(lines) + + for i, (mangled, count) in enumerate(sorted_calls[: self.TOP_CALLS_LIMIT]): + lines.append(self._format_call_row(i + 1, mangled, count, symbol_sizes)) + + total_calls = sum(self._function_call_counts.values()) + lines.append("") + lines.append( + f"Total: {len(self._function_call_counts)} unique targets, " + f"{total_calls:,} call sites" + ) + lines.append("") + + def _add_inline_candidates(self, lines: list[str]) -> None: + """Add inlining candidates section. + + Shows frequently called functions that are small enough to benefit + from inlining (< 16 bytes). These are the best candidates for + reducing call overhead. + """ + self._add_section_header( + lines, + f"Inlining Candidates (<{self.INLINE_SIZE_THRESHOLD} B, by call count)", + ) + + symbol_sizes = self._build_symbol_sizes() + + # Filter to small functions with known size, sort by call count + candidates = sorted( + ( + (mangled, count) + for mangled, count in self._function_call_counts.items() + if mangled in symbol_sizes + and symbol_sizes[mangled] < self.INLINE_SIZE_THRESHOLD + ), + key=lambda x: x[1], + reverse=True, + ) + + if not candidates: + lines.append("No candidates found.") + lines.append("") + return + + self._add_call_table_header(lines) + + for i, (mangled, count) in enumerate( + candidates[: self.INLINE_CANDIDATES_LIMIT] + ): + lines.append(self._format_call_row(i + 1, mangled, count, symbol_sizes)) + + lines.append("") + lines.append( + f"Showing top {min(len(candidates), self.INLINE_CANDIDATES_LIMIT)} " + f"of {len(candidates)} functions under " + f"{self.INLINE_SIZE_THRESHOLD} B" + ) + lines.append("") def generate_report(self, detailed: bool = False) -> str: """Generate a formatted memory report.""" @@ -248,6 +453,9 @@ class MemoryAnalyzerCLI(MemoryAnalyzer): "RAM", ) + # Top largest symbols in the binary + self._add_top_symbols(lines) + # Add ESPHome core detailed analysis if there are core symbols if self._esphome_core_symbols: self._add_section_header(lines, f"{_COMPONENT_CORE} Detailed Analysis") @@ -321,6 +529,11 @@ class MemoryAnalyzerCLI(MemoryAnalyzer): for name, mem in components if name.startswith(_COMPONENT_PREFIX_EXTERNAL) ] + library_components = [ + (name, mem) + for name, mem in components + if name.startswith(_COMPONENT_PREFIX_LIB) + ] top_esphome_components = sorted( esphome_components, key=lambda x: x[1].flash_total, reverse=True @@ -331,6 +544,11 @@ class MemoryAnalyzerCLI(MemoryAnalyzer): external_components, key=lambda x: x[1].flash_total, reverse=True ) + # Include all library components + top_library_components = sorted( + library_components, key=lambda x: x[1].flash_total, reverse=True + ) + # Check if API component exists and ensure it's included api_component = None for name, mem in components: @@ -349,10 +567,11 @@ class MemoryAnalyzerCLI(MemoryAnalyzer): if name in system_components_to_include ] - # Combine all components to analyze: top ESPHome + all external + API if not already included + system components + # Combine all components to analyze: top ESPHome + all external + libraries + API if not already included + system components components_to_analyze = ( list(top_esphome_components) + list(top_external_components) + + list(top_library_components) + system_components ) if api_component and api_component not in components_to_analyze: @@ -371,15 +590,16 @@ class MemoryAnalyzerCLI(MemoryAnalyzer): lines.append(f"Total size: {comp_mem.flash_total:,} B") lines.append("") - # Show all symbols above threshold for better visibility + # Show symbols above threshold, always include storage symbols large_symbols = [ (sym, dem, size, sec) for sym, dem, size, sec in sorted_symbols if size > self.SYMBOL_SIZE_THRESHOLD + or dem.endswith(_PSTORAGE_SUFFIX) ] lines.append( - f"{comp_name} Symbols > {self.SYMBOL_SIZE_THRESHOLD} B ({len(large_symbols)} symbols):" + f"{comp_name} Symbols > {self.SYMBOL_SIZE_THRESHOLD} B & storage ({len(large_symbols)} symbols):" ) for i, (symbol, demangled, size, section) in enumerate(large_symbols): lines.append( @@ -402,7 +622,10 @@ class MemoryAnalyzerCLI(MemoryAnalyzer): # Sort by size descending sorted_ram_syms = sorted(ram_syms, key=lambda x: x[2], reverse=True) large_ram_syms = [ - s for s in sorted_ram_syms if s[2] > self.RAM_SYMBOL_SIZE_THRESHOLD + s + for s in sorted_ram_syms + if s[2] > self.RAM_SYMBOL_SIZE_THRESHOLD + or s[1].endswith(_PSTORAGE_SUFFIX) ] lines.append(f"{name} ({mem.ram_total:,} B total RAM):") @@ -420,17 +643,27 @@ class MemoryAnalyzerCLI(MemoryAnalyzer): for symbol, demangled, size, section in large_ram_syms[:10]: # Format section label consistently by stripping leading dot section_label = section.lstrip(".") if section else "" + display_name = _format_pstorage_name(demangled) # Add ellipsis if name is truncated - demangled_display = ( - f"{demangled[:70]}..." if len(demangled) > 70 else demangled - ) - lines.append( - f" {size:>6,} B [{section_label}] {demangled_display}" + display_name = ( + f"{display_name[:70]}..." + if len(display_name) > 70 + else display_name ) + lines.append(f" {size:>6,} B [{section_label}] {display_name}") if len(large_ram_syms) > 10: lines.append(f" ... and {len(large_ram_syms) - 10} more") lines.append("") + # CSWTCH (GCC switch table) analysis + if self._cswtch_symbols: + self._add_cswtch_analysis(lines) + + # Function call frequency analysis + if self._function_call_counts: + self._add_function_call_analysis(lines) + self._add_inline_candidates(lines) + lines.append( "Note: This analysis covers symbols in the ELF file. Some runtime allocations may not be included." ) diff --git a/esphome/analyze_memory/const.py b/esphome/analyze_memory/const.py index 83547b1eb5..0c871d0727 100644 --- a/esphome/analyze_memory/const.py +++ b/esphome/analyze_memory/const.py @@ -66,15 +66,6 @@ SECTION_MAPPING = { ), } -# Section to ComponentMemory attribute mapping -# Maps section names to the attribute name in ComponentMemory dataclass -SECTION_TO_ATTR = { - ".text": "text_size", - ".rodata": "rodata_size", - ".data": "data_size", - ".bss": "bss_size", -} - # Component identification rules # Symbol patterns: patterns found in raw symbol names SYMBOL_PATTERNS = { @@ -265,7 +256,7 @@ SYMBOL_PATTERNS = { "ipv6_stack": ["nd6_", "ip6_", "mld6_", "icmp6_", "icmp6_input"], # Order matters! More specific categories must come before general ones. # mdns must come before bluetooth to avoid "_mdns_disable_pcb" matching "ble_" pattern - "mdns_lib": ["mdns"], + "mdns_lib": ["mdns", "packet$"], # memory_mgmt must come before wifi_stack to catch mmu_hal_* symbols "memory_mgmt": [ "mem_", @@ -417,7 +408,6 @@ SYMBOL_PATTERNS = { ], "arduino_core": [ "pinMode", - "resetPins", "millis", "micros", "delay(", # More specific - Arduino delay function with parenthesis @@ -513,7 +503,9 @@ SYMBOL_PATTERNS = { "__FUNCTION__$", "DAYS_IN_MONTH", "_DAYS_BEFORE_MONTH", - "CSWTCH$", + # Note: CSWTCH$ symbols are GCC switch table lookup tables. + # They are attributed to their source object files via _analyze_cswtch_symbols() + # rather than being lumped into libc. "dst$", "sulp", "_strtol_l", # String to long with locale @@ -801,7 +793,6 @@ SYMBOL_PATTERNS = { "s_dp", "s_ni", "s_reg_dump", - "packet$", "d_mult_table", "K", "fcstab", diff --git a/esphome/automation.py b/esphome/automation.py index 2439b1ddc4..94d64086ec 100644 --- a/esphome/automation.py +++ b/esphome/automation.py @@ -1,3 +1,5 @@ +import logging + import esphome.codegen as cg import esphome.config_validation as cv from esphome.const import ( @@ -57,8 +59,42 @@ def maybe_conf(conf, *validators): return validate -def register_action(name: str, action_type: MockObjClass, schema: cv.Schema): - return ACTION_REGISTRY.register(name, action_type, schema) +_LOGGER = logging.getLogger(__name__) + + +def register_action( + name: str, + action_type: MockObjClass, + schema: cv.Schema, + *, + synchronous: bool | None = None, +): + """Register an action type. + + All callers must pass ``synchronous`` explicitly. + + ``synchronous=True`` — the action never defers ``play_next_()`` to a + later point (callback, timer, or ``loop()``). Trigger arguments are + only used during the initial call, so string args can use non-owning + StringRef for zero-copy access. + + ``synchronous=False`` — the action defers ``play_next_()`` via a + callback, timer, or ``Component::loop()``. Trigger arguments must + outlive the initial call, so string args use owning std::string to + prevent dangling references. + """ + if synchronous is None: + _LOGGER.warning( + "register_action('%s', ...) is missing the synchronous= parameter. " + "Defaulting to synchronous=False (safe but prevents StringRef " + "optimization). Check the C++ class: use synchronous=False if " + "play_next_() is deferred to a callback, timer, or loop(); " + "use synchronous=True if play_next_() always runs before the " + "initial play/play_complex call returns", + name, + ) + synchronous = False + return ACTION_REGISTRY.register(name, action_type, schema, synchronous=synchronous) def register_condition(name: str, condition_type: MockObjClass, schema: cv.Schema): @@ -101,6 +137,9 @@ UpdateComponentAction = cg.esphome_ns.class_("UpdateComponentAction", Action) SuspendComponentAction = cg.esphome_ns.class_("SuspendComponentAction", Action) ResumeComponentAction = cg.esphome_ns.class_("ResumeComponentAction", Action) Automation = cg.esphome_ns.class_("Automation") +TriggerForwarder = cg.esphome_ns.class_("TriggerForwarder") +TriggerOnTrueForwarder = cg.esphome_ns.class_("TriggerOnTrueForwarder") +TriggerOnFalseForwarder = cg.esphome_ns.class_("TriggerOnFalseForwarder") LambdaCondition = cg.esphome_ns.class_("LambdaCondition", Condition) StatelessLambdaCondition = cg.esphome_ns.class_("StatelessLambdaCondition", Condition) @@ -211,7 +250,9 @@ async def and_condition_to_code( args: TemplateArgsType, ) -> MockObj: conditions = await build_condition_list(config, template_arg, args) - return cg.new_Pvariable(condition_id, template_arg, conditions) + return cg.new_Pvariable( + condition_id, cg.TemplateArguments(len(conditions), *template_arg), conditions + ) @register_condition("or", OrCondition, validate_condition_list) @@ -222,7 +263,9 @@ async def or_condition_to_code( args: TemplateArgsType, ) -> MockObj: conditions = await build_condition_list(config, template_arg, args) - return cg.new_Pvariable(condition_id, template_arg, conditions) + return cg.new_Pvariable( + condition_id, cg.TemplateArguments(len(conditions), *template_arg), conditions + ) @register_condition("all", AndCondition, validate_condition_list) @@ -233,7 +276,9 @@ async def all_condition_to_code( args: TemplateArgsType, ) -> MockObj: conditions = await build_condition_list(config, template_arg, args) - return cg.new_Pvariable(condition_id, template_arg, conditions) + return cg.new_Pvariable( + condition_id, cg.TemplateArguments(len(conditions), *template_arg), conditions + ) @register_condition("any", OrCondition, validate_condition_list) @@ -244,7 +289,9 @@ async def any_condition_to_code( args: TemplateArgsType, ) -> MockObj: conditions = await build_condition_list(config, template_arg, args) - return cg.new_Pvariable(condition_id, template_arg, conditions) + return cg.new_Pvariable( + condition_id, cg.TemplateArguments(len(conditions), *template_arg), conditions + ) @register_condition("not", NotCondition, validate_potentially_and_condition) @@ -266,7 +313,9 @@ async def xor_condition_to_code( args: TemplateArgsType, ) -> MockObj: conditions = await build_condition_list(config, template_arg, args) - return cg.new_Pvariable(condition_id, template_arg, conditions) + return cg.new_Pvariable( + condition_id, cg.TemplateArguments(len(conditions), *template_arg), conditions + ) @register_condition("lambda", LambdaCondition, cv.returning_lambda) @@ -335,7 +384,10 @@ async def component_is_idle_condition_to_code( @register_action( - "delay", DelayAction, cv.templatable(cv.positive_time_period_milliseconds) + "delay", + DelayAction, + cv.templatable(cv.positive_time_period_milliseconds), + synchronous=False, ) async def delay_action_to_code( config: ConfigType, @@ -366,6 +418,7 @@ async def delay_action_to_code( cv.has_at_least_one_key(CONF_THEN, CONF_ELSE), cv.has_at_least_one_key(CONF_CONDITION, CONF_ANY, CONF_ALL), ), + synchronous=True, ) async def if_action_to_code( config: ConfigType, @@ -373,13 +426,16 @@ async def if_action_to_code( template_arg: cg.TemplateArguments, args: TemplateArgsType, ) -> MockObj: + has_else = CONF_ELSE in config + # Prepend HasElse bool to template arguments: IfAction + if_template_arg = cg.TemplateArguments(has_else, *template_arg) cond_conf = next(el for el in config if el in (CONF_ANY, CONF_ALL, CONF_CONDITION)) condition = await build_condition(config[cond_conf], template_arg, args) - var = cg.new_Pvariable(action_id, template_arg, condition) + var = cg.new_Pvariable(action_id, if_template_arg, condition) if CONF_THEN in config: actions = await build_action_list(config[CONF_THEN], template_arg, args) cg.add(var.add_then(actions)) - if CONF_ELSE in config: + if has_else: actions = await build_action_list(config[CONF_ELSE], template_arg, args) cg.add(var.add_else(actions)) return var @@ -394,6 +450,7 @@ async def if_action_to_code( cv.Required(CONF_THEN): validate_action_list, } ), + synchronous=True, ) async def while_action_to_code( config: ConfigType, @@ -417,6 +474,7 @@ async def while_action_to_code( cv.Required(CONF_THEN): validate_action_list, } ), + synchronous=True, ) async def repeat_action_to_code( config: ConfigType, @@ -445,7 +503,7 @@ _validate_wait_until = cv.maybe_simple_value( ) -@register_action("wait_until", WaitUntilAction, _validate_wait_until) +@register_action("wait_until", WaitUntilAction, _validate_wait_until, synchronous=False) async def wait_until_action_to_code( config: ConfigType, action_id: ID, @@ -461,7 +519,12 @@ async def wait_until_action_to_code( return var -@register_action("lambda", LambdaAction, cv.lambda_) +# Lambda executes user C++ inline and returns — synchronous by execution model. +# User code could theoretically store the StringRef for deferred use, but StringRef +# is a view type and storing views beyond their scope is always unsafe regardless +# of this optimization. Marking non-synchronous would disable StringRef for nearly +# all user services since most use lambda. +@register_action("lambda", LambdaAction, cv.lambda_, synchronous=True) async def lambda_action_to_code( config: ConfigType, action_id: ID, @@ -480,6 +543,7 @@ async def lambda_action_to_code( cv.Required(CONF_ID): cv.use_id(cg.PollingComponent), } ), + synchronous=True, ) async def component_update_action_to_code( config: ConfigType, @@ -499,6 +563,7 @@ async def component_update_action_to_code( cv.Required(CONF_ID): cv.use_id(cg.PollingComponent), } ), + synchronous=True, ) async def component_suspend_action_to_code( config: ConfigType, @@ -521,6 +586,7 @@ async def component_suspend_action_to_code( ), } ), + synchronous=True, ) async def component_resume_action_to_code( config: ConfigType, @@ -578,6 +644,27 @@ async def build_condition_list( return conditions +def has_non_synchronous_actions(actions: ConfigType) -> bool: + """Check if a validated action list contains any non-synchronous actions. + + Non-synchronous actions (delay, wait_until, script.wait, etc.) store + trigger args for later execution, making non-owning types like StringRef + unsafe. + """ + if isinstance(actions, list): + return any(has_non_synchronous_actions(item) for item in actions) + if isinstance(actions, dict): + for key in actions: + if key in ACTION_REGISTRY and not ACTION_REGISTRY[key].synchronous: + return True + return any( + has_non_synchronous_actions(v) + for v in actions.values() + if isinstance(v, (list, dict)) + ) + return False + + async def build_automation( trigger: MockObj, args: TemplateArgsType, config: ConfigType ) -> MockObj: @@ -587,3 +674,44 @@ async def build_automation( actions = await build_action_list(config[CONF_THEN], templ, args) cg.add(obj.add_actions(actions)) return obj + + +async def build_callback_automation( + parent: MockObj, + callback_method: str, + args: TemplateArgsType, + config: ConfigType, + forwarder: MockObj | MockObjClass | None = None, +) -> None: + """Build an Automation and register it as a callback on the parent. + + Eliminates the need for a Trigger wrapper object by registering the + automation's trigger() directly as a callback on the parent component. + + Uses template forwarder structs so the compiler deduplicates the operator() + body across all call sites with the same signature. The forwarder must be + pointer-sized (single Automation* field) to fit inline in Callback::ctx_ + and avoid heap allocation. + + :param parent: The component object (e.g., button, sensor). + :param callback_method: Name of the callback method (e.g., "add_on_press_callback"). + :param args: Automation template args as list of (type, name) tuples. + :param config: The automation config dict. + :param forwarder: Optional forwarder type to use instead of the default + TriggerForwarder. Pass any struct type whose aggregate init takes + a single Automation pointer (e.g., TriggerOnTrueForwarder). + """ + arg_types = [arg[0] for arg in args] + templ = cg.TemplateArguments(*arg_types) + obj = cg.new_Pvariable(config[CONF_AUTOMATION_ID], templ) + actions = await build_action_list(config[CONF_THEN], templ, args) + cg.add(obj.add_actions(actions)) + # Use template forwarder structs for deduplication. The compiler generates + # one operator() per forwarder type; different automation pointers are just + # data in the struct. + if forwarder is None: + forwarder = TriggerForwarder.template(templ) + # RawExpression for aggregate init — both forwarder and obj are codegen + # MockObjs (not user input), and there's no Expression type for positional + # aggregate initialization (StructInitializer uses named fields). + cg.add(getattr(parent, callback_method)(cg.RawExpression(f"{forwarder}{{{obj}}}"))) diff --git a/esphome/build_gen/espidf.py b/esphome/build_gen/espidf.py index f45efb82c1..01923baaac 100644 --- a/esphome/build_gen/espidf.py +++ b/esphome/build_gen/espidf.py @@ -53,6 +53,13 @@ def get_project_cmakelists() -> str: variant = get_esp32_variant() idf_target = variant.lower().replace("-", "") + # Extract compile definitions from build flags (-DXXX -> XXX) + compile_defs = [flag for flag in CORE.build_flags if flag.startswith("-D")] + extra_compile_options = "\n".join( + f'idf_build_set_property(COMPILE_OPTIONS "{compile_def}" APPEND)' + for compile_def in compile_defs + ) + return f"""\ # Auto-generated by ESPHome cmake_minimum_required(VERSION 3.16) @@ -61,6 +68,9 @@ set(IDF_TARGET {idf_target}) set(EXTRA_COMPONENT_DIRS ${{CMAKE_SOURCE_DIR}}/src) include($ENV{{IDF_PATH}}/tools/cmake/project.cmake) + +{extra_compile_options} + project({CORE.name}) """ @@ -70,21 +80,17 @@ def get_component_cmakelists(minimal: bool = False) -> str: idf_requires = [] if minimal else (get_available_components() or []) requires_str = " ".join(idf_requires) - # Extract compile definitions from build flags (-DXXX -> XXX) - compile_defs = [flag[2:] for flag in CORE.build_flags if flag.startswith("-D")] - compile_defs_str = "\n ".join(compile_defs) if compile_defs else "" - # Extract compile options (-W flags, excluding linker flags) compile_opts = [ flag for flag in CORE.build_flags if flag.startswith("-W") and not flag.startswith("-Wl,") ] - compile_opts_str = "\n ".join(compile_opts) if compile_opts else "" + compile_opts_str = "\n ".join(sorted(compile_opts)) if compile_opts else "" # Extract linker options (-Wl, flags) link_opts = [flag for flag in CORE.build_flags if flag.startswith("-Wl,")] - link_opts_str = "\n ".join(link_opts) if link_opts else "" + link_opts_str = "\n ".join(sorted(link_opts)) if link_opts else "" return f"""\ # Auto-generated by ESPHome @@ -104,11 +110,6 @@ idf_component_register( # Apply C++ standard target_compile_features(${{COMPONENT_LIB}} PUBLIC cxx_std_20) -# ESPHome compile definitions -target_compile_definitions(${{COMPONENT_LIB}} PUBLIC - {compile_defs_str} -) - # ESPHome compile options target_compile_options(${{COMPONENT_LIB}} PUBLIC {compile_opts_str} diff --git a/esphome/codegen.py b/esphome/codegen.py index 4a2a5975c6..30e3135360 100644 --- a/esphome/codegen.py +++ b/esphome/codegen.py @@ -11,6 +11,7 @@ from esphome.cpp_generator import ( # noqa: F401 ArrayInitializer, Expression, + FlashStringLiteral, LineComment, LogStringLiteral, MockObj, @@ -87,6 +88,7 @@ from esphome.cpp_types import ( # noqa: F401 size_t, std_ns, std_shared_ptr, + std_span, std_string, std_string_ref, std_vector, diff --git a/esphome/components/absolute_humidity/absolute_humidity.cpp b/esphome/components/absolute_humidity/absolute_humidity.cpp index b13fcd519a..3137a59fad 100644 --- a/esphome/components/absolute_humidity/absolute_humidity.cpp +++ b/esphome/components/absolute_humidity/absolute_humidity.cpp @@ -1,22 +1,29 @@ #include "esphome/core/log.h" #include "absolute_humidity.h" -namespace esphome { -namespace absolute_humidity { +namespace esphome::absolute_humidity { -static const char *const TAG = "absolute_humidity.sensor"; +static const char *const TAG{"absolute_humidity.sensor"}; void AbsoluteHumidityComponent::setup() { + this->temperature_sensor_->add_on_state_callback([this](float state) { + this->temperature_ = state; + this->enable_loop(); + }); ESP_LOGD(TAG, " Added callback for temperature '%s'", this->temperature_sensor_->get_name().c_str()); - this->temperature_sensor_->add_on_state_callback([this](float state) { this->temperature_callback_(state); }); + // Get initial value if (this->temperature_sensor_->has_state()) { - this->temperature_callback_(this->temperature_sensor_->get_state()); + this->temperature_ = this->temperature_sensor_->get_state(); } + this->humidity_sensor_->add_on_state_callback([this](float state) { + this->humidity_ = state; + this->enable_loop(); + }); ESP_LOGD(TAG, " Added callback for relative humidity '%s'", this->humidity_sensor_->get_name().c_str()); - this->humidity_sensor_->add_on_state_callback([this](float state) { this->humidity_callback_(state); }); + // Get initial value if (this->humidity_sensor_->has_state()) { - this->humidity_callback_(this->humidity_sensor_->get_state()); + this->humidity_ = this->humidity_sensor_->get_state(); } } @@ -45,17 +52,13 @@ void AbsoluteHumidityComponent::dump_config() { this->temperature_sensor_->get_name().c_str(), this->humidity_sensor_->get_name().c_str()); } -float AbsoluteHumidityComponent::get_setup_priority() const { return setup_priority::DATA; } - void AbsoluteHumidityComponent::loop() { - if (!this->next_update_) { - return; - } - this->next_update_ = false; + // Only run once + this->disable_loop(); // Ensure we have source data - const bool no_temperature = std::isnan(this->temperature_); - const bool no_humidity = std::isnan(this->humidity_); + const bool no_temperature{std::isnan(this->temperature_)}; + const bool no_humidity{std::isnan(this->humidity_)}; if (no_temperature || no_humidity) { if (no_temperature) { ESP_LOGW(TAG, "No valid state from temperature sensor!"); @@ -69,9 +72,9 @@ void AbsoluteHumidityComponent::loop() { } // Convert to desired units - const float temperature_c = this->temperature_; - const float temperature_k = temperature_c + 273.15; - const float hr = this->humidity_ / 100; + const float temperature_c{this->temperature_}; + const float temperature_k{temperature_c + 273.15f}; + const float hr{this->humidity_ / 100.0f}; // Calculate saturation vapor pressure float es; @@ -92,12 +95,9 @@ void AbsoluteHumidityComponent::loop() { } // Calculate absolute humidity - const float absolute_humidity = vapor_density(es, hr, temperature_k); + const float absolute_humidity{vapor_density(es, hr, temperature_k)}; - ESP_LOGD(TAG, - "Saturation vapor pressure %f kPa\n" - "Publishing absolute humidity %f g/m³", - es, absolute_humidity); + ESP_LOGD(TAG, "Saturation vapor pressure %f kPa, absolute humidity %f g/m³", es, absolute_humidity); // Publish absolute humidity this->status_clear_warning(); @@ -108,16 +108,16 @@ void AbsoluteHumidityComponent::loop() { // More accurate than Tetens in normal meteorologic conditions float AbsoluteHumidityComponent::es_buck(float temperature_c) { float a, b, c, d; - if (temperature_c >= 0) { - a = 0.61121; - b = 18.678; - c = 234.5; - d = 257.14; + if (temperature_c >= 0.0f) { + a = 0.61121f; + b = 18.678f; + c = 234.5f; + d = 257.14f; } else { - a = 0.61115; - b = 18.678; - c = 233.7; - d = 279.82; + a = 0.61115f; + b = 18.678f; + c = 233.7f; + d = 279.82f; } return a * expf((b - (temperature_c / c)) * (temperature_c / (d + temperature_c))); } @@ -125,14 +125,14 @@ float AbsoluteHumidityComponent::es_buck(float temperature_c) { // Tetens equation (https://en.wikipedia.org/wiki/Tetens_equation) float AbsoluteHumidityComponent::es_tetens(float temperature_c) { float a, b; - if (temperature_c >= 0) { - a = 17.27; - b = 237.3; + if (temperature_c >= 0.0f) { + a = 17.27f; + b = 237.3f; } else { - a = 21.875; - b = 265.5; + a = 21.875f; + b = 265.5f; } - return 0.61078 * expf((a * temperature_c) / (temperature_c + b)); + return 0.61078f * expf((a * temperature_c) / (temperature_c + b)); } // Wobus equation @@ -151,18 +151,18 @@ float AbsoluteHumidityComponent::es_wobus(float t) { // // Baker, Schlatter 17-MAY-1982 Original version. - const float c0 = +0.99999683e00; - const float c1 = -0.90826951e-02; - const float c2 = +0.78736169e-04; - const float c3 = -0.61117958e-06; - const float c4 = +0.43884187e-08; - const float c5 = -0.29883885e-10; - const float c6 = +0.21874425e-12; - const float c7 = -0.17892321e-14; - const float c8 = +0.11112018e-16; - const float c9 = -0.30994571e-19; - const float p = c0 + t * (c1 + t * (c2 + t * (c3 + t * (c4 + t * (c5 + t * (c6 + t * (c7 + t * (c8 + t * (c9))))))))); - return 0.61078 / pow(p, 8); + constexpr float c0{+0.99999683e+00f}; + constexpr float c1{-0.90826951e-02f}; + constexpr float c2{+0.78736169e-04f}; + constexpr float c3{-0.61117958e-06f}; + constexpr float c4{+0.43884187e-08f}; + constexpr float c5{-0.29883885e-10f}; + constexpr float c6{+0.21874425e-12f}; + constexpr float c7{-0.17892321e-14f}; + constexpr float c8{+0.11112018e-16f}; + constexpr float c9{-0.30994571e-19f}; + const float p{c0 + t * (c1 + t * (c2 + t * (c3 + t * (c4 + t * (c5 + t * (c6 + t * (c7 + t * (c8 + t * (c9)))))))))}; + return 0.61078f / powf(p, 8.0f); } // From https://www.environmentalbiophysics.org/chalk-talk-how-to-calculate-absolute-humidity/ @@ -173,11 +173,10 @@ float AbsoluteHumidityComponent::vapor_density(float es, float hr, float ta) { // hr = relative humidity [0-1] // ta = absolute temperature (K) - const float ea = hr * es * 1000; // vapor pressure of the air (Pa) - const float mw = 18.01528; // molar mass of water (g⋅mol⁻¹) - const float r = 8.31446261815324; // molar gas constant (J⋅K⁻¹) + const float ea{hr * es * 1000.0f}; // vapor pressure of the air (Pa) + const float mw{18.01528f}; // molar mass of water (g⋅mol⁻¹) + const float r{8.31446261815324f}; // molar gas constant (J⋅K⁻¹) return (ea * mw) / (r * ta); } -} // namespace absolute_humidity -} // namespace esphome +} // namespace esphome::absolute_humidity diff --git a/esphome/components/absolute_humidity/absolute_humidity.h b/esphome/components/absolute_humidity/absolute_humidity.h index 9f3b9eab8b..be28d3dc50 100644 --- a/esphome/components/absolute_humidity/absolute_humidity.h +++ b/esphome/components/absolute_humidity/absolute_humidity.h @@ -3,8 +3,7 @@ #include "esphome/core/component.h" #include "esphome/components/sensor/sensor.h" -namespace esphome { -namespace absolute_humidity { +namespace esphome::absolute_humidity { /// Enum listing all implemented saturation vapor pressure equations. enum SaturationVaporPressureEquation { @@ -16,27 +15,15 @@ enum SaturationVaporPressureEquation { /// This class implements calculation of absolute humidity from temperature and relative humidity. class AbsoluteHumidityComponent : public sensor::Sensor, public Component { public: - AbsoluteHumidityComponent() = default; - void set_temperature_sensor(sensor::Sensor *temperature_sensor) { this->temperature_sensor_ = temperature_sensor; } void set_humidity_sensor(sensor::Sensor *humidity_sensor) { this->humidity_sensor_ = humidity_sensor; } void set_equation(SaturationVaporPressureEquation equation) { this->equation_ = equation; } void setup() override; void dump_config() override; - float get_setup_priority() const override; void loop() override; protected: - void temperature_callback_(float state) { - this->next_update_ = true; - this->temperature_ = state; - } - void humidity_callback_(float state) { - this->next_update_ = true; - this->humidity_ = state; - } - /** Buck equation for saturation vapor pressure in kPa. * * @param temperature_c Air temperature in °C. @@ -58,19 +45,15 @@ class AbsoluteHumidityComponent : public sensor::Sensor, public Component { * @param es Saturation vapor pressure in kPa. * @param hr Relative humidity 0 to 1. * @param ta Absolute temperature in K. - * @param heater_duration The duration in ms that the heater should turn on for when measuring. */ static float vapor_density(float es, float hr, float ta); sensor::Sensor *temperature_sensor_{nullptr}; sensor::Sensor *humidity_sensor_{nullptr}; - bool next_update_{false}; - float temperature_{NAN}; float humidity_{NAN}; SaturationVaporPressureEquation equation_; }; -} // namespace absolute_humidity -} // namespace esphome +} // namespace esphome::absolute_humidity diff --git a/esphome/components/ac_dimmer/ac_dimmer.cpp b/esphome/components/ac_dimmer/ac_dimmer.cpp index 1e850a18fe..f731a8c753 100644 --- a/esphome/components/ac_dimmer/ac_dimmer.cpp +++ b/esphome/components/ac_dimmer/ac_dimmer.cpp @@ -199,12 +199,19 @@ void AcDimmer::setup() { setTimer1Callback(&timer_interrupt); #endif #ifdef USE_ESP32 - dimmer_timer = timer_begin(TIMER_FREQUENCY_HZ); - timer_attach_interrupt(dimmer_timer, &AcDimmerDataStore::s_timer_intr); - // For ESP32, we can't use dynamic interval calculation because the timerX functions - // are not callable from ISR (placed in flash storage). - // Here we just use an interrupt firing every 50 µs. - timer_alarm(dimmer_timer, TIMER_INTERVAL_US, true, 0); + if (dimmer_timer == nullptr) { + dimmer_timer = timer_begin(TIMER_FREQUENCY_HZ); + if (dimmer_timer == nullptr) { + ESP_LOGE(TAG, "Failed to create GPTimer for AC dimmer"); + this->mark_failed(); + return; + } + timer_attach_interrupt(dimmer_timer, &AcDimmerDataStore::s_timer_intr); + // For ESP32, we can't use dynamic interval calculation because the timerX functions + // are not callable from ISR (placed in flash storage). + // Here we just use an interrupt firing every 50 µs. + timer_alarm(dimmer_timer, TIMER_INTERVAL_US, true, 0); + } #endif } diff --git a/esphome/components/adc/adc_sensor.h b/esphome/components/adc/adc_sensor.h index 526dd57fd5..cf48ccd9c3 100644 --- a/esphome/components/adc/adc_sensor.h +++ b/esphome/components/adc/adc_sensor.h @@ -22,7 +22,8 @@ namespace adc { #ifdef USE_ESP32 // clang-format off -#if (ESP_IDF_VERSION_MAJOR == 5 && \ +#if ESP_IDF_VERSION_MAJOR >= 6 || \ + (ESP_IDF_VERSION_MAJOR == 5 && \ ((ESP_IDF_VERSION_MINOR == 0 && ESP_IDF_VERSION_PATCH >= 5) || \ (ESP_IDF_VERSION_MINOR == 1 && ESP_IDF_VERSION_PATCH >= 3) || \ (ESP_IDF_VERSION_MINOR >= 2)) \ @@ -68,11 +69,6 @@ class ADCSensor : public sensor::Sensor, public PollingComponent, public voltage /// This method is called during the ESPHome setup process to log the configuration. void dump_config() override; - /// Return the setup priority for this component. - /// Components with higher priority are initialized earlier during setup. - /// @return A float representing the setup priority. - float get_setup_priority() const override; - #ifdef USE_ZEPHYR /// Set the ADC channel to be used by the ADC sensor. /// @param channel Pointer to an adc_dt_spec structure representing the ADC channel. diff --git a/esphome/components/adc/adc_sensor_common.cpp b/esphome/components/adc/adc_sensor_common.cpp index 748c8634b7..c779fd5893 100644 --- a/esphome/components/adc/adc_sensor_common.cpp +++ b/esphome/components/adc/adc_sensor_common.cpp @@ -79,7 +79,5 @@ void ADCSensor::set_sample_count(uint8_t sample_count) { void ADCSensor::set_sampling_mode(SamplingMode sampling_mode) { this->sampling_mode_ = sampling_mode; } -float ADCSensor::get_setup_priority() const { return setup_priority::DATA; } - } // namespace adc } // namespace esphome diff --git a/esphome/components/adc/adc_sensor_esp32.cpp b/esphome/components/adc/adc_sensor_esp32.cpp index ea1263db5f..fc707013a8 100644 --- a/esphome/components/adc/adc_sensor_esp32.cpp +++ b/esphome/components/adc/adc_sensor_esp32.cpp @@ -2,6 +2,7 @@ #include "adc_sensor.h" #include "esphome/core/log.h" +#include namespace esphome { namespace adc { @@ -42,11 +43,11 @@ void ADCSensor::setup() { adc_oneshot_unit_init_cfg_t init_config = {}; // Zero initialize init_config.unit_id = this->adc_unit_; init_config.ulp_mode = ADC_ULP_MODE_DISABLE; -#if USE_ESP32_VARIANT_ESP32C3 || USE_ESP32_VARIANT_ESP32C5 || USE_ESP32_VARIANT_ESP32C6 || \ - USE_ESP32_VARIANT_ESP32C61 || USE_ESP32_VARIANT_ESP32H2 +#if USE_ESP32_VARIANT_ESP32C2 || USE_ESP32_VARIANT_ESP32C3 || USE_ESP32_VARIANT_ESP32C5 || \ + USE_ESP32_VARIANT_ESP32C6 || USE_ESP32_VARIANT_ESP32C61 || USE_ESP32_VARIANT_ESP32H2 init_config.clk_src = ADC_DIGI_CLK_SRC_DEFAULT; -#endif // USE_ESP32_VARIANT_ESP32C3 || USE_ESP32_VARIANT_ESP32C5 || USE_ESP32_VARIANT_ESP32C6 || - // USE_ESP32_VARIANT_ESP32C61 || USE_ESP32_VARIANT_ESP32H2 +#endif // USE_ESP32_VARIANT_ESP32C2 || USE_ESP32_VARIANT_ESP32C3 || USE_ESP32_VARIANT_ESP32C5 || + // USE_ESP32_VARIANT_ESP32C6 || USE_ESP32_VARIANT_ESP32C61 || USE_ESP32_VARIANT_ESP32H2 esp_err_t err = adc_oneshot_new_unit(&init_config, &ADCSensor::shared_adc_handles[this->adc_unit_]); if (err != ESP_OK) { ESP_LOGE(TAG, "Error initializing %s: %d", LOG_STR_ARG(adc_unit_to_str(this->adc_unit_)), err); @@ -76,7 +77,7 @@ void ADCSensor::setup() { #if USE_ESP32_VARIANT_ESP32C3 || USE_ESP32_VARIANT_ESP32C5 || USE_ESP32_VARIANT_ESP32C6 || \ USE_ESP32_VARIANT_ESP32C61 || USE_ESP32_VARIANT_ESP32H2 || USE_ESP32_VARIANT_ESP32P4 || USE_ESP32_VARIANT_ESP32S3 - // RISC-V variants and S3 use curve fitting calibration + // RISC-V variants (except C2) and S3 use curve fitting calibration adc_cali_curve_fitting_config_t cali_config = {}; // Zero initialize first #if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 3, 0) cali_config.chan = this->channel_; @@ -94,14 +95,14 @@ void ADCSensor::setup() { ESP_LOGW(TAG, "Curve fitting calibration failed with error %d, will use uncalibrated readings", err); this->setup_flags_.calibration_complete = false; } -#else // Other ESP32 variants use line fitting calibration +#else // ESP32, ESP32-S2, and ESP32-C2 use line fitting calibration adc_cali_line_fitting_config_t cali_config = { .unit_id = this->adc_unit_, .atten = this->attenuation_, .bitwidth = ADC_BITWIDTH_DEFAULT, -#if !defined(USE_ESP32_VARIANT_ESP32S2) +#if !defined(USE_ESP32_VARIANT_ESP32S2) && !defined(USE_ESP32_VARIANT_ESP32C2) .default_vref = 1100, // Default reference voltage in mV -#endif // !defined(USE_ESP32_VARIANT_ESP32S2) +#endif // !defined(USE_ESP32_VARIANT_ESP32S2) && !defined(USE_ESP32_VARIANT_ESP32C2) }; err = adc_cali_create_scheme_line_fitting(&cali_config, &handle); if (err == ESP_OK) { @@ -112,7 +113,7 @@ void ADCSensor::setup() { ESP_LOGW(TAG, "Line fitting calibration failed with error %d, will use uncalibrated readings", err); this->setup_flags_.calibration_complete = false; } -#endif // USE_ESP32_VARIANT_ESP32C3 || ESP32C5 || ESP32C6 || ESP32C61 || ESP32H2 || ESP32P4 || ESP32S3 +#endif // ESP32C3 || ESP32C5 || ESP32C6 || ESP32C61 || ESP32H2 || ESP32P4 || ESP32S3 } this->setup_flags_.init_complete = true; @@ -189,7 +190,7 @@ float ADCSensor::sample_fixed_attenuation_() { adc_cali_delete_scheme_curve_fitting(this->calibration_handle_); #else // Other ESP32 variants use line fitting calibration adc_cali_delete_scheme_line_fitting(this->calibration_handle_); -#endif // USE_ESP32_VARIANT_ESP32C3 || ESP32C5 || ESP32C6 || ESP32C61 || ESP32H2 || ESP32P4 || ESP32S3 +#endif // ESP32C3 || ESP32C5 || ESP32C6 || ESP32C61 || ESP32H2 || ESP32P4 || ESP32S3 this->calibration_handle_ = nullptr; } } @@ -247,7 +248,7 @@ float ADCSensor::sample_autorange_() { .unit_id = this->adc_unit_, .atten = atten, .bitwidth = ADC_BITWIDTH_DEFAULT, -#if !defined(USE_ESP32_VARIANT_ESP32S2) +#if !defined(USE_ESP32_VARIANT_ESP32S2) && !defined(USE_ESP32_VARIANT_ESP32C2) .default_vref = 1100, #endif }; @@ -346,7 +347,8 @@ float ADCSensor::sample_autorange_() { ESP_LOGVV(TAG, "Autorange summary:"); ESP_LOGVV(TAG, " Raw readings: 12db=%d, 6db=%d, 2.5db=%d, 0db=%d", raw12, raw6, raw2, raw0); ESP_LOGVV(TAG, " Voltages: 12db=%.6f, 6db=%.6f, 2.5db=%.6f, 0db=%.6f", mv12, mv6, mv2, mv0); - ESP_LOGVV(TAG, " Coefficients: c12=%u, c6=%u, c2=%u, c0=%u, sum=%u", c12, c6, c2, c0, csum); + ESP_LOGVV(TAG, " Coefficients: c12=%" PRIu32 ", c6=%" PRIu32 ", c2=%" PRIu32 ", c0=%" PRIu32 ", sum=%" PRIu32, c12, + c6, c2, c0, csum); if (csum == 0) { ESP_LOGE(TAG, "Invalid weight sum in autorange calculation"); @@ -354,8 +356,10 @@ float ADCSensor::sample_autorange_() { } const float final_result = (mv12 * c12 + mv6 * c6 + mv2 * c2 + mv0 * c0) / csum; - ESP_LOGV(TAG, "Autorange final: (%.6f*%u + %.6f*%u + %.6f*%u + %.6f*%u)/%u = %.6fV", mv12, c12, mv6, c6, mv2, c2, mv0, - c0, csum, final_result); + ESP_LOGV(TAG, + "Autorange final: (%.6f*%" PRIu32 " + %.6f*%" PRIu32 " + %.6f*%" PRIu32 " + %.6f*%" PRIu32 ")/%" PRIu32 + " = %.6fV", + mv12, c12, mv6, c6, mv2, c2, mv0, c0, csum, final_result); return final_result; } diff --git a/esphome/components/adc/adc_sensor_rp2040.cpp b/esphome/components/adc/adc_sensor_rp2040.cpp index 8496e0f41e..a79707e234 100644 --- a/esphome/components/adc/adc_sensor_rp2040.cpp +++ b/esphome/components/adc/adc_sensor_rp2040.cpp @@ -8,6 +8,13 @@ #endif // CYW43_USES_VSYS_PIN #include +// PICO_VSYS_PIN is defined in pico-sdk board headers (e.g. boards/pico2.h), +// but the Arduino framework's config_autogen.h includes a generic board header +// that doesn't define it. Provide the standard value (pin 29) as a fallback. +#ifndef PICO_VSYS_PIN +#define PICO_VSYS_PIN 29 // NOLINT(cppcoreguidelines-macro-usage) +#endif + namespace esphome { namespace adc { diff --git a/esphome/components/adc/sensor.py b/esphome/components/adc/sensor.py index 64dd22b0c3..bab2762f00 100644 --- a/esphome/components/adc/sensor.py +++ b/esphome/components/adc/sensor.py @@ -2,7 +2,7 @@ import logging import esphome.codegen as cg 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.zephyr import ( zephyr_add_overlay, @@ -118,6 +118,9 @@ async def to_code(config): cg.add(var.set_sampling_mode(config[CONF_SAMPLING_MODE])) 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 == "auto": cg.add(var.set_autorange(cg.global_ns.true)) diff --git a/esphome/components/adc128s102/sensor/adc128s102_sensor.cpp b/esphome/components/adc128s102/sensor/adc128s102_sensor.cpp index 03ce31d3cb..800b2d5261 100644 --- a/esphome/components/adc128s102/sensor/adc128s102_sensor.cpp +++ b/esphome/components/adc128s102/sensor/adc128s102_sensor.cpp @@ -9,8 +9,6 @@ static const char *const TAG = "adc128s102.sensor"; ADC128S102Sensor::ADC128S102Sensor(uint8_t channel) : channel_(channel) {} -float ADC128S102Sensor::get_setup_priority() const { return setup_priority::DATA; } - void ADC128S102Sensor::dump_config() { LOG_SENSOR("", "ADC128S102 Sensor", this); ESP_LOGCONFIG(TAG, " Pin: %u", this->channel_); diff --git a/esphome/components/adc128s102/sensor/adc128s102_sensor.h b/esphome/components/adc128s102/sensor/adc128s102_sensor.h index 234500c2f4..5e6fc74e9c 100644 --- a/esphome/components/adc128s102/sensor/adc128s102_sensor.h +++ b/esphome/components/adc128s102/sensor/adc128s102_sensor.h @@ -19,7 +19,6 @@ class ADC128S102Sensor : public PollingComponent, void update() override; void dump_config() override; - float get_setup_priority() const override; float sample() override; protected: diff --git a/esphome/components/addressable_light/addressable_light_display.cpp b/esphome/components/addressable_light/addressable_light_display.cpp index 16fab15b17..329620bcf0 100644 --- a/esphome/components/addressable_light/addressable_light_display.cpp +++ b/esphome/components/addressable_light/addressable_light_display.cpp @@ -58,7 +58,10 @@ void HOT AddressableLightDisplay::draw_absolute_pixel_internal(int x, int y, Col if (this->pixel_mapper_f_.has_value()) { // Params are passed by reference, so they may be modified in call. - this->addressable_light_buffer_[(*this->pixel_mapper_f_)(x, y)] = color; + int index = (*this->pixel_mapper_f_)(x, y); + if (index < 0 || static_cast(index) >= this->addressable_light_buffer_.size()) + return; + this->addressable_light_buffer_[index] = color; } else { this->addressable_light_buffer_[y * this->get_width_internal() + x] = color; } diff --git a/esphome/components/addressable_light/addressable_light_display.h b/esphome/components/addressable_light/addressable_light_display.h index 53f8604b7d..d9b8680547 100644 --- a/esphome/components/addressable_light/addressable_light_display.h +++ b/esphome/components/addressable_light/addressable_light_display.h @@ -33,7 +33,7 @@ class AddressableLightDisplay : public display::DisplayBuffer { // - Save the current effect index. this->last_effect_index_ = light_state_->get_current_effect_index(); // - Disable any current effect. - light_state_->make_call().set_effect(0).perform(); + light_state_->make_call().set_effect(uint32_t{0}).perform(); } } enabled_ = enabled; diff --git a/esphome/components/ade7880/ade7880.cpp b/esphome/components/ade7880/ade7880.cpp index f6a15190cd..8fb3e55b91 100644 --- a/esphome/components/ade7880/ade7880.cpp +++ b/esphome/components/ade7880/ade7880.cpp @@ -121,7 +121,7 @@ void ADE7880::update() { this->update_sensor_from_s32_register16_(chan->forward_active_energy, AFWATTHR, [&chan](float val) { return chan->forward_active_energy_total += val / 14400.0f; }); - this->update_sensor_from_s32_register16_(chan->reverse_active_energy, AFWATTHR, [&chan](float val) { + this->update_sensor_from_s32_register16_(chan->reverse_active_energy, ARWATTHR, [&chan](float val) { return chan->reverse_active_energy_total += val / 14400.0f; }); } @@ -137,7 +137,7 @@ void ADE7880::update() { this->update_sensor_from_s32_register16_(chan->forward_active_energy, BFWATTHR, [&chan](float val) { return chan->forward_active_energy_total += val / 14400.0f; }); - this->update_sensor_from_s32_register16_(chan->reverse_active_energy, BFWATTHR, [&chan](float val) { + this->update_sensor_from_s32_register16_(chan->reverse_active_energy, BRWATTHR, [&chan](float val) { return chan->reverse_active_energy_total += val / 14400.0f; }); } @@ -153,7 +153,7 @@ void ADE7880::update() { this->update_sensor_from_s32_register16_(chan->forward_active_energy, CFWATTHR, [&chan](float val) { return chan->forward_active_energy_total += val / 14400.0f; }); - this->update_sensor_from_s32_register16_(chan->reverse_active_energy, CFWATTHR, [&chan](float val) { + this->update_sensor_from_s32_register16_(chan->reverse_active_energy, CRWATTHR, [&chan](float val) { return chan->reverse_active_energy_total += val / 14400.0f; }); } diff --git a/esphome/components/ade7880/ade7880_registers.h b/esphome/components/ade7880/ade7880_registers.h index 8b5b68abb0..9fd8ca3bf5 100644 --- a/esphome/components/ade7880/ade7880_registers.h +++ b/esphome/components/ade7880/ade7880_registers.h @@ -85,6 +85,9 @@ constexpr uint16_t CWATTHR = 0xE402; constexpr uint16_t AFWATTHR = 0xE403; constexpr uint16_t BFWATTHR = 0xE404; constexpr uint16_t CFWATTHR = 0xE405; +constexpr uint16_t ARWATTHR = 0xE406; +constexpr uint16_t BRWATTHR = 0xE407; +constexpr uint16_t CRWATTHR = 0xE408; constexpr uint16_t AFVARHR = 0xE409; constexpr uint16_t BFVARHR = 0xE40A; constexpr uint16_t CFVARHR = 0xE40B; diff --git a/esphome/components/ads1115/ads1115.cpp b/esphome/components/ads1115/ads1115.cpp index f4996cd3b1..d493a6a6d3 100644 --- a/esphome/components/ads1115/ads1115.cpp +++ b/esphome/components/ads1115/ads1115.cpp @@ -173,19 +173,8 @@ float ADS1115Component::request_measurement(ADS1115Multiplexer multiplexer, ADS1 } if (resolution == ADS1015_12_BITS) { - bool negative = (raw_conversion >> 15) == 1; - - // shift raw_conversion as it's only 12-bits, left justified - raw_conversion = raw_conversion >> (16 - ADS1015_12_BITS); - - // check if number was negative in order to keep the sign - if (negative) { - // the number was negative - // 1) set the negative bit back - raw_conversion |= 0x8000; - // 2) reset the former (shifted) negative bit - raw_conversion &= 0xF7FF; - } + // ADS1015 returns 12-bit value left-justified in 16 bits; shift right and sign-extend + raw_conversion = static_cast(static_cast(raw_conversion) >> (16 - ADS1015_12_BITS)); } auto signed_conversion = static_cast(raw_conversion); diff --git a/esphome/components/ags10/sensor.py b/esphome/components/ags10/sensor.py index 8f0f372951..4cfa9e67ec 100644 --- a/esphome/components/ags10/sensor.py +++ b/esphome/components/ags10/sensor.py @@ -92,6 +92,7 @@ AGS10_NEW_I2C_ADDRESS_SCHEMA = cv.maybe_simple_value( "ags10.new_i2c_address", AGS10NewI2cAddressAction, AGS10_NEW_I2C_ADDRESS_SCHEMA, + synchronous=True, ) async def ags10newi2caddress_to_code(config, action_id, template_arg, args): var = cg.new_Pvariable(action_id, template_arg) @@ -121,6 +122,7 @@ AGS10_SET_ZERO_POINT_SCHEMA = cv.Schema( "ags10.set_zero_point", AGS10SetZeroPointAction, AGS10_SET_ZERO_POINT_SCHEMA, + synchronous=True, ) async def ags10setzeropoint_to_code(config, action_id, template_arg, args): var = cg.new_Pvariable(action_id, template_arg) diff --git a/esphome/components/aht10/aht10.cpp b/esphome/components/aht10/aht10.cpp index 03d9d9cd9e..1b1f8335cc 100644 --- a/esphome/components/aht10/aht10.cpp +++ b/esphome/components/aht10/aht10.cpp @@ -150,8 +150,6 @@ void AHT10Component::update() { this->restart_read_(); } -float AHT10Component::get_setup_priority() const { return setup_priority::DATA; } - void AHT10Component::dump_config() { ESP_LOGCONFIG(TAG, "AHT10:"); LOG_I2C_DEVICE(this); diff --git a/esphome/components/aht10/aht10.h b/esphome/components/aht10/aht10.h index a3320c77e0..ce9cd963ad 100644 --- a/esphome/components/aht10/aht10.h +++ b/esphome/components/aht10/aht10.h @@ -16,7 +16,6 @@ class AHT10Component : public PollingComponent, public i2c::I2CDevice { void setup() override; void update() override; void dump_config() override; - float get_setup_priority() const override; void set_variant(AHT10Variant variant) { this->variant_ = variant; } void set_temperature_sensor(sensor::Sensor *temperature_sensor) { temperature_sensor_ = temperature_sensor; } diff --git a/esphome/components/aic3204/audio_dac.py b/esphome/components/aic3204/audio_dac.py index da7a54df54..a644638f69 100644 --- a/esphome/components/aic3204/audio_dac.py +++ b/esphome/components/aic3204/audio_dac.py @@ -34,7 +34,10 @@ SET_AUTO_MUTE_ACTION_SCHEMA = cv.maybe_simple_value( @automation.register_action( - "aic3204.set_auto_mute_mode", SetAutoMuteAction, SET_AUTO_MUTE_ACTION_SCHEMA + "aic3204.set_auto_mute_mode", + SetAutoMuteAction, + SET_AUTO_MUTE_ACTION_SCHEMA, + synchronous=True, ) async def aic3204_set_volume_to_code(config, action_id, template_arg, args): paren = await cg.get_variable(config[CONF_ID]) diff --git a/esphome/components/alarm_control_panel/__init__.py b/esphome/components/alarm_control_panel/__init__.py index b1e2252ce7..4ee073a15b 100644 --- a/esphome/components/alarm_control_panel/__init__.py +++ b/esphome/components/alarm_control_panel/__init__.py @@ -10,7 +10,6 @@ from esphome.const import ( CONF_ID, CONF_MQTT_ID, CONF_ON_STATE, - CONF_TRIGGER_ID, CONF_WEB_SERVER, ) from esphome.core import CORE, CoroPriority, coroutine_with_priority @@ -34,39 +33,9 @@ CONF_ON_READY = "on_ready" alarm_control_panel_ns = cg.esphome_ns.namespace("alarm_control_panel") AlarmControlPanel = alarm_control_panel_ns.class_("AlarmControlPanel", cg.EntityBase) -StateTrigger = alarm_control_panel_ns.class_( - "StateTrigger", automation.Trigger.template() -) -TriggeredTrigger = alarm_control_panel_ns.class_( - "TriggeredTrigger", automation.Trigger.template() -) -ClearedTrigger = alarm_control_panel_ns.class_( - "ClearedTrigger", automation.Trigger.template() -) -ArmingTrigger = alarm_control_panel_ns.class_( - "ArmingTrigger", automation.Trigger.template() -) -PendingTrigger = alarm_control_panel_ns.class_( - "PendingTrigger", automation.Trigger.template() -) -ArmedHomeTrigger = alarm_control_panel_ns.class_( - "ArmedHomeTrigger", automation.Trigger.template() -) -ArmedNightTrigger = alarm_control_panel_ns.class_( - "ArmedNightTrigger", automation.Trigger.template() -) -ArmedAwayTrigger = alarm_control_panel_ns.class_( - "ArmedAwayTrigger", automation.Trigger.template() -) -DisarmedTrigger = alarm_control_panel_ns.class_( - "DisarmedTrigger", automation.Trigger.template() -) -ChimeTrigger = alarm_control_panel_ns.class_( - "ChimeTrigger", automation.Trigger.template() -) -ReadyTrigger = alarm_control_panel_ns.class_( - "ReadyTrigger", automation.Trigger.template() -) +StateAnyForwarder = alarm_control_panel_ns.class_("StateAnyForwarder") +StateEnterForwarder = alarm_control_panel_ns.class_("StateEnterForwarder") +AlarmControlPanelState = alarm_control_panel_ns.enum("AlarmControlPanelState") ArmAwayAction = alarm_control_panel_ns.class_("ArmAwayAction", automation.Action) ArmHomeAction = alarm_control_panel_ns.class_("ArmHomeAction", automation.Action) @@ -89,61 +58,17 @@ _ALARM_CONTROL_PANEL_SCHEMA = ( cv.OnlyWith(CONF_MQTT_ID, "mqtt"): cv.declare_id( mqtt.MQTTAlarmControlPanelComponent ), - cv.Optional(CONF_ON_STATE): automation.validate_automation( - { - cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(StateTrigger), - } - ), - cv.Optional(CONF_ON_TRIGGERED): automation.validate_automation( - { - cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(TriggeredTrigger), - } - ), - cv.Optional(CONF_ON_ARMING): automation.validate_automation( - { - cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(ArmingTrigger), - } - ), - cv.Optional(CONF_ON_PENDING): automation.validate_automation( - { - cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(PendingTrigger), - } - ), - cv.Optional(CONF_ON_ARMED_HOME): automation.validate_automation( - { - cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(ArmedHomeTrigger), - } - ), - cv.Optional(CONF_ON_ARMED_NIGHT): automation.validate_automation( - { - cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(ArmedNightTrigger), - } - ), - cv.Optional(CONF_ON_ARMED_AWAY): automation.validate_automation( - { - cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(ArmedAwayTrigger), - } - ), - cv.Optional(CONF_ON_DISARMED): automation.validate_automation( - { - cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(DisarmedTrigger), - } - ), - cv.Optional(CONF_ON_CLEARED): automation.validate_automation( - { - cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(ClearedTrigger), - } - ), - cv.Optional(CONF_ON_CHIME): automation.validate_automation( - { - cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(ChimeTrigger), - } - ), - cv.Optional(CONF_ON_READY): automation.validate_automation( - { - cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(ReadyTrigger), - } - ), + cv.Optional(CONF_ON_STATE): automation.validate_automation({}), + cv.Optional(CONF_ON_TRIGGERED): automation.validate_automation({}), + cv.Optional(CONF_ON_ARMING): automation.validate_automation({}), + cv.Optional(CONF_ON_PENDING): automation.validate_automation({}), + cv.Optional(CONF_ON_ARMED_HOME): automation.validate_automation({}), + cv.Optional(CONF_ON_ARMED_NIGHT): automation.validate_automation({}), + cv.Optional(CONF_ON_ARMED_AWAY): automation.validate_automation({}), + cv.Optional(CONF_ON_DISARMED): automation.validate_automation({}), + cv.Optional(CONF_ON_CLEARED): automation.validate_automation({}), + cv.Optional(CONF_ON_CHIME): automation.validate_automation({}), + cv.Optional(CONF_ON_READY): automation.validate_automation({}), } ) ) @@ -186,41 +111,42 @@ ALARM_CONTROL_PANEL_CONDITION_SCHEMA = maybe_simple_id( ) +@setup_entity("alarm_control_panel") async def setup_alarm_control_panel_core_(var, config): - await setup_entity(var, config, "alarm_control_panel") for conf in config.get(CONF_ON_STATE, []): - trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) - await automation.build_automation(trigger, [], conf) - for conf in config.get(CONF_ON_TRIGGERED, []): - trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) - await automation.build_automation(trigger, [], conf) - for conf in config.get(CONF_ON_ARMING, []): - trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) - await automation.build_automation(trigger, [], conf) - for conf in config.get(CONF_ON_PENDING, []): - trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) - await automation.build_automation(trigger, [], conf) - for conf in config.get(CONF_ON_ARMED_HOME, []): - trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) - await automation.build_automation(trigger, [], conf) - for conf in config.get(CONF_ON_ARMED_NIGHT, []): - trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) - await automation.build_automation(trigger, [], conf) - for conf in config.get(CONF_ON_ARMED_AWAY, []): - trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) - await automation.build_automation(trigger, [], conf) - for conf in config.get(CONF_ON_DISARMED, []): - trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) - await automation.build_automation(trigger, [], conf) + await automation.build_callback_automation( + var, "add_on_state_callback", [], conf, forwarder=StateAnyForwarder + ) + _STATE_ENTER_MAP = { + CONF_ON_TRIGGERED: AlarmControlPanelState.ACP_STATE_TRIGGERED, + CONF_ON_ARMING: AlarmControlPanelState.ACP_STATE_ARMING, + CONF_ON_PENDING: AlarmControlPanelState.ACP_STATE_PENDING, + CONF_ON_ARMED_HOME: AlarmControlPanelState.ACP_STATE_ARMED_HOME, + CONF_ON_ARMED_NIGHT: AlarmControlPanelState.ACP_STATE_ARMED_NIGHT, + CONF_ON_ARMED_AWAY: AlarmControlPanelState.ACP_STATE_ARMED_AWAY, + CONF_ON_DISARMED: AlarmControlPanelState.ACP_STATE_DISARMED, + } + for conf_key, state_enum in _STATE_ENTER_MAP.items(): + for conf in config.get(conf_key, []): + await automation.build_callback_automation( + var, + "add_on_state_callback", + [], + conf, + forwarder=StateEnterForwarder.template(state_enum), + ) for conf in config.get(CONF_ON_CLEARED, []): - trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) - await automation.build_automation(trigger, [], conf) + await automation.build_callback_automation( + var, "add_on_cleared_callback", [], conf + ) for conf in config.get(CONF_ON_CHIME, []): - trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) - await automation.build_automation(trigger, [], conf) + await automation.build_callback_automation( + var, "add_on_chime_callback", [], conf + ) for conf in config.get(CONF_ON_READY, []): - trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) - await automation.build_automation(trigger, [], conf) + await automation.build_callback_automation( + var, "add_on_ready_callback", [], conf + ) if web_server_config := config.get(CONF_WEB_SERVER): await web_server.add_entity_config(var, web_server_config) if mqtt_id := config.get(CONF_MQTT_ID): @@ -243,7 +169,10 @@ async def new_alarm_control_panel(config, *args): @automation.register_action( - "alarm_control_panel.arm_away", ArmAwayAction, ALARM_CONTROL_PANEL_ACTION_SCHEMA + "alarm_control_panel.arm_away", + ArmAwayAction, + ALARM_CONTROL_PANEL_ACTION_SCHEMA, + synchronous=True, ) async def alarm_action_arm_away_to_code(config, action_id, template_arg, args): paren = await cg.get_variable(config[CONF_ID]) @@ -255,7 +184,10 @@ async def alarm_action_arm_away_to_code(config, action_id, template_arg, args): @automation.register_action( - "alarm_control_panel.arm_home", ArmHomeAction, ALARM_CONTROL_PANEL_ACTION_SCHEMA + "alarm_control_panel.arm_home", + ArmHomeAction, + ALARM_CONTROL_PANEL_ACTION_SCHEMA, + synchronous=True, ) async def alarm_action_arm_home_to_code(config, action_id, template_arg, args): paren = await cg.get_variable(config[CONF_ID]) @@ -267,7 +199,10 @@ async def alarm_action_arm_home_to_code(config, action_id, template_arg, args): @automation.register_action( - "alarm_control_panel.arm_night", ArmNightAction, ALARM_CONTROL_PANEL_ACTION_SCHEMA + "alarm_control_panel.arm_night", + ArmNightAction, + ALARM_CONTROL_PANEL_ACTION_SCHEMA, + synchronous=True, ) async def alarm_action_arm_night_to_code(config, action_id, template_arg, args): paren = await cg.get_variable(config[CONF_ID]) @@ -279,7 +214,10 @@ async def alarm_action_arm_night_to_code(config, action_id, template_arg, args): @automation.register_action( - "alarm_control_panel.disarm", DisarmAction, ALARM_CONTROL_PANEL_ACTION_SCHEMA + "alarm_control_panel.disarm", + DisarmAction, + ALARM_CONTROL_PANEL_ACTION_SCHEMA, + synchronous=True, ) async def alarm_action_disarm_to_code(config, action_id, template_arg, args): paren = await cg.get_variable(config[CONF_ID]) @@ -291,7 +229,10 @@ async def alarm_action_disarm_to_code(config, action_id, template_arg, args): @automation.register_action( - "alarm_control_panel.pending", PendingAction, ALARM_CONTROL_PANEL_ACTION_SCHEMA + "alarm_control_panel.pending", + PendingAction, + ALARM_CONTROL_PANEL_ACTION_SCHEMA, + synchronous=True, ) async def alarm_action_pending_to_code(config, action_id, template_arg, args): paren = await cg.get_variable(config[CONF_ID]) @@ -299,7 +240,10 @@ async def alarm_action_pending_to_code(config, action_id, template_arg, args): @automation.register_action( - "alarm_control_panel.triggered", TriggeredAction, ALARM_CONTROL_PANEL_ACTION_SCHEMA + "alarm_control_panel.triggered", + TriggeredAction, + ALARM_CONTROL_PANEL_ACTION_SCHEMA, + synchronous=True, ) async def alarm_action_trigger_to_code(config, action_id, template_arg, args): paren = await cg.get_variable(config[CONF_ID]) @@ -307,7 +251,10 @@ async def alarm_action_trigger_to_code(config, action_id, template_arg, args): @automation.register_action( - "alarm_control_panel.chime", ChimeAction, ALARM_CONTROL_PANEL_ACTION_SCHEMA + "alarm_control_panel.chime", + ChimeAction, + ALARM_CONTROL_PANEL_ACTION_SCHEMA, + synchronous=True, ) async def alarm_action_chime_to_code(config, action_id, template_arg, args): paren = await cg.get_variable(config[CONF_ID]) @@ -315,7 +262,10 @@ async def alarm_action_chime_to_code(config, action_id, template_arg, args): @automation.register_action( - "alarm_control_panel.ready", ReadyAction, ALARM_CONTROL_PANEL_ACTION_SCHEMA + "alarm_control_panel.ready", + ReadyAction, + ALARM_CONTROL_PANEL_ACTION_SCHEMA, + synchronous=True, ) @automation.register_condition( "alarm_control_panel.ready", diff --git a/esphome/components/alarm_control_panel/alarm_control_panel.cpp b/esphome/components/alarm_control_panel/alarm_control_panel.cpp index ab0a780cef..fc72c13ce3 100644 --- a/esphome/components/alarm_control_panel/alarm_control_panel.cpp +++ b/esphome/components/alarm_control_panel/alarm_control_panel.cpp @@ -31,12 +31,12 @@ void AlarmControlPanel::publish_state(AlarmControlPanelState state) { this->last_update_ = millis(); if (state != this->current_state_) { auto prev_state = this->current_state_; - ESP_LOGD(TAG, "'%s' >> %s (was %s)", this->get_name().c_str(), + ESP_LOGV(TAG, "'%s' >> %s (was %s)", this->get_name().c_str(), LOG_STR_ARG(alarm_control_panel_state_to_string(state)), LOG_STR_ARG(alarm_control_panel_state_to_string(prev_state))); this->current_state_ = state; - // Single state callback - triggers check get_state() for specific states - this->state_callback_.call(); + // Single state callback - listeners receive the new state as an argument + this->state_callback_.call(state); #if defined(USE_ALARM_CONTROL_PANEL) && defined(USE_CONTROLLER_REGISTRY) ControllerRegistry::notify_alarm_control_panel_update(this); #endif @@ -51,22 +51,6 @@ void AlarmControlPanel::publish_state(AlarmControlPanelState state) { } } -void AlarmControlPanel::add_on_state_callback(std::function &&callback) { - this->state_callback_.add(std::move(callback)); -} - -void AlarmControlPanel::add_on_cleared_callback(std::function &&callback) { - this->cleared_callback_.add(std::move(callback)); -} - -void AlarmControlPanel::add_on_chime_callback(std::function &&callback) { - this->chime_callback_.add(std::move(callback)); -} - -void AlarmControlPanel::add_on_ready_callback(std::function &&callback) { - this->ready_callback_.add(std::move(callback)); -} - void AlarmControlPanel::arm_with_code_(AlarmControlPanelCall &(AlarmControlPanelCall::*arm_method)(), const char *code) { auto call = this->make_call(); diff --git a/esphome/components/alarm_control_panel/alarm_control_panel.h b/esphome/components/alarm_control_panel/alarm_control_panel.h index e8dc197e26..e748b8621b 100644 --- a/esphome/components/alarm_control_panel/alarm_control_panel.h +++ b/esphome/components/alarm_control_panel/alarm_control_panel.h @@ -37,25 +37,24 @@ class AlarmControlPanel : public EntityBase { * * @param callback The callback function */ - void add_on_state_callback(std::function &&callback); + template void add_on_state_callback(F &&callback) { + this->state_callback_.add(std::forward(callback)); + } - /** Add a callback for when the state of the alarm_control_panel clears from triggered - * - * @param callback The callback function - */ - void add_on_cleared_callback(std::function &&callback); + /** Add a callback for when the state of the alarm_control_panel clears from triggered. */ + template void add_on_cleared_callback(F &&callback) { + this->cleared_callback_.add(std::forward(callback)); + } - /** Add a callback for when a chime zone goes from closed to open - * - * @param callback The callback function - */ - void add_on_chime_callback(std::function &&callback); + /** Add a callback for when a chime zone goes from closed to open. */ + template void add_on_chime_callback(F &&callback) { + this->chime_callback_.add(std::forward(callback)); + } - /** Add a callback for when a ready state changes - * - * @param callback The callback function - */ - void add_on_ready_callback(std::function &&callback); + /** Add a callback for when a ready state changes. */ + template void add_on_ready_callback(F &&callback) { + this->ready_callback_.add(std::forward(callback)); + } /** A numeric representation of the supported features as per HomeAssistant * @@ -146,8 +145,8 @@ class AlarmControlPanel : public EntityBase { uint32_t last_update_; // the call control function virtual void control(const AlarmControlPanelCall &call) = 0; - // state callback - triggers check get_state() for specific state - LazyCallbackManager state_callback_{}; + // state callback - passes the new state to listeners + LazyCallbackManager state_callback_{}; // clear callback - fires when leaving TRIGGERED state LazyCallbackManager cleared_callback_{}; // chime callback diff --git a/esphome/components/alarm_control_panel/alarm_control_panel_call.cpp b/esphome/components/alarm_control_panel/alarm_control_panel_call.cpp index ba58ee3904..0c43cd555a 100644 --- a/esphome/components/alarm_control_panel/alarm_control_panel_call.cpp +++ b/esphome/components/alarm_control_panel/alarm_control_panel_call.cpp @@ -12,7 +12,14 @@ AlarmControlPanelCall::AlarmControlPanelCall(AlarmControlPanel *parent) : parent AlarmControlPanelCall &AlarmControlPanelCall::set_code(const char *code) { if (code != nullptr) { - this->code_ = std::string(code); + return this->set_code(code, strlen(code)); + } + return *this; +} + +AlarmControlPanelCall &AlarmControlPanelCall::set_code(const char *code, size_t len) { + if (code != nullptr) { + this->code_ = std::string(code, len); } return *this; } diff --git a/esphome/components/alarm_control_panel/alarm_control_panel_call.h b/esphome/components/alarm_control_panel/alarm_control_panel_call.h index 58764ea166..6e39a0a413 100644 --- a/esphome/components/alarm_control_panel/alarm_control_panel_call.h +++ b/esphome/components/alarm_control_panel/alarm_control_panel_call.h @@ -15,7 +15,8 @@ class AlarmControlPanelCall { AlarmControlPanelCall(AlarmControlPanel *parent); AlarmControlPanelCall &set_code(const char *code); - AlarmControlPanelCall &set_code(const std::string &code) { return this->set_code(code.c_str()); } + AlarmControlPanelCall &set_code(const char *code, size_t len); + AlarmControlPanelCall &set_code(const std::string &code) { return this->set_code(code.c_str(), code.size()); } AlarmControlPanelCall &arm_away(); AlarmControlPanelCall &arm_home(); AlarmControlPanelCall &arm_night(); diff --git a/esphome/components/alarm_control_panel/alarm_control_panel_state.cpp b/esphome/components/alarm_control_panel/alarm_control_panel_state.cpp index 862c620497..b8d246c861 100644 --- a/esphome/components/alarm_control_panel/alarm_control_panel_state.cpp +++ b/esphome/components/alarm_control_panel/alarm_control_panel_state.cpp @@ -1,32 +1,15 @@ #include "alarm_control_panel_state.h" +#include "esphome/core/progmem.h" namespace esphome::alarm_control_panel { +// Alarm control panel state strings indexed by AlarmControlPanelState enum (0-9) +PROGMEM_STRING_TABLE(AlarmControlPanelStateStrings, "DISARMED", "ARMED_HOME", "ARMED_AWAY", "ARMED_NIGHT", + "ARMED_VACATION", "ARMED_CUSTOM_BYPASS", "PENDING", "ARMING", "DISARMING", "TRIGGERED", "UNKNOWN"); + const LogString *alarm_control_panel_state_to_string(AlarmControlPanelState state) { - switch (state) { - case ACP_STATE_DISARMED: - return LOG_STR("DISARMED"); - case ACP_STATE_ARMED_HOME: - return LOG_STR("ARMED_HOME"); - case ACP_STATE_ARMED_AWAY: - return LOG_STR("ARMED_AWAY"); - case ACP_STATE_ARMED_NIGHT: - return LOG_STR("ARMED_NIGHT"); - case ACP_STATE_ARMED_VACATION: - return LOG_STR("ARMED_VACATION"); - case ACP_STATE_ARMED_CUSTOM_BYPASS: - return LOG_STR("ARMED_CUSTOM_BYPASS"); - case ACP_STATE_PENDING: - return LOG_STR("PENDING"); - case ACP_STATE_ARMING: - return LOG_STR("ARMING"); - case ACP_STATE_DISARMING: - return LOG_STR("DISARMING"); - case ACP_STATE_TRIGGERED: - return LOG_STR("TRIGGERED"); - default: - return LOG_STR("UNKNOWN"); - } + return AlarmControlPanelStateStrings::get_log_str(static_cast(state), + AlarmControlPanelStateStrings::LAST_INDEX); } } // namespace esphome::alarm_control_panel diff --git a/esphome/components/alarm_control_panel/automation.h b/esphome/components/alarm_control_panel/automation.h index 4ff34de0d5..022d2650d2 100644 --- a/esphome/components/alarm_control_panel/automation.h +++ b/esphome/components/alarm_control_panel/automation.h @@ -5,60 +5,27 @@ namespace esphome::alarm_control_panel { -/// Trigger on any state change -class StateTrigger : public Trigger<> { - public: - explicit StateTrigger(AlarmControlPanel *alarm_control_panel) { - alarm_control_panel->add_on_state_callback([this]() { this->trigger(); }); +/// Callback forwarder that triggers an Automation<> on any state change. +/// Pointer-sized (single Automation* field) to fit inline in Callback::ctx_. +struct StateAnyForwarder { + Automation<> *automation; + void operator()(AlarmControlPanelState /*state*/) const { this->automation->trigger(); } +}; + +/// Callback forwarder that triggers an Automation<> only when the alarm enters a specific state. +/// Pointer-sized (single Automation* field) to fit inline in Callback::ctx_. +template struct StateEnterForwarder { + Automation<> *automation; + void operator()(AlarmControlPanelState state) const { + if (state == State) + this->automation->trigger(); } }; -/// Template trigger that fires when entering a specific state -template class StateEnterTrigger : public Trigger<> { - public: - explicit StateEnterTrigger(AlarmControlPanel *alarm_control_panel) : alarm_control_panel_(alarm_control_panel) { - alarm_control_panel->add_on_state_callback([this]() { - if (this->alarm_control_panel_->get_state() == State) - this->trigger(); - }); - } - - protected: - AlarmControlPanel *alarm_control_panel_; -}; - -// Type aliases for state-specific triggers -using TriggeredTrigger = StateEnterTrigger; -using ArmingTrigger = StateEnterTrigger; -using PendingTrigger = StateEnterTrigger; -using ArmedHomeTrigger = StateEnterTrigger; -using ArmedNightTrigger = StateEnterTrigger; -using ArmedAwayTrigger = StateEnterTrigger; -using DisarmedTrigger = StateEnterTrigger; - -/// Trigger when leaving TRIGGERED state (alarm cleared) -class ClearedTrigger : public Trigger<> { - public: - explicit ClearedTrigger(AlarmControlPanel *alarm_control_panel) { - alarm_control_panel->add_on_cleared_callback([this]() { this->trigger(); }); - } -}; - -/// Trigger on chime event (zone opened while disarmed) -class ChimeTrigger : public Trigger<> { - public: - explicit ChimeTrigger(AlarmControlPanel *alarm_control_panel) { - alarm_control_panel->add_on_chime_callback([this]() { this->trigger(); }); - } -}; - -/// Trigger on ready state change -class ReadyTrigger : public Trigger<> { - public: - explicit ReadyTrigger(AlarmControlPanel *alarm_control_panel) { - alarm_control_panel->add_on_ready_callback([this]() { this->trigger(); }); - } -}; +static_assert(sizeof(StateAnyForwarder) <= sizeof(void *)); +static_assert(std::is_trivially_copyable_v); +static_assert(sizeof(StateEnterForwarder) <= sizeof(void *)); +static_assert(std::is_trivially_copyable_v>); template class ArmAwayAction : public Action { public: diff --git a/esphome/components/alpha3/alpha3.cpp b/esphome/components/alpha3/alpha3.cpp index f22a8e2444..6e82ec047d 100644 --- a/esphome/components/alpha3/alpha3.cpp +++ b/esphome/components/alpha3/alpha3.cpp @@ -125,7 +125,7 @@ void Alpha3::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc this->current_sensor_->publish_state(NAN); if (this->speed_sensor_ != nullptr) this->speed_sensor_->publish_state(NAN); - if (this->speed_sensor_ != nullptr) + if (this->voltage_sensor_ != nullptr) this->voltage_sensor_->publish_state(NAN); break; } diff --git a/esphome/components/am2315c/am2315c.cpp b/esphome/components/am2315c/am2315c.cpp index b20a8c6cbb..1390b74975 100644 --- a/esphome/components/am2315c/am2315c.cpp +++ b/esphome/components/am2315c/am2315c.cpp @@ -176,7 +176,5 @@ void AM2315C::dump_config() { LOG_SENSOR(" ", "Humidity", this->humidity_sensor_); } -float AM2315C::get_setup_priority() const { return setup_priority::DATA; } - } // namespace am2315c } // namespace esphome diff --git a/esphome/components/am2315c/am2315c.h b/esphome/components/am2315c/am2315c.h index c8d01beeaa..d7baf01cae 100644 --- a/esphome/components/am2315c/am2315c.h +++ b/esphome/components/am2315c/am2315c.h @@ -33,7 +33,6 @@ class AM2315C : public PollingComponent, public i2c::I2CDevice { void dump_config() override; void update() override; void setup() override; - float get_setup_priority() const override; void set_temperature_sensor(sensor::Sensor *temperature_sensor) { this->temperature_sensor_ = temperature_sensor; } void set_humidity_sensor(sensor::Sensor *humidity_sensor) { this->humidity_sensor_ = humidity_sensor; } diff --git a/esphome/components/am2320/am2320.cpp b/esphome/components/am2320/am2320.cpp index 055be2aeee..7fef3bb3a6 100644 --- a/esphome/components/am2320/am2320.cpp +++ b/esphome/components/am2320/am2320.cpp @@ -51,7 +51,6 @@ void AM2320Component::dump_config() { LOG_SENSOR(" ", "Temperature", this->temperature_sensor_); LOG_SENSOR(" ", "Humidity", this->humidity_sensor_); } -float AM2320Component::get_setup_priority() const { return setup_priority::DATA; } bool AM2320Component::read_bytes_(uint8_t a_register, uint8_t *data, uint8_t len, uint32_t conversion) { if (!this->write_bytes(a_register, data, 2)) { diff --git a/esphome/components/am2320/am2320.h b/esphome/components/am2320/am2320.h index da1e87cf65..708dbb632e 100644 --- a/esphome/components/am2320/am2320.h +++ b/esphome/components/am2320/am2320.h @@ -11,7 +11,6 @@ class AM2320Component : public PollingComponent, public i2c::I2CDevice { public: void setup() override; void dump_config() override; - float get_setup_priority() const override; void update() override; void set_temperature_sensor(sensor::Sensor *temperature_sensor) { temperature_sensor_ = temperature_sensor; } diff --git a/esphome/components/am43/cover/am43_cover.cpp b/esphome/components/am43/cover/am43_cover.cpp index 0d49439095..2fa26d266a 100644 --- a/esphome/components/am43/cover/am43_cover.cpp +++ b/esphome/components/am43/cover/am43_cover.cpp @@ -63,8 +63,9 @@ void Am43Component::control(const CoverCall &call) { ESP_LOGW(TAG, "[%s] Error writing stop command to device, error = %d", this->get_name().c_str(), status); } } - if (call.get_position().has_value()) { - auto pos = *call.get_position(); + auto opt_pos = call.get_position(); + if (opt_pos.has_value()) { + auto pos = *opt_pos; if (this->invert_position_) pos = 1 - pos; diff --git a/esphome/components/am43/sensor/am43_sensor.h b/esphome/components/am43/sensor/am43_sensor.h index 91973d8e33..195b96a19e 100644 --- a/esphome/components/am43/sensor/am43_sensor.h +++ b/esphome/components/am43/sensor/am43_sensor.h @@ -35,7 +35,7 @@ class Am43 : public esphome::ble_client::BLEClientNode, public PollingComponent uint8_t current_sensor_; // The AM43 often gets into a state where it spams loads of battery update // notifications. Here we will limit to no more than every 10s. - uint8_t last_battery_update_; + uint32_t last_battery_update_; }; } // namespace am43 diff --git a/esphome/components/analog_threshold/analog_threshold_binary_sensor.h b/esphome/components/analog_threshold/analog_threshold_binary_sensor.h index 9ea95d8570..dd70768105 100644 --- a/esphome/components/analog_threshold/analog_threshold_binary_sensor.h +++ b/esphome/components/analog_threshold/analog_threshold_binary_sensor.h @@ -1,5 +1,6 @@ #pragma once +#include "esphome/core/automation.h" #include "esphome/core/component.h" #include "esphome/components/binary_sensor/binary_sensor.h" #include "esphome/components/sensor/sensor.h" diff --git a/esphome/components/animation/__init__.py b/esphome/components/animation/__init__.py index c4ac7adb23..e9630f5266 100644 --- a/esphome/components/animation/__init__.py +++ b/esphome/components/animation/__init__.py @@ -69,9 +69,15 @@ SET_FRAME_SCHEMA = cv.Schema( ) -@automation.register_action("animation.next_frame", NextFrameAction, NEXT_FRAME_SCHEMA) -@automation.register_action("animation.prev_frame", PrevFrameAction, PREV_FRAME_SCHEMA) -@automation.register_action("animation.set_frame", SetFrameAction, SET_FRAME_SCHEMA) +@automation.register_action( + "animation.next_frame", NextFrameAction, NEXT_FRAME_SCHEMA, synchronous=True +) +@automation.register_action( + "animation.prev_frame", PrevFrameAction, PREV_FRAME_SCHEMA, synchronous=True +) +@automation.register_action( + "animation.set_frame", SetFrameAction, SET_FRAME_SCHEMA, synchronous=True +) async def animation_action_to_code(config, action_id, template_arg, args): paren = await cg.get_variable(config[CONF_ID]) var = cg.new_Pvariable(action_id, template_arg, paren) diff --git a/esphome/components/anova/anova.cpp b/esphome/components/anova/anova.cpp index 5054488089..f21230b075 100644 --- a/esphome/components/anova/anova.cpp +++ b/esphome/components/anova/anova.cpp @@ -24,8 +24,9 @@ void Anova::loop() { } void Anova::control(const ClimateCall &call) { - if (call.get_mode().has_value()) { - ClimateMode mode = *call.get_mode(); + auto mode_val = call.get_mode(); + if (mode_val.has_value()) { + ClimateMode mode = *mode_val; AnovaPacket *pkt; switch (mode) { case climate::CLIMATE_MODE_OFF: @@ -45,8 +46,9 @@ void Anova::control(const ClimateCall &call) { ESP_LOGW(TAG, "[%s] esp_ble_gattc_write_char failed, status=%d", this->parent_->address_str(), status); } } - if (call.get_target_temperature().has_value()) { - auto *pkt = this->codec_->get_set_target_temp_request(*call.get_target_temperature()); + auto target_temp = call.get_target_temperature(); + if (target_temp.has_value()) { + auto *pkt = this->codec_->get_set_target_temp_request(*target_temp); auto status = esp_ble_gattc_write_char(this->parent_->get_gattc_if(), this->parent_->get_conn_id(), this->char_handle_, pkt->length, pkt->data, ESP_GATT_WRITE_TYPE_NO_RSP, ESP_GATT_AUTH_REQ_NONE); @@ -67,10 +69,8 @@ void Anova::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_ case ESP_GATTC_SEARCH_CMPL_EVT: { auto *chr = this->parent_->get_characteristic(ANOVA_SERVICE_UUID, ANOVA_CHARACTERISTIC_UUID); if (chr == nullptr) { - ESP_LOGW(TAG, - "[%s] No control service found at device, not an Anova..?\n" - "[%s] Note, this component does not currently support Anova Nano.", - this->get_name().c_str(), this->get_name().c_str()); + ESP_LOGW(TAG, "[%s] No control service found at device, not an Anova..?", this->get_name().c_str()); + ESP_LOGW(TAG, "[%s] Note, this component does not currently support Anova Nano.", this->get_name().c_str()); break; } this->char_handle_ = chr->handle; @@ -144,9 +144,12 @@ void Anova::update() { return; if (this->current_request_ < 2) { - auto *pkt = this->codec_->get_read_device_status_request(); - if (this->current_request_ == 0) - this->codec_->get_set_unit_request(this->fahrenheit_ ? 'f' : 'c'); + AnovaPacket *pkt; + if (this->current_request_ == 0) { + pkt = this->codec_->get_set_unit_request(this->fahrenheit_ ? 'f' : 'c'); + } else { + pkt = this->codec_->get_read_device_status_request(); + } auto status = esp_ble_gattc_write_char(this->parent_->get_gattc_if(), this->parent_->get_conn_id(), this->char_handle_, pkt->length, pkt->data, ESP_GATT_WRITE_TYPE_NO_RSP, ESP_GATT_AUTH_REQ_NONE); diff --git a/esphome/components/apds9960/apds9960.cpp b/esphome/components/apds9960/apds9960.cpp index 93038d3160..a07175f2c9 100644 --- a/esphome/components/apds9960/apds9960.cpp +++ b/esphome/components/apds9960/apds9960.cpp @@ -251,11 +251,11 @@ void APDS9960::read_gesture_data_() { uint8_t buf[128]; for (uint8_t pos = 0; pos < fifo_level * 4; pos += 32) { - // The ESP's i2c driver has a limited buffer size. - // This way of retrieving the data should be wrong according to the datasheet - // but it seems to work. + // Read in 32-byte chunks due to ESP8266 I2C buffer limit. + // Always read from 0xFC — the FIFO auto-increments through 0xFC-0xFF + // and advances its internal pointer after every 4th byte. uint8_t read = std::min(32, fifo_level * 4 - pos); - APDS9960_WARNING_CHECK(this->read_bytes(0xFC + pos, buf + pos, read), "Reading FIFO buffer failed."); + APDS9960_WARNING_CHECK(this->read_bytes(0xFC, buf + pos, read), "Reading FIFO buffer failed."); } if (millis() - this->gesture_start_ > 500) { @@ -384,7 +384,6 @@ void APDS9960::process_dataset_(int up, int down, int left, int right) { } } } -float APDS9960::get_setup_priority() const { return setup_priority::DATA; } bool APDS9960::is_proximity_enabled_() const { return #ifdef USE_SENSOR diff --git a/esphome/components/apds9960/apds9960.h b/esphome/components/apds9960/apds9960.h index 2a0fbb5c19..4574b70a42 100644 --- a/esphome/components/apds9960/apds9960.h +++ b/esphome/components/apds9960/apds9960.h @@ -32,7 +32,6 @@ class APDS9960 : public PollingComponent, public i2c::I2CDevice { public: void setup() override; void dump_config() override; - float get_setup_priority() const override; void update() override; void loop() override; diff --git a/esphome/components/api/__init__.py b/esphome/components/api/__init__.py index 9bff9f5635..84589d540d 100644 --- a/esphome/components/api/__init__.py +++ b/esphome/components/api/__init__.py @@ -76,7 +76,7 @@ SERVICE_ARG_NATIVE_TYPES: dict[str, MockObj] = { "bool": cg.bool_, "int": cg.int32, "float": cg.float_, - "string": cg.std_string, + "string": cg.StringRef, "bool[]": cg.FixedVector.template(cg.bool_).operator("const").operator("ref"), "int[]": cg.FixedVector.template(cg.int32).operator("const").operator("ref"), "float[]": cg.FixedVector.template(cg.float_).operator("const").operator("ref"), @@ -233,8 +233,8 @@ def _consume_api_sockets(config: ConfigType) -> ConfigType: # API needs 1 listening socket + typically 3 concurrent client connections # (not max_connections, which is the upper limit rarely reached) - sockets_needed = 1 + 3 - socket.consume_sockets(sockets_needed, "api")(config) + socket.consume_sockets(3, "api")(config) + socket.consume_sockets(1, "api", socket.SocketType.TCP_LISTEN)(config) return config @@ -301,11 +301,12 @@ CONFIG_SCHEMA = cv.All( # Maximum queued send buffers per connection before dropping connection # Each buffer uses ~8-12 bytes overhead plus actual message size # Platform defaults based on available RAM and typical message rates: + # CONF_MAX_SEND_QUEUE defaults are power of 2 for efficient modulo cv.SplitDefault( CONF_MAX_SEND_QUEUE, - esp8266=5, # Limited RAM, need to fail fast + esp8266=4, # Limited RAM, need to fail fast esp32=8, # More RAM, can buffer more - rp2040=5, # Limited RAM + rp2040=8, # Moderate RAM bk72xx=8, # Moderate RAM nrf52=8, # Moderate RAM rtl87xx=8, # Moderate RAM @@ -380,9 +381,18 @@ async def to_code(config: ConfigType) -> None: if is_optional: func_args.append((cg.bool_, "return_response")) + # Check if action chain has non-synchronous actions that would make + # non-owning StringRef dangle (rx_buf_ reused after delay) + has_non_synchronous = automation.has_non_synchronous_actions( + conf.get(CONF_THEN, []) + ) + service_arg_names: list[str] = [] for name, var_ in conf[CONF_VARIABLES].items(): native = SERVICE_ARG_NATIVE_TYPES[var_] + # Fall back to std::string for string args if non-synchronous actions exist + if has_non_synchronous and native is cg.StringRef: + native = cg.std_string service_template_args.append(native) func_args.append((native, name)) service_arg_names.append(name) @@ -444,7 +454,10 @@ async def to_code(config: ConfigType) -> None: # and plaintext disabled. Only a factory reset can remove it. cg.add_define("USE_API_PLAINTEXT") cg.add_define("USE_API_NOISE") - cg.add_library("esphome/noise-c", "0.1.10") + cg.add_library("esphome/noise-c", "0.1.11") + # Enable optimized memzero/memcmp in libsodium instead of volatile byte loops + cg.add_build_flag("-DHAVE_WEAK_SYMBOLS=1") + cg.add_build_flag("-DHAVE_INLINE_ASM=1") else: cg.add_define("USE_API_PLAINTEXT") @@ -509,11 +522,13 @@ HOMEASSISTANT_ACTION_ACTION_SCHEMA = cv.All( "homeassistant.action", HomeAssistantServiceCallAction, HOMEASSISTANT_ACTION_ACTION_SCHEMA, + synchronous=True, ) @automation.register_action( "homeassistant.service", HomeAssistantServiceCallAction, HOMEASSISTANT_ACTION_ACTION_SCHEMA, + synchronous=True, ) async def homeassistant_service_to_code( config: ConfigType, @@ -524,24 +539,31 @@ async def homeassistant_service_to_code( cg.add_define("USE_API_HOMEASSISTANT_SERVICES") serv = await cg.get_variable(config[CONF_ID]) var = cg.new_Pvariable(action_id, template_arg, serv, False) - templ = await cg.templatable(config[CONF_ACTION], args, None) + templ = await cg.templatable(config[CONF_ACTION], args, cg.std_string) cg.add(var.set_service(templ)) # Initialize FixedVectors with exact sizes from config cg.add(var.init_data(len(config[CONF_DATA]))) for key, value in config[CONF_DATA].items(): + # output_type=None because lambdas can return non-string types (int, + # float, char*) that TemplatableStringValue converts via to_string. + # Static strings are manually wrapped for PROGMEM on ESP8266. templ = await cg.templatable(value, args, None) - cg.add(var.add_data(key, templ)) + if isinstance(templ, str): + templ = cg.FlashStringLiteral(templ) + cg.add(var.add_data(cg.FlashStringLiteral(key), templ)) cg.add(var.init_data_template(len(config[CONF_DATA_TEMPLATE]))) for key, value in config[CONF_DATA_TEMPLATE].items(): templ = await cg.templatable(value, args, None) - cg.add(var.add_data_template(key, templ)) + if isinstance(templ, str): + templ = cg.FlashStringLiteral(templ) + cg.add(var.add_data_template(cg.FlashStringLiteral(key), templ)) cg.add(var.init_variables(len(config[CONF_VARIABLES]))) for key, value in config[CONF_VARIABLES].items(): templ = await cg.templatable(value, args, None) - cg.add(var.add_variable(key, templ)) + cg.add(var.add_variable(cg.FlashStringLiteral(key), templ)) if on_error := config.get(CONF_ON_ERROR): cg.add_define("USE_API_HOMEASSISTANT_ACTION_RESPONSES") @@ -604,29 +626,37 @@ HOMEASSISTANT_EVENT_ACTION_SCHEMA = cv.Schema( "homeassistant.event", HomeAssistantServiceCallAction, HOMEASSISTANT_EVENT_ACTION_SCHEMA, + synchronous=True, ) async def homeassistant_event_to_code(config, action_id, template_arg, args): cg.add_define("USE_API_HOMEASSISTANT_SERVICES") serv = await cg.get_variable(config[CONF_ID]) var = cg.new_Pvariable(action_id, template_arg, serv, True) - templ = await cg.templatable(config[CONF_EVENT], args, None) + templ = await cg.templatable(config[CONF_EVENT], args, cg.std_string) cg.add(var.set_service(templ)) # Initialize FixedVectors with exact sizes from config cg.add(var.init_data(len(config[CONF_DATA]))) for key, value in config[CONF_DATA].items(): + # output_type=None because lambdas can return non-string types (int, + # float, char*) that TemplatableStringValue converts via to_string. + # Static strings are manually wrapped for PROGMEM on ESP8266. templ = await cg.templatable(value, args, None) - cg.add(var.add_data(key, templ)) + if isinstance(templ, str): + templ = cg.FlashStringLiteral(templ) + cg.add(var.add_data(cg.FlashStringLiteral(key), templ)) cg.add(var.init_data_template(len(config[CONF_DATA_TEMPLATE]))) for key, value in config[CONF_DATA_TEMPLATE].items(): templ = await cg.templatable(value, args, None) - cg.add(var.add_data_template(key, templ)) + if isinstance(templ, str): + templ = cg.FlashStringLiteral(templ) + cg.add(var.add_data_template(cg.FlashStringLiteral(key), templ)) cg.add(var.init_variables(len(config[CONF_VARIABLES]))) for key, value in config[CONF_VARIABLES].items(): templ = await cg.templatable(value, args, None) - cg.add(var.add_variable(key, templ)) + cg.add(var.add_variable(cg.FlashStringLiteral(key), templ)) return var @@ -644,16 +674,17 @@ HOMEASSISTANT_TAG_SCANNED_ACTION_SCHEMA = cv.maybe_simple_value( "homeassistant.tag_scanned", HomeAssistantServiceCallAction, HOMEASSISTANT_TAG_SCANNED_ACTION_SCHEMA, + synchronous=True, ) async def homeassistant_tag_scanned_to_code(config, action_id, template_arg, args): cg.add_define("USE_API_HOMEASSISTANT_SERVICES") serv = await cg.get_variable(config[CONF_ID]) var = cg.new_Pvariable(action_id, template_arg, serv, True) - cg.add(var.set_service("esphome.tag_scanned")) + cg.add(var.set_service(cg.FlashStringLiteral("esphome.tag_scanned"))) # Initialize FixedVector with exact size (1 data field) cg.add(var.init_data(1)) templ = await cg.templatable(config[CONF_TAG], args, cg.std_string) - cg.add(var.add_data("tag_id", templ)) + cg.add(var.add_data(cg.FlashStringLiteral("tag_id"), templ)) return var @@ -685,6 +716,7 @@ API_RESPOND_ACTION_SCHEMA = cv.All( "api.respond", APIRespondAction, API_RESPOND_ACTION_SCHEMA, + synchronous=True, ) async def api_respond_to_code( config: ConfigType, diff --git a/esphome/components/api/api.proto b/esphome/components/api/api.proto index 597da25883..96ee2fb920 100644 --- a/esphome/components/api/api.proto +++ b/esphome/components/api/api.proto @@ -45,6 +45,7 @@ service APIConnection { rpc time_command (TimeCommandRequest) returns (void) {} rpc update_command (UpdateCommandRequest) returns (void) {} rpc valve_command (ValveCommandRequest) returns (void) {} + rpc water_heater_command (WaterHeaterCommandRequest) returns (void) {} rpc subscribe_bluetooth_le_advertisements(SubscribeBluetoothLEAdvertisementsRequest) returns (void) {} rpc bluetooth_device_request(BluetoothDeviceRequest) returns (void) {} @@ -57,6 +58,7 @@ service APIConnection { rpc subscribe_bluetooth_connections_free(SubscribeBluetoothConnectionsFreeRequest) returns (BluetoothConnectionsFreeResponse) {} rpc unsubscribe_bluetooth_le_advertisements(UnsubscribeBluetoothLEAdvertisementsRequest) returns (void) {} rpc bluetooth_scanner_set_mode(BluetoothScannerSetModeRequest) returns (void) {} + rpc bluetooth_set_connection_params(BluetoothSetConnectionParamsRequest) returns (BluetoothSetConnectionParamsResponse) {} rpc subscribe_voice_assistant(SubscribeVoiceAssistantRequest) returns (void) {} rpc voice_assistant_get_configuration(VoiceAssistantConfigurationRequest) returns (VoiceAssistantConfigurationResponse) {} @@ -68,6 +70,12 @@ service APIConnection { rpc zwave_proxy_request(ZWaveProxyRequest) returns (void) {} rpc infrared_rf_transmit_raw_timings(InfraredRFTransmitRawTimingsRequest) returns (void) {} + + rpc serial_proxy_configure(SerialProxyConfigureRequest) returns (void) {} + rpc serial_proxy_write(SerialProxyWriteRequest) returns (void) {} + rpc serial_proxy_set_modem_pins(SerialProxySetModemPinsRequest) returns (void) {} + rpc serial_proxy_get_modem_pins(SerialProxyGetModemPinsRequest) returns (void) {} + rpc serial_proxy_request(SerialProxyRequest) returns (void) {} } @@ -197,6 +205,17 @@ message DeviceInfo { uint32 area_id = 3; } +enum SerialProxyPortType { + SERIAL_PROXY_PORT_TYPE_TTL = 0; + SERIAL_PROXY_PORT_TYPE_RS232 = 1; + SERIAL_PROXY_PORT_TYPE_RS485 = 2; +} + +message SerialProxyInfo { + string name = 1; // Human-readable port name + SerialProxyPortType port_type = 2; // Port type (RS232, RS485) +} + message DeviceInfoResponse { option (id) = 10; option (source) = SOURCE_SERVER; @@ -259,6 +278,9 @@ message DeviceInfoResponse { // Indicates if Z-Wave proxy support is available and features supported uint32 zwave_proxy_feature_flags = 23 [(field_ifdef) = "USE_ZWAVE_PROXY"]; uint32 zwave_home_id = 24 [(field_ifdef) = "USE_ZWAVE_PROXY"]; + + // Serial proxy instance metadata + repeated SerialProxyInfo serial_proxies = 25 [(field_ifdef) = "USE_SERIAL_PROXY", (fixed_array_size_define) = "SERIAL_PROXY_COUNT"]; } message ListEntitiesRequest { @@ -294,7 +316,7 @@ message ListEntitiesBinarySensorResponse { option (ifdef) = "USE_BINARY_SENSOR"; string object_id = 1; - fixed32 key = 2; + fixed32 key = 2 [(force) = true]; string name = 3; reserved 4; // Deprecated: was string unique_id @@ -312,7 +334,7 @@ message BinarySensorStateResponse { option (ifdef) = "USE_BINARY_SENSOR"; option (no_delay) = true; - fixed32 key = 1; + fixed32 key = 1 [(force) = true]; bool state = 2; // If the binary sensor does not have a valid state yet. // Equivalent to `!obj->has_state()` - inverse logic to make state packets smaller @@ -328,7 +350,7 @@ message ListEntitiesCoverResponse { option (ifdef) = "USE_COVER"; string object_id = 1; - fixed32 key = 2; + fixed32 key = 2 [(force) = true]; string name = 3; reserved 4; // Deprecated: was string unique_id @@ -361,7 +383,7 @@ message CoverStateResponse { option (ifdef) = "USE_COVER"; option (no_delay) = true; - fixed32 key = 1; + fixed32 key = 1 [(force) = true]; // legacy: state has been removed in 1.13 // clients/servers must still send/accept it until the next protocol change // Deprecated in API version 1.1 @@ -387,7 +409,7 @@ message CoverCommandRequest { option (no_delay) = true; option (base_class) = "CommandProtoMessage"; - fixed32 key = 1; + fixed32 key = 1 [(force) = true]; // legacy: command has been removed in 1.13 // clients/servers must still send/accept it until the next protocol change @@ -412,7 +434,7 @@ message ListEntitiesFanResponse { option (ifdef) = "USE_FAN"; string object_id = 1; - fixed32 key = 2; + fixed32 key = 2 [(force) = true]; string name = 3; reserved 4; // Deprecated: was string unique_id @@ -444,7 +466,7 @@ message FanStateResponse { option (ifdef) = "USE_FAN"; option (no_delay) = true; - fixed32 key = 1; + fixed32 key = 1 [(force) = true]; bool state = 2; bool oscillating = 3; // Deprecated in API version 1.6 @@ -461,7 +483,7 @@ message FanCommandRequest { option (no_delay) = true; option (base_class) = "CommandProtoMessage"; - fixed32 key = 1; + fixed32 key = 1 [(force) = true]; bool has_state = 2; bool state = 3; // Deprecated in API version 1.6 @@ -500,7 +522,7 @@ message ListEntitiesLightResponse { option (ifdef) = "USE_LIGHT"; string object_id = 1; - fixed32 key = 2; + fixed32 key = 2 [(force) = true]; string name = 3; reserved 4; // Deprecated: was string unique_id @@ -529,7 +551,7 @@ message LightStateResponse { option (ifdef) = "USE_LIGHT"; option (no_delay) = true; - fixed32 key = 1; + fixed32 key = 1 [(force) = true]; bool state = 2; float brightness = 3; ColorMode color_mode = 11; @@ -551,7 +573,7 @@ message LightCommandRequest { option (no_delay) = true; option (base_class) = "CommandProtoMessage"; - fixed32 key = 1; + fixed32 key = 1 [(force) = true]; bool has_state = 2; bool state = 3; bool has_brightness = 4; @@ -605,7 +627,7 @@ message ListEntitiesSensorResponse { option (ifdef) = "USE_SENSOR"; string object_id = 1; - fixed32 key = 2; + fixed32 key = 2 [(force) = true]; string name = 3; reserved 4; // Deprecated: was string unique_id @@ -629,7 +651,7 @@ message SensorStateResponse { option (ifdef) = "USE_SENSOR"; option (no_delay) = true; - fixed32 key = 1; + fixed32 key = 1 [(force) = true]; float state = 2; // If the sensor does not have a valid state yet. // Equivalent to `!obj->has_state()` - inverse logic to make state packets smaller @@ -645,7 +667,7 @@ message ListEntitiesSwitchResponse { option (ifdef) = "USE_SWITCH"; string object_id = 1; - fixed32 key = 2; + fixed32 key = 2 [(force) = true]; string name = 3; reserved 4; // Deprecated: was string unique_id @@ -663,7 +685,7 @@ message SwitchStateResponse { option (ifdef) = "USE_SWITCH"; option (no_delay) = true; - fixed32 key = 1; + fixed32 key = 1 [(force) = true]; bool state = 2; uint32 device_id = 3 [(field_ifdef) = "USE_DEVICES"]; } @@ -674,7 +696,7 @@ message SwitchCommandRequest { option (no_delay) = true; option (base_class) = "CommandProtoMessage"; - fixed32 key = 1; + fixed32 key = 1 [(force) = true]; bool state = 2; uint32 device_id = 3 [(field_ifdef) = "USE_DEVICES"]; } @@ -687,7 +709,7 @@ message ListEntitiesTextSensorResponse { option (ifdef) = "USE_TEXT_SENSOR"; string object_id = 1; - fixed32 key = 2; + fixed32 key = 2 [(force) = true]; string name = 3; reserved 4; // Deprecated: was string unique_id @@ -704,7 +726,7 @@ message TextSensorStateResponse { option (ifdef) = "USE_TEXT_SENSOR"; option (no_delay) = true; - fixed32 key = 1; + fixed32 key = 1 [(force) = true]; string state = 2; // If the text sensor does not have a valid state yet. // Equivalent to `!obj->has_state()` - inverse logic to make state packets smaller @@ -833,6 +855,33 @@ message GetTimeRequest { option (source) = SOURCE_SERVER; } +enum DSTRuleType { + DST_RULE_TYPE_NONE = 0; + DST_RULE_TYPE_MONTH_WEEK_DAY = 1; + DST_RULE_TYPE_JULIAN_NO_LEAP = 2; + DST_RULE_TYPE_DAY_OF_YEAR = 3; +} + +message DSTRule { + option (source) = SOURCE_CLIENT; + + sint32 time_seconds = 1; + uint32 day = 2; + DSTRuleType type = 3; + uint32 month = 4; + uint32 week = 5; + uint32 day_of_week = 6; +} + +message ParsedTimezone { + option (source) = SOURCE_CLIENT; + + sint32 std_offset_seconds = 1; + sint32 dst_offset_seconds = 2; + DSTRule dst_start = 3; + DSTRule dst_end = 4; +} + message GetTimeResponse { option (id) = 37; option (source) = SOURCE_CLIENT; @@ -840,6 +889,7 @@ message GetTimeResponse { fixed32 epoch_seconds = 1; string timezone = 2; + ParsedTimezone parsed_timezone = 3; } // ==================== USER-DEFINES SERVICES ==================== @@ -872,7 +922,7 @@ message ListEntitiesServicesResponse { option (ifdef) = "USE_API_USER_DEFINED_ACTIONS"; string name = 1; - fixed32 key = 2; + fixed32 key = 2 [(force) = true]; repeated ListEntitiesServicesArgument args = 3 [(fixed_vector) = true]; SupportsResponseType supports_response = 4; } @@ -895,7 +945,7 @@ message ExecuteServiceRequest { option (no_delay) = true; option (ifdef) = "USE_API_USER_DEFINED_ACTIONS"; - fixed32 key = 1; + fixed32 key = 1 [(force) = true]; repeated ExecuteServiceArgument args = 2 [(fixed_vector) = true]; uint32 call_id = 3 [(field_ifdef) = "USE_API_USER_DEFINED_ACTION_RESPONSES"]; bool return_response = 4 [(field_ifdef) = "USE_API_USER_DEFINED_ACTION_RESPONSES"]; @@ -922,7 +972,7 @@ message ListEntitiesCameraResponse { option (ifdef) = "USE_CAMERA"; string object_id = 1; - fixed32 key = 2; + fixed32 key = 2 [(force) = true]; string name = 3; reserved 4; // Deprecated: was string unique_id bool disabled_by_default = 5; @@ -937,7 +987,7 @@ message CameraImageResponse { option (source) = SOURCE_SERVER; option (ifdef) = "USE_CAMERA"; - fixed32 key = 1; + fixed32 key = 1 [(force) = true]; bytes data = 2; bool done = 3; uint32 device_id = 4 [(field_ifdef) = "USE_DEVICES"]; @@ -988,6 +1038,7 @@ enum ClimateAction { CLIMATE_ACTION_IDLE = 4; CLIMATE_ACTION_DRYING = 5; CLIMATE_ACTION_FAN = 6; + CLIMATE_ACTION_DEFROSTING = 7; } enum ClimatePreset { CLIMATE_PRESET_NONE = 0; @@ -1006,7 +1057,7 @@ message ListEntitiesClimateResponse { option (ifdef) = "USE_CLIMATE"; string object_id = 1; - fixed32 key = 2; + fixed32 key = 2 [(force) = true]; string name = 3; reserved 4; // Deprecated: was string unique_id @@ -1044,7 +1095,7 @@ message ClimateStateResponse { option (ifdef) = "USE_CLIMATE"; option (no_delay) = true; - fixed32 key = 1; + fixed32 key = 1 [(force) = true]; ClimateMode mode = 2; float current_temperature = 3; float target_temperature = 4; @@ -1070,7 +1121,7 @@ message ClimateCommandRequest { option (no_delay) = true; option (base_class) = "CommandProtoMessage"; - fixed32 key = 1; + fixed32 key = 1 [(force) = true]; bool has_mode = 2; ClimateMode mode = 3; bool has_target_temperature = 4; @@ -1117,7 +1168,7 @@ message ListEntitiesWaterHeaterResponse { option (ifdef) = "USE_WATER_HEATER"; string object_id = 1; - fixed32 key = 2; + fixed32 key = 2 [(force) = true]; string name = 3; string icon = 4 [(field_ifdef) = "USE_ENTITY_ICON"]; bool disabled_by_default = 5; @@ -1138,7 +1189,7 @@ message WaterHeaterStateResponse { option (ifdef) = "USE_WATER_HEATER"; option (no_delay) = true; - fixed32 key = 1; + fixed32 key = 1 [(force) = true]; float current_temperature = 2; float target_temperature = 3; WaterHeaterMode mode = 4; @@ -1154,9 +1205,11 @@ enum WaterHeaterCommandHasField { WATER_HEATER_COMMAND_HAS_NONE = 0; WATER_HEATER_COMMAND_HAS_MODE = 1; WATER_HEATER_COMMAND_HAS_TARGET_TEMPERATURE = 2; - WATER_HEATER_COMMAND_HAS_STATE = 4; + WATER_HEATER_COMMAND_HAS_STATE = 4 [deprecated=true]; WATER_HEATER_COMMAND_HAS_TARGET_TEMPERATURE_LOW = 8; WATER_HEATER_COMMAND_HAS_TARGET_TEMPERATURE_HIGH = 16; + WATER_HEATER_COMMAND_HAS_ON_STATE = 32; + WATER_HEATER_COMMAND_HAS_AWAY_STATE = 64; } message WaterHeaterCommandRequest { @@ -1166,7 +1219,7 @@ message WaterHeaterCommandRequest { option (no_delay) = true; option (base_class) = "CommandProtoMessage"; - fixed32 key = 1; + fixed32 key = 1 [(force) = true]; // Bitmask of which fields are set (see WaterHeaterCommandHasField) uint32 has_fields = 2; WaterHeaterMode mode = 3; @@ -1191,7 +1244,7 @@ message ListEntitiesNumberResponse { option (ifdef) = "USE_NUMBER"; string object_id = 1; - fixed32 key = 2; + fixed32 key = 2 [(force) = true]; string name = 3; reserved 4; // Deprecated: was string unique_id @@ -1213,7 +1266,7 @@ message NumberStateResponse { option (ifdef) = "USE_NUMBER"; option (no_delay) = true; - fixed32 key = 1; + fixed32 key = 1 [(force) = true]; float state = 2; // If the number does not have a valid state yet. // Equivalent to `!obj->has_state()` - inverse logic to make state packets smaller @@ -1227,7 +1280,7 @@ message NumberCommandRequest { option (no_delay) = true; option (base_class) = "CommandProtoMessage"; - fixed32 key = 1; + fixed32 key = 1 [(force) = true]; float state = 2; uint32 device_id = 3 [(field_ifdef) = "USE_DEVICES"]; } @@ -1240,7 +1293,7 @@ message ListEntitiesSelectResponse { option (ifdef) = "USE_SELECT"; string object_id = 1; - fixed32 key = 2; + fixed32 key = 2 [(force) = true]; string name = 3; reserved 4; // Deprecated: was string unique_id @@ -1257,7 +1310,7 @@ message SelectStateResponse { option (ifdef) = "USE_SELECT"; option (no_delay) = true; - fixed32 key = 1; + fixed32 key = 1 [(force) = true]; string state = 2; // If the select does not have a valid state yet. // Equivalent to `!obj->has_state()` - inverse logic to make state packets smaller @@ -1271,7 +1324,7 @@ message SelectCommandRequest { option (no_delay) = true; option (base_class) = "CommandProtoMessage"; - fixed32 key = 1; + fixed32 key = 1 [(force) = true]; string state = 2; uint32 device_id = 3 [(field_ifdef) = "USE_DEVICES"]; } @@ -1284,7 +1337,7 @@ message ListEntitiesSirenResponse { option (ifdef) = "USE_SIREN"; string object_id = 1; - fixed32 key = 2; + fixed32 key = 2 [(force) = true]; string name = 3; reserved 4; // Deprecated: was string unique_id @@ -1303,7 +1356,7 @@ message SirenStateResponse { option (ifdef) = "USE_SIREN"; option (no_delay) = true; - fixed32 key = 1; + fixed32 key = 1 [(force) = true]; bool state = 2; uint32 device_id = 3 [(field_ifdef) = "USE_DEVICES"]; } @@ -1314,7 +1367,7 @@ message SirenCommandRequest { option (no_delay) = true; option (base_class) = "CommandProtoMessage"; - fixed32 key = 1; + fixed32 key = 1 [(force) = true]; bool has_state = 2; bool state = 3; bool has_tone = 4; @@ -1347,7 +1400,7 @@ message ListEntitiesLockResponse { option (ifdef) = "USE_LOCK"; string object_id = 1; - fixed32 key = 2; + fixed32 key = 2 [(force) = true]; string name = 3; reserved 4; // Deprecated: was string unique_id @@ -1369,7 +1422,7 @@ message LockStateResponse { option (source) = SOURCE_SERVER; option (ifdef) = "USE_LOCK"; option (no_delay) = true; - fixed32 key = 1; + fixed32 key = 1 [(force) = true]; LockState state = 2; uint32 device_id = 3 [(field_ifdef) = "USE_DEVICES"]; } @@ -1379,7 +1432,7 @@ message LockCommandRequest { option (ifdef) = "USE_LOCK"; option (no_delay) = true; option (base_class) = "CommandProtoMessage"; - fixed32 key = 1; + fixed32 key = 1 [(force) = true]; LockCommand command = 2; // Not yet implemented: @@ -1396,7 +1449,7 @@ message ListEntitiesButtonResponse { option (ifdef) = "USE_BUTTON"; string object_id = 1; - fixed32 key = 2; + fixed32 key = 2 [(force) = true]; string name = 3; reserved 4; // Deprecated: was string unique_id @@ -1413,7 +1466,7 @@ message ButtonCommandRequest { option (no_delay) = true; option (base_class) = "CommandProtoMessage"; - fixed32 key = 1; + fixed32 key = 1 [(force) = true]; uint32 device_id = 2 [(field_ifdef) = "USE_DEVICES"]; } @@ -1463,7 +1516,7 @@ message ListEntitiesMediaPlayerResponse { option (ifdef) = "USE_MEDIA_PLAYER"; string object_id = 1; - fixed32 key = 2; + fixed32 key = 2 [(force) = true]; string name = 3; reserved 4; // Deprecated: was string unique_id @@ -1485,7 +1538,7 @@ message MediaPlayerStateResponse { option (source) = SOURCE_SERVER; option (ifdef) = "USE_MEDIA_PLAYER"; option (no_delay) = true; - fixed32 key = 1; + fixed32 key = 1 [(force) = true]; MediaPlayerState state = 2; float volume = 3; bool muted = 4; @@ -1498,7 +1551,7 @@ message MediaPlayerCommandRequest { option (no_delay) = true; option (base_class) = "CommandProtoMessage"; - fixed32 key = 1; + fixed32 key = 1 [(force) = true]; bool has_command = 2; MediaPlayerCommand command = 3; @@ -1551,11 +1604,11 @@ message BluetoothLEAdvertisementResponse { } message BluetoothLERawAdvertisement { - uint64 address = 1; - sint32 rssi = 2; + uint64 address = 1 [(force) = true]; + sint32 rssi = 2 [(force) = true]; uint32 address_type = 3; - bytes data = 4 [(fixed_array_size) = 62]; + bytes data = 4 [(fixed_array_size) = 62, (force) = true]; } message BluetoothLERawAdvertisementsResponse { @@ -2051,7 +2104,7 @@ message ListEntitiesAlarmControlPanelResponse { option (ifdef) = "USE_ALARM_CONTROL_PANEL"; string object_id = 1; - fixed32 key = 2; + fixed32 key = 2 [(force) = true]; string name = 3; reserved 4; // Deprecated: was string unique_id string icon = 5 [(field_ifdef) = "USE_ENTITY_ICON"]; @@ -2069,7 +2122,7 @@ message AlarmControlPanelStateResponse { option (source) = SOURCE_SERVER; option (ifdef) = "USE_ALARM_CONTROL_PANEL"; option (no_delay) = true; - fixed32 key = 1; + fixed32 key = 1 [(force) = true]; AlarmControlPanelState state = 2; uint32 device_id = 3 [(field_ifdef) = "USE_DEVICES"]; } @@ -2080,7 +2133,7 @@ message AlarmControlPanelCommandRequest { option (ifdef) = "USE_ALARM_CONTROL_PANEL"; option (no_delay) = true; option (base_class) = "CommandProtoMessage"; - fixed32 key = 1; + fixed32 key = 1 [(force) = true]; AlarmControlPanelStateCommand command = 2; string code = 3; uint32 device_id = 4 [(field_ifdef) = "USE_DEVICES"]; @@ -2098,7 +2151,7 @@ message ListEntitiesTextResponse { option (ifdef) = "USE_TEXT"; string object_id = 1; - fixed32 key = 2; + fixed32 key = 2 [(force) = true]; string name = 3; reserved 4; // Deprecated: was string unique_id string icon = 5 [(field_ifdef) = "USE_ENTITY_ICON"]; @@ -2118,7 +2171,7 @@ message TextStateResponse { option (ifdef) = "USE_TEXT"; option (no_delay) = true; - fixed32 key = 1; + fixed32 key = 1 [(force) = true]; string state = 2; // If the Text does not have a valid state yet. // Equivalent to `!obj->has_state()` - inverse logic to make state packets smaller @@ -2132,7 +2185,7 @@ message TextCommandRequest { option (no_delay) = true; option (base_class) = "CommandProtoMessage"; - fixed32 key = 1; + fixed32 key = 1 [(force) = true]; string state = 2; uint32 device_id = 3 [(field_ifdef) = "USE_DEVICES"]; } @@ -2146,7 +2199,7 @@ message ListEntitiesDateResponse { option (ifdef) = "USE_DATETIME_DATE"; string object_id = 1; - fixed32 key = 2; + fixed32 key = 2 [(force) = true]; string name = 3; reserved 4; // Deprecated: was string unique_id @@ -2162,7 +2215,7 @@ message DateStateResponse { option (ifdef) = "USE_DATETIME_DATE"; option (no_delay) = true; - fixed32 key = 1; + fixed32 key = 1 [(force) = true]; // If the date does not have a valid state yet. // Equivalent to `!obj->has_state()` - inverse logic to make state packets smaller bool missing_state = 2; @@ -2178,7 +2231,7 @@ message DateCommandRequest { option (no_delay) = true; option (base_class) = "CommandProtoMessage"; - fixed32 key = 1; + fixed32 key = 1 [(force) = true]; uint32 year = 2; uint32 month = 3; uint32 day = 4; @@ -2193,7 +2246,7 @@ message ListEntitiesTimeResponse { option (ifdef) = "USE_DATETIME_TIME"; string object_id = 1; - fixed32 key = 2; + fixed32 key = 2 [(force) = true]; string name = 3; reserved 4; // Deprecated: was string unique_id @@ -2209,7 +2262,7 @@ message TimeStateResponse { option (ifdef) = "USE_DATETIME_TIME"; option (no_delay) = true; - fixed32 key = 1; + fixed32 key = 1 [(force) = true]; // If the time does not have a valid state yet. // Equivalent to `!obj->has_state()` - inverse logic to make state packets smaller bool missing_state = 2; @@ -2225,7 +2278,7 @@ message TimeCommandRequest { option (no_delay) = true; option (base_class) = "CommandProtoMessage"; - fixed32 key = 1; + fixed32 key = 1 [(force) = true]; uint32 hour = 2; uint32 minute = 3; uint32 second = 4; @@ -2240,7 +2293,7 @@ message ListEntitiesEventResponse { option (ifdef) = "USE_EVENT"; string object_id = 1; - fixed32 key = 2; + fixed32 key = 2 [(force) = true]; string name = 3; reserved 4; // Deprecated: was string unique_id @@ -2258,7 +2311,7 @@ message EventResponse { option (source) = SOURCE_SERVER; option (ifdef) = "USE_EVENT"; - fixed32 key = 1; + fixed32 key = 1 [(force) = true]; string event_type = 2; uint32 device_id = 3 [(field_ifdef) = "USE_DEVICES"]; } @@ -2271,7 +2324,7 @@ message ListEntitiesValveResponse { option (ifdef) = "USE_VALVE"; string object_id = 1; - fixed32 key = 2; + fixed32 key = 2 [(force) = true]; string name = 3; reserved 4; // Deprecated: was string unique_id @@ -2298,7 +2351,7 @@ message ValveStateResponse { option (ifdef) = "USE_VALVE"; option (no_delay) = true; - fixed32 key = 1; + fixed32 key = 1 [(force) = true]; float position = 2; ValveOperation current_operation = 3; uint32 device_id = 4 [(field_ifdef) = "USE_DEVICES"]; @@ -2311,7 +2364,7 @@ message ValveCommandRequest { option (no_delay) = true; option (base_class) = "CommandProtoMessage"; - fixed32 key = 1; + fixed32 key = 1 [(force) = true]; bool has_position = 2; float position = 3; bool stop = 4; @@ -2326,7 +2379,7 @@ message ListEntitiesDateTimeResponse { option (ifdef) = "USE_DATETIME_DATETIME"; string object_id = 1; - fixed32 key = 2; + fixed32 key = 2 [(force) = true]; string name = 3; reserved 4; // Deprecated: was string unique_id @@ -2342,7 +2395,7 @@ message DateTimeStateResponse { option (ifdef) = "USE_DATETIME_DATETIME"; option (no_delay) = true; - fixed32 key = 1; + fixed32 key = 1 [(force) = true]; // If the datetime does not have a valid state yet. // Equivalent to `!obj->has_state()` - inverse logic to make state packets smaller bool missing_state = 2; @@ -2356,7 +2409,7 @@ message DateTimeCommandRequest { option (no_delay) = true; option (base_class) = "CommandProtoMessage"; - fixed32 key = 1; + fixed32 key = 1 [(force) = true]; fixed32 epoch_seconds = 2; uint32 device_id = 3 [(field_ifdef) = "USE_DEVICES"]; } @@ -2369,7 +2422,7 @@ message ListEntitiesUpdateResponse { option (ifdef) = "USE_UPDATE"; string object_id = 1; - fixed32 key = 2; + fixed32 key = 2 [(force) = true]; string name = 3; reserved 4; // Deprecated: was string unique_id @@ -2386,7 +2439,7 @@ message UpdateStateResponse { option (ifdef) = "USE_UPDATE"; option (no_delay) = true; - fixed32 key = 1; + fixed32 key = 1 [(force) = true]; bool missing_state = 2; bool in_progress = 3; bool has_progress = 4; @@ -2410,7 +2463,7 @@ message UpdateCommandRequest { option (no_delay) = true; option (base_class) = "CommandProtoMessage"; - fixed32 key = 1; + fixed32 key = 1 [(force) = true]; UpdateCommand command = 2; uint32 device_id = 3 [(field_ifdef) = "USE_DEVICES"]; } @@ -2452,13 +2505,14 @@ message ListEntitiesInfraredResponse { option (ifdef) = "USE_INFRARED"; string object_id = 1; - fixed32 key = 2; + fixed32 key = 2 [(force) = true]; string name = 3; string icon = 4 [(field_ifdef) = "USE_ENTITY_ICON"]; bool disabled_by_default = 5; EntityCategory entity_category = 6; uint32 device_id = 7 [(field_ifdef) = "USE_DEVICES"]; uint32 capabilities = 8; // Bitfield of InfraredCapabilityFlags + uint32 receiver_frequency = 9; // Demodulation frequency of the IR receiver in Hz (0 = unspecified) } // Command to transmit infrared/RF data using raw timings @@ -2468,7 +2522,7 @@ message InfraredRFTransmitRawTimingsRequest { option (ifdef) = "USE_IR_RF"; uint32 device_id = 1 [(field_ifdef) = "USE_DEVICES"]; - fixed32 key = 2; // Key identifying the transmitter instance + fixed32 key = 2 [(force) = true]; // Key identifying the transmitter instance uint32 carrier_frequency = 3; // Carrier frequency in Hz uint32 repeat_count = 4; // Number of times to transmit (1 = once, 2 = twice, etc.) repeated sint32 timings = 5 [packed = true, (packed_buffer) = true]; // Raw timings in microseconds (zigzag-encoded): positive = mark (LED/TX on), negative = space (LED/TX off) @@ -2482,6 +2536,137 @@ message InfraredRFReceiveEvent { option (no_delay) = true; uint32 device_id = 1 [(field_ifdef) = "USE_DEVICES"]; - fixed32 key = 2; // Key identifying the receiver instance + fixed32 key = 2 [(force) = true]; // Key identifying the receiver instance repeated sint32 timings = 3 [packed = true, (container_pointer_no_template) = "std::vector"]; // Raw timings in microseconds (zigzag-encoded): alternating mark/space periods } + +// ==================== SERIAL PROXY ==================== + +enum SerialProxyParity { + SERIAL_PROXY_PARITY_NONE = 0; + SERIAL_PROXY_PARITY_EVEN = 1; + SERIAL_PROXY_PARITY_ODD = 2; +} + +// Configure UART parameters for a serial proxy instance +message SerialProxyConfigureRequest { + option (id) = 138; + option (source) = SOURCE_CLIENT; + option (ifdef) = "USE_SERIAL_PROXY"; + + uint32 instance = 1; // Instance index (0-based) + uint32 baudrate = 2; // Baud rate in bits per second + bool flow_control = 3; // Enable hardware flow control + SerialProxyParity parity = 4; // Parity setting + uint32 stop_bits = 5; // Number of stop bits (1 or 2) + uint32 data_size = 6; // Number of data bits (5-8) +} + +// Data received from a serial device, forwarded to clients +message SerialProxyDataReceived { + option (id) = 139; + option (source) = SOURCE_SERVER; + option (ifdef) = "USE_SERIAL_PROXY"; + option (no_delay) = true; + + uint32 instance = 1; // Instance index (0-based) + bytes data = 2; // Raw data received from the serial device +} + +// Write data to a serial device +message SerialProxyWriteRequest { + option (id) = 140; + option (source) = SOURCE_CLIENT; + option (ifdef) = "USE_SERIAL_PROXY"; + option (no_delay) = true; + + uint32 instance = 1; // Instance index (0-based) + bytes data = 2; // Raw data to write to the serial device +} + +// Set modem control pin states (RTS and DTR) +message SerialProxySetModemPinsRequest { + option (id) = 141; + option (source) = SOURCE_CLIENT; + option (ifdef) = "USE_SERIAL_PROXY"; + + uint32 instance = 1; // Instance index (0-based) + uint32 line_states = 2; // Bitmask of SerialProxyLineStateFlags +} + +// Request current modem control pin states +message SerialProxyGetModemPinsRequest { + option (id) = 142; + option (source) = SOURCE_CLIENT; + option (ifdef) = "USE_SERIAL_PROXY"; + + uint32 instance = 1; // Instance index (0-based) +} + +// Response with current modem control pin states +message SerialProxyGetModemPinsResponse { + option (id) = 143; + option (source) = SOURCE_SERVER; + option (ifdef) = "USE_SERIAL_PROXY"; + + uint32 instance = 1; // Instance index (0-based) + uint32 line_states = 2; // Bitmask of SerialProxyLineStateFlags +} + +enum SerialProxyRequestType { + SERIAL_PROXY_REQUEST_TYPE_SUBSCRIBE = 0; // Subscribe to receive data from this serial proxy instance + SERIAL_PROXY_REQUEST_TYPE_UNSUBSCRIBE = 1; // Unsubscribe from this serial proxy instance + SERIAL_PROXY_REQUEST_TYPE_FLUSH = 2; // Flush the serial port (block until all TX data is sent) +} + +enum SerialProxyStatus { + SERIAL_PROXY_STATUS_OK = 0; // Completed successfully; TX drain confirmed + SERIAL_PROXY_STATUS_ASSUMED_SUCCESS = 1; // Platform cannot confirm TX drain; success assumed + SERIAL_PROXY_STATUS_ERROR = 2; // Driver or hardware error + SERIAL_PROXY_STATUS_TIMEOUT = 3; // Timed out before TX completed + SERIAL_PROXY_STATUS_NOT_SUPPORTED = 4; // Request type not supported by this instance +} + +// Generic request message for simple serial proxy operations +message SerialProxyRequest { + option (id) = 144; + option (source) = SOURCE_CLIENT; + option (ifdef) = "USE_SERIAL_PROXY"; + + uint32 instance = 1; // Instance index (0-based) + SerialProxyRequestType type = 2; // Request type +} + +// Response to a SerialProxyRequest (e.g. flush completion or failure) +message SerialProxyRequestResponse { + option (id) = 147; + option (source) = SOURCE_SERVER; + option (ifdef) = "USE_SERIAL_PROXY"; + + uint32 instance = 1; // Instance index (0-based) + SerialProxyRequestType type = 2; // Which request type this responds to + SerialProxyStatus status = 3; // Result status + string error_message = 4; // Additional detail on failure (optional) +} + +// ==================== BLUETOOTH CONNECTION PARAMS ==================== +message BluetoothSetConnectionParamsRequest { + option (id) = 145; + option (source) = SOURCE_CLIENT; + option (ifdef) = "USE_BLUETOOTH_PROXY"; + + uint64 address = 1; + uint32 min_interval = 2; // units of 1.25ms + uint32 max_interval = 3; // units of 1.25ms + uint32 latency = 4; + uint32 timeout = 5; // units of 10ms +} + +message BluetoothSetConnectionParamsResponse { + option (id) = 146; + option (source) = SOURCE_SERVER; + option (ifdef) = "USE_BLUETOOTH_PROXY"; + + uint64 address = 1; + int32 error = 2; +} diff --git a/esphome/components/api/api_buffer.cpp b/esphome/components/api/api_buffer.cpp new file mode 100644 index 0000000000..6db18b0365 --- /dev/null +++ b/esphome/components/api/api_buffer.cpp @@ -0,0 +1,13 @@ +#include "api_buffer.h" + +namespace esphome::api { + +void APIBuffer::grow_(size_t n) { + auto new_data = make_buffer(n); + if (this->size_) + std::memcpy(new_data.get(), this->data_.get(), this->size_); + this->data_ = std::move(new_data); + this->capacity_ = n; +} + +} // namespace esphome::api diff --git a/esphome/components/api/api_buffer.h b/esphome/components/api/api_buffer.h new file mode 100644 index 0000000000..1d0cccf61c --- /dev/null +++ b/esphome/components/api/api_buffer.h @@ -0,0 +1,73 @@ +#pragma once + +#include +#include +#include + +#include "esphome/core/defines.h" +#include "esphome/core/helpers.h" + +namespace esphome::api { + +/// Helper to use make_unique_for_overwrite where available (skips zero-fill), +/// falling back to make_unique on older GCC (ESP8266, LibreTiny). +inline std::unique_ptr make_buffer(size_t n) { +#if defined(USE_ESP8266) || defined(USE_LIBRETINY) + return std::make_unique(n); +#else + return std::make_unique_for_overwrite(n); +#endif +} + +/// Byte buffer that skips zero-initialization on resize(). +/// +/// std::vector::resize() zero-fills new bytes via memset. For the +/// shared protobuf write buffer, every byte is overwritten by the encoder, +/// making the zero-fill pure waste. For the receive buffer, bytes are +/// overwritten by socket reads. +/// +/// Designed for bulk clear/resize/overwrite patterns. grow_() allocates +/// exactly the requested size (no growth factor) since callers resize to +/// known sizes rather than appending incrementally. +/// +/// Safe because: callers always write exactly the number of bytes they +/// resize for. In the protobuf write path, debug_check_bounds_ validates +/// writes in debug builds. +class APIBuffer { + public: + void clear() { this->size_ = 0; } + inline void reserve(size_t n) ESPHOME_ALWAYS_INLINE { + if (n > this->capacity_) + this->grow_(n); + } + inline void resize(size_t n) ESPHOME_ALWAYS_INLINE { + this->reserve(n); + this->size_ = n; // no zero-fill + } + /// Reserve capacity for max(reserve_size, new_size) bytes, then set size to new_size. + /// Single grow_ check regardless of argument order. + inline void reserve_and_resize(size_t reserve_size, size_t new_size) ESPHOME_ALWAYS_INLINE { + this->reserve(std::max(reserve_size, new_size)); + this->size_ = new_size; + } + uint8_t *data() { return this->data_.get(); } + const uint8_t *data() const { return this->data_.get(); } + size_t size() const { return this->size_; } + bool empty() const { return this->size_ == 0; } + uint8_t &operator[](size_t i) { return this->data_[i]; } + const uint8_t &operator[](size_t i) const { return this->data_[i]; } + /// Release all memory (equivalent to std::vector swap trick). + void release() { + this->data_.reset(); + this->size_ = 0; + this->capacity_ = 0; + } + + protected: + void grow_(size_t n); + std::unique_ptr data_; + size_t size_{0}; + size_t capacity_{0}; +}; + +} // namespace esphome::api diff --git a/esphome/components/api/api_connection.cpp b/esphome/components/api/api_connection.cpp index 1626f395e6..0f456ecd0c 100644 --- a/esphome/components/api/api_connection.cpp +++ b/esphome/components/api/api_connection.cpp @@ -60,6 +60,15 @@ static constexpr uint8_t MAX_MESSAGES_PER_LOOP = 5; static constexpr uint8_t MAX_PING_RETRIES = 60; static constexpr uint16_t PING_RETRY_INTERVAL = 1000; static constexpr uint32_t KEEPALIVE_DISCONNECT_TIMEOUT = (KEEPALIVE_TIMEOUT_MS * 5) / 2; +// Timeout for completing the handshake (Noise transport + HelloRequest). +// A stalled handshake from a buggy client or network glitch holds a connection +// slot, which can prevent legitimate clients from reconnecting. Also hardens +// against the less likely case of intentional connection slot exhaustion. +// +// 60s is intentionally high: on ESP8266 with power_save_mode: LIGHT and weak +// WiFi (-70 dBm+), TCP retransmissions push real-world handshake times to +// 28-30s. See https://github.com/esphome/esphome/issues/14999 +static constexpr uint32_t HANDSHAKE_TIMEOUT_MS = 60000; static constexpr auto ESPHOME_VERSION_REF = StringRef::from_lit(ESPHOME_VERSION); @@ -109,9 +118,10 @@ APIConnection::APIConnection(std::unique_ptr sock, APIServer *pa this->helper_ = std::unique_ptr{new APIPlaintextFrameHelper(std::move(sock))}; } #elif defined(USE_API_PLAINTEXT) - this->helper_ = std::unique_ptr{new APIPlaintextFrameHelper(std::move(sock))}; + this->helper_ = std::unique_ptr{new APIPlaintextFrameHelper(std::move(sock))}; #elif defined(USE_API_NOISE) - this->helper_ = std::unique_ptr{new APINoiseFrameHelper(std::move(sock), parent->get_noise_ctx())}; + this->helper_ = + std::unique_ptr{new APINoiseFrameHelper(std::move(sock), parent->get_noise_ctx())}; #else #error "No frame helper defined" #endif @@ -122,8 +132,6 @@ APIConnection::APIConnection(std::unique_ptr sock, APIServer *pa #endif } -uint32_t APIConnection::get_batch_delay_ms_() const { return this->parent_->get_batch_delay(); } - void APIConnection::start() { this->last_traffic_ = App.get_loop_component_start_time(); @@ -133,8 +141,8 @@ void APIConnection::start() { return; } // Initialize client name with peername (IP address) until Hello message provides actual name - const char *peername = this->helper_->get_client_peername(); - this->helper_->set_client_name(peername, strlen(peername)); + char peername[socket::SOCKADDR_STR_LEN]; + this->helper_->set_client_name(this->helper_->get_peername_to(peername), strlen(peername)); } APIConnection::~APIConnection() { @@ -149,6 +157,18 @@ APIConnection::~APIConnection() { voice_assistant::global_voice_assistant->client_subscription(this, false); } #endif +#ifdef USE_ZWAVE_PROXY + if (zwave_proxy::global_zwave_proxy != nullptr && zwave_proxy::global_zwave_proxy->get_api_connection() == this) { + zwave_proxy::global_zwave_proxy->zwave_proxy_request(this, enums::ZWAVE_PROXY_REQUEST_TYPE_UNSUBSCRIBE); + } +#endif +#ifdef USE_SERIAL_PROXY + for (auto *proxy : App.get_serial_proxies()) { + if (proxy->get_api_connection() == this) { + proxy->serial_proxy_request(this, enums::SERIAL_PROXY_REQUEST_TYPE_UNSUBSCRIBE); + } + } +#endif } void APIConnection::destroy_active_iterator_() { @@ -179,8 +199,8 @@ void APIConnection::begin_iterator_(ActiveIterator type) { void APIConnection::loop() { if (this->flags_.next_close) { - // requested a disconnect - this->helper_->close(); + // requested a disconnect - don't close socket here, let APIServer::loop() do it + // so getpeername() still works for the disconnect trigger this->flags_.remove = true; return; } @@ -205,9 +225,14 @@ void APIConnection::loop() { this->fatal_error_with_log_(LOG_STR("Reading failed"), err); return; } else { - this->last_traffic_ = now; + // Only update last_traffic_ after authentication to ensure the + // handshake timeout is an absolute deadline from connection start. + // Pre-auth messages (e.g. PingRequest) must not reset the timer. + if (this->is_authenticated()) { + this->last_traffic_ = now; + } // read a packet - this->read_message(buffer.data_len, buffer.type, buffer.data); + this->read_message_(buffer.data_len, buffer.type, buffer.data); if (this->flags_.remove) return; } @@ -219,55 +244,24 @@ void APIConnection::loop() { this->process_batch_(); } - switch (this->active_iterator_) { - case ActiveIterator::LIST_ENTITIES: - if (this->iterator_storage_.list_entities.completed()) { - this->destroy_active_iterator_(); - if (this->flags_.state_subscription) { - this->begin_iterator_(ActiveIterator::INITIAL_STATE); - } - } else { - this->process_iterator_batch_(this->iterator_storage_.list_entities); - } - break; - case ActiveIterator::INITIAL_STATE: - if (this->iterator_storage_.initial_state.completed()) { - this->destroy_active_iterator_(); - // Process any remaining batched messages immediately - if (!this->deferred_batch_.empty()) { - this->process_batch_(); - } - // Now that everything is sent, enable immediate sending for future state changes - this->flags_.should_try_send_immediately = true; - // Release excess memory from buffers that grew during initial sync - this->deferred_batch_.release_buffer(); - this->helper_->release_buffers(); - } else { - this->process_iterator_batch_(this->iterator_storage_.initial_state); - } - break; - case ActiveIterator::NONE: - break; + if (this->active_iterator_ != ActiveIterator::NONE) { + this->process_active_iterator_(); } - if (this->flags_.sent_ping) { - // Disconnect if not responded within 2.5*keepalive - if (now - this->last_traffic_ > KEEPALIVE_DISCONNECT_TIMEOUT) { - on_fatal_error(); - this->log_client_(ESPHOME_LOG_LEVEL_WARN, LOG_STR("is unresponsive; disconnecting")); - } - } else if (now - this->last_traffic_ > KEEPALIVE_TIMEOUT_MS && !this->flags_.remove) { - // Only send ping if we're not disconnecting - ESP_LOGVV(TAG, "Sending keepalive PING"); - PingRequest req; - this->flags_.sent_ping = this->send_message(req, PingRequest::MESSAGE_TYPE); - if (!this->flags_.sent_ping) { - // If we can't send the ping request directly (tx_buffer full), - // schedule it at the front of the batch so it will be sent with priority - ESP_LOGW(TAG, "Buffer full, ping queued"); - this->schedule_message_front_(nullptr, PingRequest::MESSAGE_TYPE, PingRequest::ESTIMATED_SIZE); - this->flags_.sent_ping = true; // Mark as sent to avoid scheduling multiple pings - } + // Disconnect clients that haven't completed the handshake in time. + // Stale half-open connections from buggy clients or network issues can + // accumulate and block legitimate clients from reconnecting. + if (!this->is_authenticated() && now - this->last_traffic_ > HANDSHAKE_TIMEOUT_MS) { + this->on_fatal_error(); + this->log_client_(ESPHOME_LOG_LEVEL_WARN, LOG_STR("handshake timeout; disconnecting")); + return; + } + + // Keepalive: only call into the cold path when enough time has elapsed. + // When sent_ping is true, last_traffic_ hasn't been updated so this + // condition is already satisfied — covers both send-ping and disconnect cases. + if (now - this->last_traffic_ > KEEPALIVE_TIMEOUT_MS) { + this->check_keepalive_(now); } #ifdef USE_API_HOMEASSISTANT_STATES @@ -283,80 +277,138 @@ void APIConnection::loop() { #endif } -bool APIConnection::send_disconnect_response(const DisconnectRequest &msg) { +void APIConnection::check_keepalive_(uint32_t now) { + // Caller guarantees: now - last_traffic_ > KEEPALIVE_TIMEOUT_MS + if (this->flags_.sent_ping) { + // Disconnect if not responded within 2.5*keepalive + if (now - this->last_traffic_ > KEEPALIVE_DISCONNECT_TIMEOUT) { + on_fatal_error(); + this->log_client_(ESPHOME_LOG_LEVEL_WARN, LOG_STR("is unresponsive; disconnecting")); + } + } else if (!this->flags_.remove) { + // Only send ping if we're not disconnecting + ESP_LOGVV(TAG, "Sending keepalive PING"); + PingRequest req; + this->flags_.sent_ping = this->send_message(req); + if (!this->flags_.sent_ping) { + // If we can't send the ping request directly (tx_buffer full), + // schedule it at the front of the batch so it will be sent with priority + ESP_LOGW(TAG, "Buffer full, ping queued"); + this->schedule_message_front_(nullptr, PingRequest::MESSAGE_TYPE, PingRequest::ESTIMATED_SIZE); + this->flags_.sent_ping = true; // Mark as sent to avoid scheduling multiple pings + } + } +} + +void APIConnection::process_active_iterator_() { + // Caller ensures active_iterator_ != NONE + if (this->active_iterator_ == ActiveIterator::LIST_ENTITIES) { + if (this->iterator_storage_.list_entities.completed()) { + this->destroy_active_iterator_(); + if (this->flags_.state_subscription) { + this->begin_iterator_(ActiveIterator::INITIAL_STATE); + } + } else { + this->process_iterator_batch_(this->iterator_storage_.list_entities); + } + } else { // INITIAL_STATE + if (this->iterator_storage_.initial_state.completed()) { + this->destroy_active_iterator_(); + // Process any remaining batched messages immediately + if (!this->deferred_batch_.empty()) { + this->process_batch_(); + } + // Now that everything is sent, enable immediate sending for future state changes + this->flags_.should_try_send_immediately = true; + // Release excess memory from buffers that grew during initial sync + this->deferred_batch_.release_buffer(); + this->helper_->release_buffers(); + } else { + this->process_iterator_batch_(this->iterator_storage_.initial_state); + } + } +} + +void APIConnection::process_iterator_batch_(ComponentIterator &iterator) { + size_t initial_size = this->deferred_batch_.size(); + size_t max_batch = this->get_max_batch_size_(); + while (!iterator.completed() && (this->deferred_batch_.size() - initial_size) < max_batch) { + iterator.advance(); + } + + // If the batch is full, process it immediately + // Note: iterator.advance() already calls schedule_batch_() via schedule_message_() + if (this->deferred_batch_.size() >= max_batch) { + this->process_batch_(); + } +} + +bool APIConnection::send_disconnect_response_() { // remote initiated disconnect_client // don't close yet, we still need to send the disconnect response // close will happen on next loop this->log_client_(ESPHOME_LOG_LEVEL_DEBUG, LOG_STR("disconnected")); this->flags_.next_close = true; DisconnectResponse resp; - return this->send_message(resp, DisconnectResponse::MESSAGE_TYPE); + return this->send_message(resp); } -void APIConnection::on_disconnect_response(const DisconnectResponse &value) { - this->helper_->close(); +void APIConnection::on_disconnect_response() { + // Don't close socket here, let APIServer::loop() do it + // so getpeername() still works for the disconnect trigger this->flags_.remove = true; } -// Encodes a message to the buffer and returns the total number of bytes used, -// including header and footer overhead. Returns 0 if the message doesn't fit. -uint16_t APIConnection::encode_message_to_buffer(ProtoMessage &msg, uint8_t message_type, APIConnection *conn, - uint32_t remaining_size, bool is_single) { -#ifdef HAS_PROTO_MESSAGE_DUMP - // If in log-only mode, just log and return - if (conn->flags_.log_only_mode) { - DumpBuffer dump_buf; - conn->log_send_message_(msg.message_name(), msg.dump_to(dump_buf)); - return 1; // Return non-zero to indicate "success" for logging - } +uint16_t APIConnection::fill_and_encode_entity_state(EntityBase *entity, StateResponseProtoMessage &msg, + CalculateSizeFn size_fn, MessageEncodeFn encode_fn, + APIConnection *conn, uint32_t remaining_size) { + msg.key = entity->get_object_id_hash(); +#ifdef USE_DEVICES + msg.device_id = entity->get_device_id(); #endif + return encode_to_buffer(size_fn(&msg), encode_fn, &msg, conn, remaining_size); +} - // Calculate size - ProtoSize size_calc; - msg.calculate_size(size_calc); - uint32_t calculated_size = size_calc.get_size(); +uint16_t APIConnection::fill_and_encode_entity_info(EntityBase *entity, InfoResponseProtoMessage &msg, + CalculateSizeFn size_fn, MessageEncodeFn encode_fn, + APIConnection *conn, uint32_t remaining_size) { + // Set common fields that are shared by all entity types + msg.key = entity->get_object_id_hash(); - // Cache frame sizes to avoid repeated virtual calls - const uint8_t header_padding = conn->helper_->frame_header_padding(); - const uint8_t footer_size = conn->helper_->frame_footer_size(); - - // Calculate total size with padding for buffer allocation - size_t total_calculated_size = calculated_size + header_padding + footer_size; - - // Check if it fits - if (total_calculated_size > remaining_size) { - return 0; // Doesn't fit + // API 1.14+ clients compute object_id client-side from the entity name + // For older clients, we must send object_id for backward compatibility + // See: https://github.com/esphome/backlog/issues/76 + // TODO: Remove this backward compat code before 2026.7.0 - all clients should support API 1.14 by then + // Buffer must remain in scope until encode_to_buffer is called + char object_id_buf[OBJECT_ID_MAX_LEN]; + if (!conn->client_supports_api_version(1, 14)) { + msg.object_id = entity->get_object_id_to(object_id_buf); } - // Get buffer size after allocation (which includes header padding) - std::vector &shared_buf = conn->parent_->get_shared_buffer_ref(); - - if (is_single || conn->flags_.batch_first_message) { - // Single message or first batch message - conn->prepare_first_message_buffer(shared_buf, header_padding, total_calculated_size); - if (conn->flags_.batch_first_message) { - conn->flags_.batch_first_message = false; - } - } else { - // Batch message second or later - // Add padding for previous message footer + this message header - size_t current_size = shared_buf.size(); - shared_buf.reserve(current_size + total_calculated_size); - shared_buf.resize(current_size + footer_size + header_padding); + if (entity->has_own_name()) { + msg.name = entity->get_name(); } - // Encode directly into buffer - size_t size_before_encode = shared_buf.size(); - msg.encode({&shared_buf}); + // Set common EntityBase properties +#ifdef USE_ENTITY_ICON + char icon_buf[MAX_ICON_LENGTH]; + msg.icon = StringRef(entity->get_icon_to(icon_buf)); +#endif + msg.disabled_by_default = entity->is_disabled_by_default(); + msg.entity_category = static_cast(entity->get_entity_category()); +#ifdef USE_DEVICES + msg.device_id = entity->get_device_id(); +#endif + return encode_to_buffer(size_fn(&msg), encode_fn, &msg, conn, remaining_size); +} - // Calculate actual encoded size (not including header that was already added) - size_t actual_payload_size = shared_buf.size() - size_before_encode; - - // Return actual total size (header + actual payload + footer) - size_t actual_total_size = header_padding + actual_payload_size + footer_size; - - // Verify that calculate_size() returned the correct value - assert(calculated_size == actual_payload_size); - return static_cast(actual_total_size); +uint16_t APIConnection::fill_and_encode_entity_info_with_device_class(EntityBase *entity, InfoResponseProtoMessage &msg, + StringRef &device_class_field, + CalculateSizeFn size_fn, + MessageEncodeFn encode_fn, APIConnection *conn, + uint32_t remaining_size) { + char dc_buf[MAX_DEVICE_CLASS_LENGTH]; + device_class_field = StringRef(entity->get_device_class_to(dc_buf)); + return fill_and_encode_entity_info(entity, msg, size_fn, encode_fn, conn, remaining_size); } #ifdef USE_BINARY_SENSOR @@ -365,24 +417,19 @@ bool APIConnection::send_binary_sensor_state(binary_sensor::BinarySensor *binary BinarySensorStateResponse::ESTIMATED_SIZE); } -uint16_t APIConnection::try_send_binary_sensor_state(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, - bool is_single) { +uint16_t APIConnection::try_send_binary_sensor_state(EntityBase *entity, APIConnection *conn, uint32_t remaining_size) { auto *binary_sensor = static_cast(entity); BinarySensorStateResponse resp; resp.state = binary_sensor->state; resp.missing_state = !binary_sensor->has_state(); - return fill_and_encode_entity_state(binary_sensor, resp, BinarySensorStateResponse::MESSAGE_TYPE, conn, - remaining_size, is_single); + return fill_and_encode_entity_state(binary_sensor, resp, conn, remaining_size); } -uint16_t APIConnection::try_send_binary_sensor_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, - bool is_single) { +uint16_t APIConnection::try_send_binary_sensor_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size) { auto *binary_sensor = static_cast(entity); ListEntitiesBinarySensorResponse msg; - msg.device_class = binary_sensor->get_device_class_ref(); msg.is_status_binary_sensor = binary_sensor->is_status_binary_sensor(); - return fill_and_encode_entity_info(binary_sensor, msg, ListEntitiesBinarySensorResponse::MESSAGE_TYPE, conn, - remaining_size, is_single); + return fill_and_encode_entity_info_with_device_class(binary_sensor, msg, msg.device_class, conn, remaining_size); } #endif @@ -390,8 +437,7 @@ uint16_t APIConnection::try_send_binary_sensor_info(EntityBase *entity, APIConne bool APIConnection::send_cover_state(cover::Cover *cover) { return this->send_message_smart_(cover, CoverStateResponse::MESSAGE_TYPE, CoverStateResponse::ESTIMATED_SIZE); } -uint16_t APIConnection::try_send_cover_state(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, - bool is_single) { +uint16_t APIConnection::try_send_cover_state(EntityBase *entity, APIConnection *conn, uint32_t remaining_size) { auto *cover = static_cast(entity); CoverStateResponse msg; auto traits = cover->get_traits(); @@ -399,10 +445,9 @@ uint16_t APIConnection::try_send_cover_state(EntityBase *entity, APIConnection * if (traits.get_supports_tilt()) msg.tilt = cover->tilt; msg.current_operation = static_cast(cover->current_operation); - return fill_and_encode_entity_state(cover, msg, CoverStateResponse::MESSAGE_TYPE, conn, remaining_size, is_single); + return fill_and_encode_entity_state(cover, msg, conn, remaining_size); } -uint16_t APIConnection::try_send_cover_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, - bool is_single) { +uint16_t APIConnection::try_send_cover_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size) { auto *cover = static_cast(entity); ListEntitiesCoverResponse msg; auto traits = cover->get_traits(); @@ -410,11 +455,9 @@ uint16_t APIConnection::try_send_cover_info(EntityBase *entity, APIConnection *c msg.supports_position = traits.get_supports_position(); msg.supports_tilt = traits.get_supports_tilt(); msg.supports_stop = traits.get_supports_stop(); - msg.device_class = cover->get_device_class_ref(); - return fill_and_encode_entity_info(cover, msg, ListEntitiesCoverResponse::MESSAGE_TYPE, conn, remaining_size, - is_single); + return fill_and_encode_entity_info_with_device_class(cover, msg, msg.device_class, conn, remaining_size); } -void APIConnection::cover_command(const CoverCommandRequest &msg) { +void APIConnection::on_cover_command_request(const CoverCommandRequest &msg) { ENTITY_COMMAND_MAKE_CALL(cover::Cover, cover, cover) if (msg.has_position) call.set_position(msg.position); @@ -430,8 +473,7 @@ void APIConnection::cover_command(const CoverCommandRequest &msg) { bool APIConnection::send_fan_state(fan::Fan *fan) { return this->send_message_smart_(fan, FanStateResponse::MESSAGE_TYPE, FanStateResponse::ESTIMATED_SIZE); } -uint16_t APIConnection::try_send_fan_state(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, - bool is_single) { +uint16_t APIConnection::try_send_fan_state(EntityBase *entity, APIConnection *conn, uint32_t remaining_size) { auto *fan = static_cast(entity); FanStateResponse msg; auto traits = fan->get_traits(); @@ -445,10 +487,9 @@ uint16_t APIConnection::try_send_fan_state(EntityBase *entity, APIConnection *co msg.direction = static_cast(fan->direction); if (traits.supports_preset_modes() && fan->has_preset_mode()) msg.preset_mode = fan->get_preset_mode(); - return fill_and_encode_entity_state(fan, msg, FanStateResponse::MESSAGE_TYPE, conn, remaining_size, is_single); + return fill_and_encode_entity_state(fan, msg, conn, remaining_size); } -uint16_t APIConnection::try_send_fan_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, - bool is_single) { +uint16_t APIConnection::try_send_fan_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size) { auto *fan = static_cast(entity); ListEntitiesFanResponse msg; auto traits = fan->get_traits(); @@ -457,9 +498,9 @@ uint16_t APIConnection::try_send_fan_info(EntityBase *entity, APIConnection *con msg.supports_direction = traits.supports_direction(); msg.supported_speed_count = traits.supported_speed_count(); msg.supported_preset_modes = &traits.supported_preset_modes(); - return fill_and_encode_entity_info(fan, msg, ListEntitiesFanResponse::MESSAGE_TYPE, conn, remaining_size, is_single); + return fill_and_encode_entity_info(fan, msg, conn, remaining_size); } -void APIConnection::fan_command(const FanCommandRequest &msg) { +void APIConnection::on_fan_command_request(const FanCommandRequest &msg) { ENTITY_COMMAND_MAKE_CALL(fan::Fan, fan, fan) if (msg.has_state) call.set_state(msg.state); @@ -481,8 +522,7 @@ void APIConnection::fan_command(const FanCommandRequest &msg) { bool APIConnection::send_light_state(light::LightState *light) { return this->send_message_smart_(light, LightStateResponse::MESSAGE_TYPE, LightStateResponse::ESTIMATED_SIZE); } -uint16_t APIConnection::try_send_light_state(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, - bool is_single) { +uint16_t APIConnection::try_send_light_state(EntityBase *entity, APIConnection *conn, uint32_t remaining_size) { auto *light = static_cast(entity); LightStateResponse resp; auto values = light->remote_values; @@ -501,10 +541,9 @@ uint16_t APIConnection::try_send_light_state(EntityBase *entity, APIConnection * if (light->supports_effects()) { resp.effect = light->get_effect_name(); } - return fill_and_encode_entity_state(light, resp, LightStateResponse::MESSAGE_TYPE, conn, remaining_size, is_single); + return fill_and_encode_entity_state(light, resp, conn, remaining_size); } -uint16_t APIConnection::try_send_light_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, - bool is_single) { +uint16_t APIConnection::try_send_light_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size) { auto *light = static_cast(entity); ListEntitiesLightResponse msg; auto traits = light->get_traits(); @@ -527,10 +566,9 @@ uint16_t APIConnection::try_send_light_info(EntityBase *entity, APIConnection *c } } msg.effects = &effects_list; - return fill_and_encode_entity_info(light, msg, ListEntitiesLightResponse::MESSAGE_TYPE, conn, remaining_size, - is_single); + return fill_and_encode_entity_info(light, msg, conn, remaining_size); } -void APIConnection::light_command(const LightCommandRequest &msg) { +void APIConnection::on_light_command_request(const LightCommandRequest &msg) { ENTITY_COMMAND_MAKE_CALL(light::LightState, light, light) if (msg.has_state) call.set_state(msg.state); @@ -568,26 +606,22 @@ bool APIConnection::send_sensor_state(sensor::Sensor *sensor) { return this->send_message_smart_(sensor, SensorStateResponse::MESSAGE_TYPE, SensorStateResponse::ESTIMATED_SIZE); } -uint16_t APIConnection::try_send_sensor_state(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, - bool is_single) { +uint16_t APIConnection::try_send_sensor_state(EntityBase *entity, APIConnection *conn, uint32_t remaining_size) { auto *sensor = static_cast(entity); SensorStateResponse resp; resp.state = sensor->state; resp.missing_state = !sensor->has_state(); - return fill_and_encode_entity_state(sensor, resp, SensorStateResponse::MESSAGE_TYPE, conn, remaining_size, is_single); + return fill_and_encode_entity_state(sensor, resp, conn, remaining_size); } -uint16_t APIConnection::try_send_sensor_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, - bool is_single) { +uint16_t APIConnection::try_send_sensor_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size) { auto *sensor = static_cast(entity); ListEntitiesSensorResponse msg; msg.unit_of_measurement = sensor->get_unit_of_measurement_ref(); msg.accuracy_decimals = sensor->get_accuracy_decimals(); msg.force_update = sensor->get_force_update(); - msg.device_class = sensor->get_device_class_ref(); msg.state_class = static_cast(sensor->get_state_class()); - return fill_and_encode_entity_info(sensor, msg, ListEntitiesSensorResponse::MESSAGE_TYPE, conn, remaining_size, - is_single); + return fill_and_encode_entity_info_with_device_class(sensor, msg, msg.device_class, conn, remaining_size); } #endif @@ -596,25 +630,20 @@ bool APIConnection::send_switch_state(switch_::Switch *a_switch) { return this->send_message_smart_(a_switch, SwitchStateResponse::MESSAGE_TYPE, SwitchStateResponse::ESTIMATED_SIZE); } -uint16_t APIConnection::try_send_switch_state(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, - bool is_single) { +uint16_t APIConnection::try_send_switch_state(EntityBase *entity, APIConnection *conn, uint32_t remaining_size) { auto *a_switch = static_cast(entity); SwitchStateResponse resp; resp.state = a_switch->state; - return fill_and_encode_entity_state(a_switch, resp, SwitchStateResponse::MESSAGE_TYPE, conn, remaining_size, - is_single); + return fill_and_encode_entity_state(a_switch, resp, conn, remaining_size); } -uint16_t APIConnection::try_send_switch_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, - bool is_single) { +uint16_t APIConnection::try_send_switch_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size) { auto *a_switch = static_cast(entity); ListEntitiesSwitchResponse msg; msg.assumed_state = a_switch->assumed_state(); - msg.device_class = a_switch->get_device_class_ref(); - return fill_and_encode_entity_info(a_switch, msg, ListEntitiesSwitchResponse::MESSAGE_TYPE, conn, remaining_size, - is_single); + return fill_and_encode_entity_info_with_device_class(a_switch, msg, msg.device_class, conn, remaining_size); } -void APIConnection::switch_command(const SwitchCommandRequest &msg) { +void APIConnection::on_switch_command_request(const SwitchCommandRequest &msg) { ENTITY_COMMAND_GET(switch_::Switch, a_switch, switch) if (msg.state) { @@ -631,22 +660,17 @@ bool APIConnection::send_text_sensor_state(text_sensor::TextSensor *text_sensor) TextSensorStateResponse::ESTIMATED_SIZE); } -uint16_t APIConnection::try_send_text_sensor_state(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, - bool is_single) { +uint16_t APIConnection::try_send_text_sensor_state(EntityBase *entity, APIConnection *conn, uint32_t remaining_size) { auto *text_sensor = static_cast(entity); TextSensorStateResponse resp; resp.state = StringRef(text_sensor->state); resp.missing_state = !text_sensor->has_state(); - return fill_and_encode_entity_state(text_sensor, resp, TextSensorStateResponse::MESSAGE_TYPE, conn, remaining_size, - is_single); + return fill_and_encode_entity_state(text_sensor, resp, conn, remaining_size); } -uint16_t APIConnection::try_send_text_sensor_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, - bool is_single) { +uint16_t APIConnection::try_send_text_sensor_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size) { auto *text_sensor = static_cast(entity); ListEntitiesTextSensorResponse msg; - msg.device_class = text_sensor->get_device_class_ref(); - return fill_and_encode_entity_info(text_sensor, msg, ListEntitiesTextSensorResponse::MESSAGE_TYPE, conn, - remaining_size, is_single); + return fill_and_encode_entity_info_with_device_class(text_sensor, msg, msg.device_class, conn, remaining_size); } #endif @@ -654,8 +678,7 @@ uint16_t APIConnection::try_send_text_sensor_info(EntityBase *entity, APIConnect bool APIConnection::send_climate_state(climate::Climate *climate) { return this->send_message_smart_(climate, ClimateStateResponse::MESSAGE_TYPE, ClimateStateResponse::ESTIMATED_SIZE); } -uint16_t APIConnection::try_send_climate_state(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, - bool is_single) { +uint16_t APIConnection::try_send_climate_state(EntityBase *entity, APIConnection *conn, uint32_t remaining_size) { auto *climate = static_cast(entity); ClimateStateResponse resp; auto traits = climate->get_traits(); @@ -687,11 +710,9 @@ uint16_t APIConnection::try_send_climate_state(EntityBase *entity, APIConnection resp.current_humidity = climate->current_humidity; if (traits.has_feature_flags(climate::CLIMATE_SUPPORTS_TARGET_HUMIDITY)) resp.target_humidity = climate->target_humidity; - return fill_and_encode_entity_state(climate, resp, ClimateStateResponse::MESSAGE_TYPE, conn, remaining_size, - is_single); + return fill_and_encode_entity_state(climate, resp, conn, remaining_size); } -uint16_t APIConnection::try_send_climate_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, - bool is_single) { +uint16_t APIConnection::try_send_climate_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size) { auto *climate = static_cast(entity); ListEntitiesClimateResponse msg; auto traits = climate->get_traits(); @@ -716,10 +737,9 @@ uint16_t APIConnection::try_send_climate_info(EntityBase *entity, APIConnection msg.supported_presets = &traits.get_supported_presets(); msg.supported_custom_presets = &traits.get_supported_custom_presets(); msg.supported_swing_modes = &traits.get_supported_swing_modes(); - return fill_and_encode_entity_info(climate, msg, ListEntitiesClimateResponse::MESSAGE_TYPE, conn, remaining_size, - is_single); + return fill_and_encode_entity_info(climate, msg, conn, remaining_size); } -void APIConnection::climate_command(const ClimateCommandRequest &msg) { +void APIConnection::on_climate_command_request(const ClimateCommandRequest &msg) { ENTITY_COMMAND_MAKE_CALL(climate::Climate, climate, climate) if (msg.has_mode) call.set_mode(static_cast(msg.mode)); @@ -750,29 +770,25 @@ bool APIConnection::send_number_state(number::Number *number) { return this->send_message_smart_(number, NumberStateResponse::MESSAGE_TYPE, NumberStateResponse::ESTIMATED_SIZE); } -uint16_t APIConnection::try_send_number_state(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, - bool is_single) { +uint16_t APIConnection::try_send_number_state(EntityBase *entity, APIConnection *conn, uint32_t remaining_size) { auto *number = static_cast(entity); NumberStateResponse resp; resp.state = number->state; resp.missing_state = !number->has_state(); - return fill_and_encode_entity_state(number, resp, NumberStateResponse::MESSAGE_TYPE, conn, remaining_size, is_single); + return fill_and_encode_entity_state(number, resp, conn, remaining_size); } -uint16_t APIConnection::try_send_number_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, - bool is_single) { +uint16_t APIConnection::try_send_number_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size) { auto *number = static_cast(entity); ListEntitiesNumberResponse msg; - msg.unit_of_measurement = number->traits.get_unit_of_measurement_ref(); + msg.unit_of_measurement = number->get_unit_of_measurement_ref(); msg.mode = static_cast(number->traits.get_mode()); - msg.device_class = number->traits.get_device_class_ref(); msg.min_value = number->traits.get_min_value(); msg.max_value = number->traits.get_max_value(); msg.step = number->traits.get_step(); - return fill_and_encode_entity_info(number, msg, ListEntitiesNumberResponse::MESSAGE_TYPE, conn, remaining_size, - is_single); + return fill_and_encode_entity_info_with_device_class(number, msg, msg.device_class, conn, remaining_size); } -void APIConnection::number_command(const NumberCommandRequest &msg) { +void APIConnection::on_number_command_request(const NumberCommandRequest &msg) { ENTITY_COMMAND_MAKE_CALL(number::Number, number, number) call.set_value(msg.state); call.perform(); @@ -783,24 +799,21 @@ void APIConnection::number_command(const NumberCommandRequest &msg) { bool APIConnection::send_date_state(datetime::DateEntity *date) { return this->send_message_smart_(date, DateStateResponse::MESSAGE_TYPE, DateStateResponse::ESTIMATED_SIZE); } -uint16_t APIConnection::try_send_date_state(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, - bool is_single) { +uint16_t APIConnection::try_send_date_state(EntityBase *entity, APIConnection *conn, uint32_t remaining_size) { auto *date = static_cast(entity); DateStateResponse resp; resp.missing_state = !date->has_state(); resp.year = date->year; resp.month = date->month; resp.day = date->day; - return fill_and_encode_entity_state(date, resp, DateStateResponse::MESSAGE_TYPE, conn, remaining_size, is_single); + return fill_and_encode_entity_state(date, resp, conn, remaining_size); } -uint16_t APIConnection::try_send_date_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, - bool is_single) { +uint16_t APIConnection::try_send_date_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size) { auto *date = static_cast(entity); ListEntitiesDateResponse msg; - return fill_and_encode_entity_info(date, msg, ListEntitiesDateResponse::MESSAGE_TYPE, conn, remaining_size, - is_single); + return fill_and_encode_entity_info(date, msg, conn, remaining_size); } -void APIConnection::date_command(const DateCommandRequest &msg) { +void APIConnection::on_date_command_request(const DateCommandRequest &msg) { ENTITY_COMMAND_MAKE_CALL(datetime::DateEntity, date, date) call.set_date(msg.year, msg.month, msg.day); call.perform(); @@ -811,24 +824,21 @@ void APIConnection::date_command(const DateCommandRequest &msg) { bool APIConnection::send_time_state(datetime::TimeEntity *time) { return this->send_message_smart_(time, TimeStateResponse::MESSAGE_TYPE, TimeStateResponse::ESTIMATED_SIZE); } -uint16_t APIConnection::try_send_time_state(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, - bool is_single) { +uint16_t APIConnection::try_send_time_state(EntityBase *entity, APIConnection *conn, uint32_t remaining_size) { auto *time = static_cast(entity); TimeStateResponse resp; resp.missing_state = !time->has_state(); resp.hour = time->hour; resp.minute = time->minute; resp.second = time->second; - return fill_and_encode_entity_state(time, resp, TimeStateResponse::MESSAGE_TYPE, conn, remaining_size, is_single); + return fill_and_encode_entity_state(time, resp, conn, remaining_size); } -uint16_t APIConnection::try_send_time_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, - bool is_single) { +uint16_t APIConnection::try_send_time_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size) { auto *time = static_cast(entity); ListEntitiesTimeResponse msg; - return fill_and_encode_entity_info(time, msg, ListEntitiesTimeResponse::MESSAGE_TYPE, conn, remaining_size, - is_single); + return fill_and_encode_entity_info(time, msg, conn, remaining_size); } -void APIConnection::time_command(const TimeCommandRequest &msg) { +void APIConnection::on_time_command_request(const TimeCommandRequest &msg) { ENTITY_COMMAND_MAKE_CALL(datetime::TimeEntity, time, time) call.set_time(msg.hour, msg.minute, msg.second); call.perform(); @@ -840,8 +850,7 @@ bool APIConnection::send_datetime_state(datetime::DateTimeEntity *datetime) { return this->send_message_smart_(datetime, DateTimeStateResponse::MESSAGE_TYPE, DateTimeStateResponse::ESTIMATED_SIZE); } -uint16_t APIConnection::try_send_datetime_state(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, - bool is_single) { +uint16_t APIConnection::try_send_datetime_state(EntityBase *entity, APIConnection *conn, uint32_t remaining_size) { auto *datetime = static_cast(entity); DateTimeStateResponse resp; resp.missing_state = !datetime->has_state(); @@ -849,17 +858,14 @@ uint16_t APIConnection::try_send_datetime_state(EntityBase *entity, APIConnectio ESPTime state = datetime->state_as_esptime(); resp.epoch_seconds = state.timestamp; } - return fill_and_encode_entity_state(datetime, resp, DateTimeStateResponse::MESSAGE_TYPE, conn, remaining_size, - is_single); + return fill_and_encode_entity_state(datetime, resp, conn, remaining_size); } -uint16_t APIConnection::try_send_datetime_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, - bool is_single) { +uint16_t APIConnection::try_send_datetime_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size) { auto *datetime = static_cast(entity); ListEntitiesDateTimeResponse msg; - return fill_and_encode_entity_info(datetime, msg, ListEntitiesDateTimeResponse::MESSAGE_TYPE, conn, remaining_size, - is_single); + return fill_and_encode_entity_info(datetime, msg, conn, remaining_size); } -void APIConnection::datetime_command(const DateTimeCommandRequest &msg) { +void APIConnection::on_date_time_command_request(const DateTimeCommandRequest &msg) { ENTITY_COMMAND_MAKE_CALL(datetime::DateTimeEntity, datetime, datetime) call.set_datetime(msg.epoch_seconds); call.perform(); @@ -871,29 +877,26 @@ bool APIConnection::send_text_state(text::Text *text) { return this->send_message_smart_(text, TextStateResponse::MESSAGE_TYPE, TextStateResponse::ESTIMATED_SIZE); } -uint16_t APIConnection::try_send_text_state(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, - bool is_single) { +uint16_t APIConnection::try_send_text_state(EntityBase *entity, APIConnection *conn, uint32_t remaining_size) { auto *text = static_cast(entity); TextStateResponse resp; resp.state = StringRef(text->state); resp.missing_state = !text->has_state(); - return fill_and_encode_entity_state(text, resp, TextStateResponse::MESSAGE_TYPE, conn, remaining_size, is_single); + return fill_and_encode_entity_state(text, resp, conn, remaining_size); } -uint16_t APIConnection::try_send_text_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, - bool is_single) { +uint16_t APIConnection::try_send_text_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size) { auto *text = static_cast(entity); ListEntitiesTextResponse msg; msg.mode = static_cast(text->traits.get_mode()); msg.min_length = text->traits.get_min_length(); msg.max_length = text->traits.get_max_length(); msg.pattern = text->traits.get_pattern_ref(); - return fill_and_encode_entity_info(text, msg, ListEntitiesTextResponse::MESSAGE_TYPE, conn, remaining_size, - is_single); + return fill_and_encode_entity_info(text, msg, conn, remaining_size); } -void APIConnection::text_command(const TextCommandRequest &msg) { +void APIConnection::on_text_command_request(const TextCommandRequest &msg) { ENTITY_COMMAND_MAKE_CALL(text::Text, text, text) - call.set_value(msg.state); + call.set_value(msg.state.c_str(), msg.state.size()); call.perform(); } #endif @@ -903,24 +906,21 @@ bool APIConnection::send_select_state(select::Select *select) { return this->send_message_smart_(select, SelectStateResponse::MESSAGE_TYPE, SelectStateResponse::ESTIMATED_SIZE); } -uint16_t APIConnection::try_send_select_state(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, - bool is_single) { +uint16_t APIConnection::try_send_select_state(EntityBase *entity, APIConnection *conn, uint32_t remaining_size) { auto *select = static_cast(entity); SelectStateResponse resp; resp.state = select->current_option(); resp.missing_state = !select->has_state(); - return fill_and_encode_entity_state(select, resp, SelectStateResponse::MESSAGE_TYPE, conn, remaining_size, is_single); + return fill_and_encode_entity_state(select, resp, conn, remaining_size); } -uint16_t APIConnection::try_send_select_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, - bool is_single) { +uint16_t APIConnection::try_send_select_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size) { auto *select = static_cast(entity); ListEntitiesSelectResponse msg; msg.options = &select->traits.get_options(); - return fill_and_encode_entity_info(select, msg, ListEntitiesSelectResponse::MESSAGE_TYPE, conn, remaining_size, - is_single); + return fill_and_encode_entity_info(select, msg, conn, remaining_size); } -void APIConnection::select_command(const SelectCommandRequest &msg) { +void APIConnection::on_select_command_request(const SelectCommandRequest &msg) { ENTITY_COMMAND_MAKE_CALL(select::Select, select, select) call.set_option(msg.state.c_str(), msg.state.size()); call.perform(); @@ -928,15 +928,12 @@ void APIConnection::select_command(const SelectCommandRequest &msg) { #endif #ifdef USE_BUTTON -uint16_t APIConnection::try_send_button_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, - bool is_single) { +uint16_t APIConnection::try_send_button_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size) { auto *button = static_cast(entity); ListEntitiesButtonResponse msg; - msg.device_class = button->get_device_class_ref(); - return fill_and_encode_entity_info(button, msg, ListEntitiesButtonResponse::MESSAGE_TYPE, conn, remaining_size, - is_single); + return fill_and_encode_entity_info_with_device_class(button, msg, msg.device_class, conn, remaining_size); } -void esphome::api::APIConnection::button_command(const ButtonCommandRequest &msg) { +void esphome::api::APIConnection::on_button_command_request(const ButtonCommandRequest &msg) { ENTITY_COMMAND_GET(button::Button, button, button) button->press(); } @@ -947,25 +944,22 @@ bool APIConnection::send_lock_state(lock::Lock *a_lock) { return this->send_message_smart_(a_lock, LockStateResponse::MESSAGE_TYPE, LockStateResponse::ESTIMATED_SIZE); } -uint16_t APIConnection::try_send_lock_state(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, - bool is_single) { +uint16_t APIConnection::try_send_lock_state(EntityBase *entity, APIConnection *conn, uint32_t remaining_size) { auto *a_lock = static_cast(entity); LockStateResponse resp; resp.state = static_cast(a_lock->state); - return fill_and_encode_entity_state(a_lock, resp, LockStateResponse::MESSAGE_TYPE, conn, remaining_size, is_single); + return fill_and_encode_entity_state(a_lock, resp, conn, remaining_size); } -uint16_t APIConnection::try_send_lock_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, - bool is_single) { +uint16_t APIConnection::try_send_lock_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size) { auto *a_lock = static_cast(entity); ListEntitiesLockResponse msg; msg.assumed_state = a_lock->traits.get_assumed_state(); msg.supports_open = a_lock->traits.get_supports_open(); msg.requires_code = a_lock->traits.get_requires_code(); - return fill_and_encode_entity_info(a_lock, msg, ListEntitiesLockResponse::MESSAGE_TYPE, conn, remaining_size, - is_single); + return fill_and_encode_entity_info(a_lock, msg, conn, remaining_size); } -void APIConnection::lock_command(const LockCommandRequest &msg) { +void APIConnection::on_lock_command_request(const LockCommandRequest &msg) { ENTITY_COMMAND_GET(lock::Lock, a_lock, lock) switch (msg.command) { @@ -986,27 +980,23 @@ void APIConnection::lock_command(const LockCommandRequest &msg) { bool APIConnection::send_valve_state(valve::Valve *valve) { return this->send_message_smart_(valve, ValveStateResponse::MESSAGE_TYPE, ValveStateResponse::ESTIMATED_SIZE); } -uint16_t APIConnection::try_send_valve_state(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, - bool is_single) { +uint16_t APIConnection::try_send_valve_state(EntityBase *entity, APIConnection *conn, uint32_t remaining_size) { auto *valve = static_cast(entity); ValveStateResponse resp; resp.position = valve->position; resp.current_operation = static_cast(valve->current_operation); - return fill_and_encode_entity_state(valve, resp, ValveStateResponse::MESSAGE_TYPE, conn, remaining_size, is_single); + return fill_and_encode_entity_state(valve, resp, conn, remaining_size); } -uint16_t APIConnection::try_send_valve_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, - bool is_single) { +uint16_t APIConnection::try_send_valve_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size) { auto *valve = static_cast(entity); ListEntitiesValveResponse msg; auto traits = valve->get_traits(); - msg.device_class = valve->get_device_class_ref(); msg.assumed_state = traits.get_is_assumed_state(); msg.supports_position = traits.get_supports_position(); msg.supports_stop = traits.get_supports_stop(); - return fill_and_encode_entity_info(valve, msg, ListEntitiesValveResponse::MESSAGE_TYPE, conn, remaining_size, - is_single); + return fill_and_encode_entity_info_with_device_class(valve, msg, msg.device_class, conn, remaining_size); } -void APIConnection::valve_command(const ValveCommandRequest &msg) { +void APIConnection::on_valve_command_request(const ValveCommandRequest &msg) { ENTITY_COMMAND_MAKE_CALL(valve::Valve, valve, valve) if (msg.has_position) call.set_position(msg.position); @@ -1021,8 +1011,7 @@ bool APIConnection::send_media_player_state(media_player::MediaPlayer *media_pla return this->send_message_smart_(media_player, MediaPlayerStateResponse::MESSAGE_TYPE, MediaPlayerStateResponse::ESTIMATED_SIZE); } -uint16_t APIConnection::try_send_media_player_state(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, - bool is_single) { +uint16_t APIConnection::try_send_media_player_state(EntityBase *entity, APIConnection *conn, uint32_t remaining_size) { auto *media_player = static_cast(entity); MediaPlayerStateResponse resp; media_player::MediaPlayerState report_state = media_player->state == media_player::MEDIA_PLAYER_STATE_ANNOUNCING @@ -1031,11 +1020,9 @@ uint16_t APIConnection::try_send_media_player_state(EntityBase *entity, APIConne resp.state = static_cast(report_state); resp.volume = media_player->volume; resp.muted = media_player->is_muted(); - return fill_and_encode_entity_state(media_player, resp, MediaPlayerStateResponse::MESSAGE_TYPE, conn, remaining_size, - is_single); + return fill_and_encode_entity_state(media_player, resp, conn, remaining_size); } -uint16_t APIConnection::try_send_media_player_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, - bool is_single) { +uint16_t APIConnection::try_send_media_player_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size) { auto *media_player = static_cast(entity); ListEntitiesMediaPlayerResponse msg; auto traits = media_player->get_traits(); @@ -1050,10 +1037,9 @@ uint16_t APIConnection::try_send_media_player_info(EntityBase *entity, APIConnec media_format.purpose = static_cast(supported_format.purpose); media_format.sample_bytes = supported_format.sample_bytes; } - return fill_and_encode_entity_info(media_player, msg, ListEntitiesMediaPlayerResponse::MESSAGE_TYPE, conn, - remaining_size, is_single); + return fill_and_encode_entity_info(media_player, msg, conn, remaining_size); } -void APIConnection::media_player_command(const MediaPlayerCommandRequest &msg) { +void APIConnection::on_media_player_command_request(const MediaPlayerCommandRequest &msg) { ENTITY_COMMAND_MAKE_CALL(media_player::MediaPlayer, media_player, media_player) if (msg.has_command) { call.set_command(static_cast(msg.command)); @@ -1092,7 +1078,7 @@ void APIConnection::try_send_camera_image_() { msg.device_id = camera::Camera::instance()->get_device_id(); #endif - if (!this->send_message_(msg, CameraImageResponse::MESSAGE_TYPE)) { + if (!this->send_message(msg)) { return; // Send failed, try again later } this->image_reader_->consume_data(to_send); @@ -1115,14 +1101,12 @@ void APIConnection::set_camera_state(std::shared_ptr image) this->try_send_camera_image_(); } } -uint16_t APIConnection::try_send_camera_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, - bool is_single) { +uint16_t APIConnection::try_send_camera_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size) { auto *camera = static_cast(entity); ListEntitiesCameraResponse msg; - return fill_and_encode_entity_info(camera, msg, ListEntitiesCameraResponse::MESSAGE_TYPE, conn, remaining_size, - is_single); + return fill_and_encode_entity_info(camera, msg, conn, remaining_size); } -void APIConnection::camera_image(const CameraImageRequest &msg) { +void APIConnection::on_camera_image_request(const CameraImageRequest &msg) { if (camera::Camera::instance() == nullptr) return; @@ -1143,7 +1127,30 @@ void APIConnection::on_get_time_response(const GetTimeResponse &value) { homeassistant::global_homeassistant_time->set_epoch_time(value.epoch_seconds); #ifdef USE_TIME_TIMEZONE if (!value.timezone.empty()) { - homeassistant::global_homeassistant_time->set_timezone(value.timezone.c_str(), value.timezone.size()); + // Check if the sender provided pre-parsed timezone data. + // If std_offset is non-zero or DST rules are present, the parsed data was populated. + // For UTC (all zeros), string parsing produces the same result, so the fallback is equivalent. + const auto &pt = value.parsed_timezone; + if (pt.std_offset_seconds != 0 || pt.dst_start.type != enums::DST_RULE_TYPE_NONE) { + time::ParsedTimezone tz{}; + tz.std_offset_seconds = pt.std_offset_seconds; + tz.dst_offset_seconds = pt.dst_offset_seconds; + tz.dst_start.time_seconds = pt.dst_start.time_seconds; + tz.dst_start.day = static_cast(pt.dst_start.day); + tz.dst_start.type = static_cast(pt.dst_start.type); + tz.dst_start.month = static_cast(pt.dst_start.month); + tz.dst_start.week = static_cast(pt.dst_start.week); + tz.dst_start.day_of_week = static_cast(pt.dst_start.day_of_week); + tz.dst_end.time_seconds = pt.dst_end.time_seconds; + tz.dst_end.day = static_cast(pt.dst_end.day); + tz.dst_end.type = static_cast(pt.dst_end.type); + tz.dst_end.month = static_cast(pt.dst_end.month); + tz.dst_end.week = static_cast(pt.dst_end.week); + tz.dst_end.day_of_week = static_cast(pt.dst_end.day_of_week); + time::set_global_tz(tz); + } else { + homeassistant::global_homeassistant_time->set_timezone(value.timezone.c_str(), value.timezone.size()); + } } #endif } @@ -1151,45 +1158,53 @@ void APIConnection::on_get_time_response(const GetTimeResponse &value) { #endif #ifdef USE_BLUETOOTH_PROXY -void APIConnection::subscribe_bluetooth_le_advertisements(const SubscribeBluetoothLEAdvertisementsRequest &msg) { +void APIConnection::on_subscribe_bluetooth_le_advertisements_request( + const SubscribeBluetoothLEAdvertisementsRequest &msg) { bluetooth_proxy::global_bluetooth_proxy->subscribe_api_connection(this, msg.flags); } -void APIConnection::unsubscribe_bluetooth_le_advertisements(const UnsubscribeBluetoothLEAdvertisementsRequest &msg) { +void APIConnection::on_unsubscribe_bluetooth_le_advertisements_request() { bluetooth_proxy::global_bluetooth_proxy->unsubscribe_api_connection(this); } -void APIConnection::bluetooth_device_request(const BluetoothDeviceRequest &msg) { +void APIConnection::on_bluetooth_device_request(const BluetoothDeviceRequest &msg) { bluetooth_proxy::global_bluetooth_proxy->bluetooth_device_request(msg); } -void APIConnection::bluetooth_gatt_read(const BluetoothGATTReadRequest &msg) { +void APIConnection::on_bluetooth_gatt_read_request(const BluetoothGATTReadRequest &msg) { bluetooth_proxy::global_bluetooth_proxy->bluetooth_gatt_read(msg); } -void APIConnection::bluetooth_gatt_write(const BluetoothGATTWriteRequest &msg) { +void APIConnection::on_bluetooth_gatt_write_request(const BluetoothGATTWriteRequest &msg) { bluetooth_proxy::global_bluetooth_proxy->bluetooth_gatt_write(msg); } -void APIConnection::bluetooth_gatt_read_descriptor(const BluetoothGATTReadDescriptorRequest &msg) { +void APIConnection::on_bluetooth_gatt_read_descriptor_request(const BluetoothGATTReadDescriptorRequest &msg) { bluetooth_proxy::global_bluetooth_proxy->bluetooth_gatt_read_descriptor(msg); } -void APIConnection::bluetooth_gatt_write_descriptor(const BluetoothGATTWriteDescriptorRequest &msg) { +void APIConnection::on_bluetooth_gatt_write_descriptor_request(const BluetoothGATTWriteDescriptorRequest &msg) { bluetooth_proxy::global_bluetooth_proxy->bluetooth_gatt_write_descriptor(msg); } -void APIConnection::bluetooth_gatt_get_services(const BluetoothGATTGetServicesRequest &msg) { +void APIConnection::on_bluetooth_gatt_get_services_request(const BluetoothGATTGetServicesRequest &msg) { bluetooth_proxy::global_bluetooth_proxy->bluetooth_gatt_send_services(msg); } -void APIConnection::bluetooth_gatt_notify(const BluetoothGATTNotifyRequest &msg) { +void APIConnection::on_bluetooth_gatt_notify_request(const BluetoothGATTNotifyRequest &msg) { bluetooth_proxy::global_bluetooth_proxy->bluetooth_gatt_notify(msg); } -bool APIConnection::send_subscribe_bluetooth_connections_free_response( - const SubscribeBluetoothConnectionsFreeRequest &msg) { +bool APIConnection::send_subscribe_bluetooth_connections_free_response_() { bluetooth_proxy::global_bluetooth_proxy->send_connections_free(this); return true; } +void APIConnection::on_subscribe_bluetooth_connections_free_request() { + if (!this->send_subscribe_bluetooth_connections_free_response_()) { + this->on_fatal_error(); + } +} -void APIConnection::bluetooth_scanner_set_mode(const BluetoothScannerSetModeRequest &msg) { +void APIConnection::on_bluetooth_scanner_set_mode_request(const BluetoothScannerSetModeRequest &msg) { bluetooth_proxy::global_bluetooth_proxy->bluetooth_scanner_set_mode( msg.mode == enums::BluetoothScannerMode::BLUETOOTH_SCANNER_MODE_ACTIVE); } +void APIConnection::on_bluetooth_set_connection_params_request(const BluetoothSetConnectionParamsRequest &msg) { + bluetooth_proxy::global_bluetooth_proxy->bluetooth_set_connection_params(msg); +} #endif #ifdef USE_VOICE_ASSISTANT @@ -1198,7 +1213,7 @@ bool APIConnection::check_voice_assistant_api_connection_() const { voice_assistant::global_voice_assistant->get_api_connection() == this; } -void APIConnection::subscribe_voice_assistant(const SubscribeVoiceAssistantRequest &msg) { +void APIConnection::on_subscribe_voice_assistant_request(const SubscribeVoiceAssistantRequest &msg) { if (voice_assistant::global_voice_assistant != nullptr) { voice_assistant::global_voice_assistant->client_subscription(this, msg.subscribe); } @@ -1244,10 +1259,10 @@ void APIConnection::on_voice_assistant_announce_request(const VoiceAssistantAnno } } -bool APIConnection::send_voice_assistant_get_configuration_response(const VoiceAssistantConfigurationRequest &msg) { +bool APIConnection::send_voice_assistant_get_configuration_response_(const VoiceAssistantConfigurationRequest &msg) { VoiceAssistantConfigurationResponse resp; if (!this->check_voice_assistant_api_connection_()) { - return this->send_message(resp, VoiceAssistantConfigurationResponse::MESSAGE_TYPE); + return this->send_message(resp); } auto &config = voice_assistant::global_voice_assistant->get_configuration(); @@ -1279,10 +1294,15 @@ bool APIConnection::send_voice_assistant_get_configuration_response(const VoiceA resp.active_wake_words = &config.active_wake_words; resp.max_active_wake_words = config.max_active_wake_words; - return this->send_message(resp, VoiceAssistantConfigurationResponse::MESSAGE_TYPE); + return this->send_message(resp); +} +void APIConnection::on_voice_assistant_configuration_request(const VoiceAssistantConfigurationRequest &msg) { + if (!this->send_voice_assistant_get_configuration_response_(msg)) { + this->on_fatal_error(); + } } -void APIConnection::voice_assistant_set_configuration(const VoiceAssistantSetConfiguration &msg) { +void APIConnection::on_voice_assistant_set_configuration(const VoiceAssistantSetConfiguration &msg) { if (this->check_voice_assistant_api_connection_()) { voice_assistant::global_voice_assistant->on_set_configuration(msg.active_wake_words); } @@ -1290,11 +1310,11 @@ void APIConnection::voice_assistant_set_configuration(const VoiceAssistantSetCon #endif #ifdef USE_ZWAVE_PROXY -void APIConnection::zwave_proxy_frame(const ZWaveProxyFrame &msg) { +void APIConnection::on_z_wave_proxy_frame(const ZWaveProxyFrame &msg) { zwave_proxy::global_zwave_proxy->send_frame(msg.data, msg.data_len); } -void APIConnection::zwave_proxy_request(const ZWaveProxyRequest &msg) { +void APIConnection::on_z_wave_proxy_request(const ZWaveProxyRequest &msg) { zwave_proxy::global_zwave_proxy->zwave_proxy_request(this, msg.type); } #endif @@ -1305,24 +1325,22 @@ bool APIConnection::send_alarm_control_panel_state(alarm_control_panel::AlarmCon AlarmControlPanelStateResponse::ESTIMATED_SIZE); } uint16_t APIConnection::try_send_alarm_control_panel_state(EntityBase *entity, APIConnection *conn, - uint32_t remaining_size, bool is_single) { + uint32_t remaining_size) { auto *a_alarm_control_panel = static_cast(entity); AlarmControlPanelStateResponse resp; resp.state = static_cast(a_alarm_control_panel->get_state()); - return fill_and_encode_entity_state(a_alarm_control_panel, resp, AlarmControlPanelStateResponse::MESSAGE_TYPE, conn, - remaining_size, is_single); + return fill_and_encode_entity_state(a_alarm_control_panel, resp, conn, remaining_size); } uint16_t APIConnection::try_send_alarm_control_panel_info(EntityBase *entity, APIConnection *conn, - uint32_t remaining_size, bool is_single) { + uint32_t remaining_size) { auto *a_alarm_control_panel = static_cast(entity); ListEntitiesAlarmControlPanelResponse msg; msg.supported_features = a_alarm_control_panel->get_supported_features(); msg.requires_code = a_alarm_control_panel->get_requires_code(); msg.requires_code_to_arm = a_alarm_control_panel->get_requires_code_to_arm(); - return fill_and_encode_entity_info(a_alarm_control_panel, msg, ListEntitiesAlarmControlPanelResponse::MESSAGE_TYPE, - conn, remaining_size, is_single); + return fill_and_encode_entity_info(a_alarm_control_panel, msg, conn, remaining_size); } -void APIConnection::alarm_control_panel_command(const AlarmControlPanelCommandRequest &msg) { +void APIConnection::on_alarm_control_panel_command_request(const AlarmControlPanelCommandRequest &msg) { ENTITY_COMMAND_MAKE_CALL(alarm_control_panel::AlarmControlPanel, a_alarm_control_panel, alarm_control_panel) switch (msg.command) { case enums::ALARM_CONTROL_PANEL_DISARM: @@ -1347,7 +1365,7 @@ void APIConnection::alarm_control_panel_command(const AlarmControlPanelCommandRe call.pending(); break; } - call.set_code(msg.code); + call.set_code(msg.code.c_str(), msg.code.size()); call.perform(); } #endif @@ -1357,8 +1375,7 @@ bool APIConnection::send_water_heater_state(water_heater::WaterHeater *water_hea return this->send_message_smart_(water_heater, WaterHeaterStateResponse::MESSAGE_TYPE, WaterHeaterStateResponse::ESTIMATED_SIZE); } -uint16_t APIConnection::try_send_water_heater_state(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, - bool is_single) { +uint16_t APIConnection::try_send_water_heater_state(EntityBase *entity, APIConnection *conn, uint32_t remaining_size) { auto *wh = static_cast(entity); WaterHeaterStateResponse resp; resp.mode = static_cast(wh->get_mode()); @@ -1367,12 +1384,10 @@ uint16_t APIConnection::try_send_water_heater_state(EntityBase *entity, APIConne resp.target_temperature_low = wh->get_target_temperature_low(); resp.target_temperature_high = wh->get_target_temperature_high(); resp.state = wh->get_state(); - resp.key = wh->get_object_id_hash(); - return encode_message_to_buffer(resp, WaterHeaterStateResponse::MESSAGE_TYPE, conn, remaining_size, is_single); + return fill_and_encode_entity_state(wh, resp, conn, remaining_size); } -uint16_t APIConnection::try_send_water_heater_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, - bool is_single) { +uint16_t APIConnection::try_send_water_heater_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size) { auto *wh = static_cast(entity); ListEntitiesWaterHeaterResponse msg; auto traits = wh->get_traits(); @@ -1381,8 +1396,7 @@ uint16_t APIConnection::try_send_water_heater_info(EntityBase *entity, APIConnec msg.target_temperature_step = traits.get_target_temperature_step(); msg.supported_modes = &traits.get_supported_modes(); msg.supported_features = traits.get_feature_flags(); - return fill_and_encode_entity_info(wh, msg, ListEntitiesWaterHeaterResponse::MESSAGE_TYPE, conn, remaining_size, - is_single); + return fill_and_encode_entity_info(wh, msg, conn, remaining_size); } void APIConnection::on_water_heater_command_request(const WaterHeaterCommandRequest &msg) { @@ -1395,8 +1409,12 @@ void APIConnection::on_water_heater_command_request(const WaterHeaterCommandRequ call.set_target_temperature_low(msg.target_temperature_low); if (msg.has_fields & enums::WATER_HEATER_COMMAND_HAS_TARGET_TEMPERATURE_HIGH) call.set_target_temperature_high(msg.target_temperature_high); - if (msg.has_fields & enums::WATER_HEATER_COMMAND_HAS_STATE) { + if ((msg.has_fields & enums::WATER_HEATER_COMMAND_HAS_AWAY_STATE) || + (msg.has_fields & enums::WATER_HEATER_COMMAND_HAS_STATE)) { call.set_away((msg.state & water_heater::WATER_HEATER_STATE_AWAY) != 0); + } + if ((msg.has_fields & enums::WATER_HEATER_COMMAND_HAS_ON_STATE) || + (msg.has_fields & enums::WATER_HEATER_COMMAND_HAS_STATE)) { call.set_on((msg.state & water_heater::WATER_HEATER_STATE_ON) != 0); } call.perform(); @@ -1411,25 +1429,22 @@ void APIConnection::send_event(event::Event *event) { event->get_last_event_type_index()); } uint16_t APIConnection::try_send_event_response(event::Event *event, StringRef event_type, APIConnection *conn, - uint32_t remaining_size, bool is_single) { + uint32_t remaining_size) { EventResponse resp; resp.event_type = event_type; - return fill_and_encode_entity_state(event, resp, EventResponse::MESSAGE_TYPE, conn, remaining_size, is_single); + return fill_and_encode_entity_state(event, resp, conn, remaining_size); } -uint16_t APIConnection::try_send_event_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, - bool is_single) { +uint16_t APIConnection::try_send_event_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size) { auto *event = static_cast(entity); ListEntitiesEventResponse msg; - msg.device_class = event->get_device_class_ref(); msg.event_types = &event->get_event_types(); - return fill_and_encode_entity_info(event, msg, ListEntitiesEventResponse::MESSAGE_TYPE, conn, remaining_size, - is_single); + return fill_and_encode_entity_info_with_device_class(event, msg, msg.device_class, conn, remaining_size); } #endif #ifdef USE_IR_RF -void APIConnection::infrared_rf_transmit_raw_timings(const InfraredRFTransmitRawTimingsRequest &msg) { +void APIConnection::on_infrared_rf_transmit_raw_timings_request(const InfraredRFTransmitRawTimingsRequest &msg) { // TODO: When RF is implemented, add a field to the message to distinguish IR vs RF // and dispatch to the appropriate entity type based on that field. #ifdef USE_INFRARED @@ -1441,19 +1456,99 @@ void APIConnection::infrared_rf_transmit_raw_timings(const InfraredRFTransmitRaw #endif } -void APIConnection::send_infrared_rf_receive_event(const InfraredRFReceiveEvent &msg) { - this->send_message(msg, InfraredRFReceiveEvent::MESSAGE_TYPE); +void APIConnection::send_infrared_rf_receive_event(const InfraredRFReceiveEvent &msg) { this->send_message(msg); } +#endif + +#ifdef USE_SERIAL_PROXY +void APIConnection::on_serial_proxy_configure_request(const SerialProxyConfigureRequest &msg) { + auto &proxies = App.get_serial_proxies(); + if (msg.instance >= proxies.size()) { + ESP_LOGW(TAG, "Serial proxy instance %" PRIu32 " out of range (max %" PRIu32 ")", msg.instance, + static_cast(proxies.size())); + return; + } + proxies[msg.instance]->configure(msg.baudrate, msg.flow_control, static_cast(msg.parity), msg.stop_bits, + msg.data_size); } + +void APIConnection::on_serial_proxy_write_request(const SerialProxyWriteRequest &msg) { + auto &proxies = App.get_serial_proxies(); + if (msg.instance >= proxies.size()) { + ESP_LOGW(TAG, "Serial proxy instance %" PRIu32 " out of range", msg.instance); + return; + } + proxies[msg.instance]->write_from_client(msg.data, msg.data_len); +} + +void APIConnection::on_serial_proxy_set_modem_pins_request(const SerialProxySetModemPinsRequest &msg) { + auto &proxies = App.get_serial_proxies(); + if (msg.instance >= proxies.size()) { + ESP_LOGW(TAG, "Serial proxy instance %" PRIu32 " out of range", msg.instance); + return; + } + proxies[msg.instance]->set_modem_pins(msg.line_states); +} + +void APIConnection::on_serial_proxy_get_modem_pins_request(const SerialProxyGetModemPinsRequest &msg) { + auto &proxies = App.get_serial_proxies(); + if (msg.instance >= proxies.size()) { + ESP_LOGW(TAG, "Serial proxy instance %" PRIu32 " out of range", msg.instance); + return; + } + SerialProxyGetModemPinsResponse resp{}; + resp.instance = msg.instance; + resp.line_states = proxies[msg.instance]->get_modem_pins(); + this->send_message(resp); +} + +void APIConnection::on_serial_proxy_request(const SerialProxyRequest &msg) { + auto &proxies = App.get_serial_proxies(); + if (msg.instance >= proxies.size()) { + ESP_LOGW(TAG, "Serial proxy instance %" PRIu32 " out of range", msg.instance); + return; + } + switch (msg.type) { + case enums::SERIAL_PROXY_REQUEST_TYPE_SUBSCRIBE: + case enums::SERIAL_PROXY_REQUEST_TYPE_UNSUBSCRIBE: + proxies[msg.instance]->serial_proxy_request(this, msg.type); + break; + case enums::SERIAL_PROXY_REQUEST_TYPE_FLUSH: { + SerialProxyRequestResponse resp{}; + resp.instance = msg.instance; + resp.type = enums::SERIAL_PROXY_REQUEST_TYPE_FLUSH; + switch (proxies[msg.instance]->flush_port()) { + case uart::UARTFlushResult::UART_FLUSH_RESULT_SUCCESS: + resp.status = enums::SERIAL_PROXY_STATUS_OK; + break; + case uart::UARTFlushResult::UART_FLUSH_RESULT_ASSUMED_SUCCESS: + resp.status = enums::SERIAL_PROXY_STATUS_ASSUMED_SUCCESS; + break; + case uart::UARTFlushResult::UART_FLUSH_RESULT_TIMEOUT: + resp.status = enums::SERIAL_PROXY_STATUS_TIMEOUT; + break; + case uart::UARTFlushResult::UART_FLUSH_RESULT_FAILED: + resp.status = enums::SERIAL_PROXY_STATUS_ERROR; + break; + } + this->send_message(resp); + break; + } + default: + ESP_LOGW(TAG, "Unknown serial proxy request type: %" PRIu32, static_cast(msg.type)); + break; + } +} + +void APIConnection::send_serial_proxy_data(const SerialProxyDataReceived &msg) { this->send_message(msg); } #endif #ifdef USE_INFRARED -uint16_t APIConnection::try_send_infrared_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, - bool is_single) { +uint16_t APIConnection::try_send_infrared_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size) { auto *infrared = static_cast(entity); ListEntitiesInfraredResponse msg; msg.capabilities = infrared->get_capability_flags(); - return fill_and_encode_entity_info(infrared, msg, ListEntitiesInfraredResponse::MESSAGE_TYPE, conn, remaining_size, - is_single); + msg.receiver_frequency = infrared->get_traits().get_receiver_frequency_hz(); + return fill_and_encode_entity_info(infrared, msg, conn, remaining_size); } #endif @@ -1461,8 +1556,7 @@ uint16_t APIConnection::try_send_infrared_info(EntityBase *entity, APIConnection bool APIConnection::send_update_state(update::UpdateEntity *update) { return this->send_message_smart_(update, UpdateStateResponse::MESSAGE_TYPE, UpdateStateResponse::ESTIMATED_SIZE); } -uint16_t APIConnection::try_send_update_state(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, - bool is_single) { +uint16_t APIConnection::try_send_update_state(EntityBase *entity, APIConnection *conn, uint32_t remaining_size) { auto *update = static_cast(entity); UpdateStateResponse resp; resp.missing_state = !update->has_state(); @@ -1478,17 +1572,14 @@ uint16_t APIConnection::try_send_update_state(EntityBase *entity, APIConnection resp.release_summary = StringRef(update->update_info.summary); resp.release_url = StringRef(update->update_info.release_url); } - return fill_and_encode_entity_state(update, resp, UpdateStateResponse::MESSAGE_TYPE, conn, remaining_size, is_single); + return fill_and_encode_entity_state(update, resp, conn, remaining_size); } -uint16_t APIConnection::try_send_update_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, - bool is_single) { +uint16_t APIConnection::try_send_update_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size) { auto *update = static_cast(entity); ListEntitiesUpdateResponse msg; - msg.device_class = update->get_device_class_ref(); - return fill_and_encode_entity_info(update, msg, ListEntitiesUpdateResponse::MESSAGE_TYPE, conn, remaining_size, - is_single); + return fill_and_encode_entity_info_with_device_class(update, msg, msg.device_class, conn, remaining_size); } -void APIConnection::update_command(const UpdateCommandRequest &msg) { +void APIConnection::on_update_command_request(const UpdateCommandRequest &msg) { ENTITY_COMMAND_GET(update::UpdateEntity, update, update) switch (msg.command) { @@ -1512,7 +1603,7 @@ bool APIConnection::try_send_log_message(int level, const char *tag, const char SubscribeLogsResponse msg; msg.level = static_cast(level); msg.set_message(reinterpret_cast(line), message_len); - return this->send_message_(msg, SubscribeLogsResponse::MESSAGE_TYPE); + return this->send_message(msg); } void APIConnection::complete_authentication_() { @@ -1522,10 +1613,15 @@ void APIConnection::complete_authentication_() { } this->flags_.connection_state = static_cast(ConnectionState::AUTHENTICATED); + // Reset traffic timer so keepalive starts from authentication, not connection start + this->last_traffic_ = App.get_loop_component_start_time(); this->log_client_(ESPHOME_LOG_LEVEL_DEBUG, LOG_STR("connected")); #ifdef USE_API_CLIENT_CONNECTED_TRIGGER - this->parent_->get_client_connected_trigger()->trigger(std::string(this->helper_->get_client_name()), - std::string(this->helper_->get_client_peername())); + { + char peername[socket::SOCKADDR_STR_LEN]; + this->parent_->get_client_connected_trigger()->trigger(std::string(this->helper_->get_client_name()), + std::string(this->helper_->get_peername_to(peername))); + } #endif #ifdef USE_HOMEASSISTANT_TIME if (homeassistant::global_homeassistant_time != nullptr) { @@ -1539,13 +1635,20 @@ void APIConnection::complete_authentication_() { #endif } -bool APIConnection::send_hello_response(const HelloRequest &msg) { +bool APIConnection::send_hello_response_(const HelloRequest &msg) { // Copy client name with truncation if needed (set_client_name handles truncation) this->helper_->set_client_name(msg.client_info.c_str(), msg.client_info.size()); this->client_api_version_major_ = msg.api_version_major; this->client_api_version_minor_ = msg.api_version_minor; - ESP_LOGV(TAG, "Hello from client: '%s' | %s | API Version %" PRIu32 ".%" PRIu32, this->helper_->get_client_name(), - this->helper_->get_client_peername(), this->client_api_version_major_, this->client_api_version_minor_); + char peername[socket::SOCKADDR_STR_LEN]; + ESP_LOGV(TAG, "Hello from client: '%s' | %s | API Version %" PRIu16 ".%" PRIu16, this->helper_->get_client_name(), + this->helper_->get_peername_to(peername), this->client_api_version_major_, this->client_api_version_minor_); + + // TODO: Remove before 2026.8.0 (one version after get_object_id backward compat removal) + if (!this->client_supports_api_version(1, 14)) { + ESP_LOGW(TAG, "'%s' using outdated API %" PRIu16 ".%" PRIu16 ", update to 1.14+", this->helper_->get_client_name(), + this->client_api_version_major_, this->client_api_version_minor_); + } HelloResponse resp; resp.api_version_major = 1; @@ -1557,16 +1660,16 @@ bool APIConnection::send_hello_response(const HelloRequest &msg) { // Auto-authenticate - password auth was removed in ESPHome 2026.1.0 this->complete_authentication_(); - return this->send_message(resp, HelloResponse::MESSAGE_TYPE); + return this->send_message(resp); } -bool APIConnection::send_ping_response(const PingRequest &msg) { +bool APIConnection::send_ping_response_() { PingResponse resp; - return this->send_message(resp, PingResponse::MESSAGE_TYPE); + return this->send_message(resp); } -bool APIConnection::send_device_info_response(const DeviceInfoRequest &msg) { - DeviceInfoResponse resp{}; +bool APIConnection::send_device_info_response_() { + DeviceInfoResponse resp; resp.name = StringRef(App.get_name()); resp.friendly_name = StringRef(App.get_friendly_name()); #ifdef USE_AREAS @@ -1661,6 +1764,16 @@ bool APIConnection::send_device_info_response(const DeviceInfoRequest &msg) { resp.zwave_proxy_feature_flags = zwave_proxy::global_zwave_proxy->get_feature_flags(); resp.zwave_home_id = zwave_proxy::global_zwave_proxy->get_home_id(); #endif +#ifdef USE_SERIAL_PROXY + size_t serial_proxy_index = 0; + for (auto const &proxy : App.get_serial_proxies()) { + if (serial_proxy_index >= SERIAL_PROXY_COUNT) + break; + auto &info = resp.serial_proxies[serial_proxy_index++]; + info.name = StringRef(proxy->get_name()); + info.port_type = proxy->get_port_type(); + } +#endif #ifdef USE_API_NOISE resp.api_encryption_supported = true; #endif @@ -1686,7 +1799,27 @@ bool APIConnection::send_device_info_response(const DeviceInfoRequest &msg) { } #endif - return this->send_message(resp, DeviceInfoResponse::MESSAGE_TYPE); + return this->send_message(resp); +} +void APIConnection::on_hello_request(const HelloRequest &msg) { + if (!this->send_hello_response_(msg)) { + this->on_fatal_error(); + } +} +void APIConnection::on_disconnect_request() { + if (!this->send_disconnect_response_()) { + this->on_fatal_error(); + } +} +void APIConnection::on_ping_request() { + if (!this->send_ping_response_()) { + this->on_fatal_error(); + } +} +void APIConnection::on_device_info_request() { + if (!this->send_device_info_response_()) { + this->on_fatal_error(); + } } #ifdef USE_API_HOMEASSISTANT_STATES @@ -1696,37 +1829,42 @@ void APIConnection::on_home_assistant_state_response(const HomeAssistantStateRes return; } + // Null-terminate state in-place for safe c_str() usage (e.g., parse_number in callbacks). + // Safe: decode is complete, byte after string data was already consumed during parse, + // and frame helpers reserve RX_BUF_NULL_TERMINATOR extra byte in rx_buf_. + // const_cast is safe: msg references mutable rx_buf_ data; the const& handler + // signature is a generated protobuf pattern, not a true immutability contract. + if (!msg.state.empty()) { + const_cast(msg.state.c_str())[msg.state.size()] = '\0'; + } + for (auto &it : this->parent_->get_state_subs()) { - // Compare entity_id: check length matches and content matches - size_t entity_id_len = strlen(it.entity_id); - if (entity_id_len != msg.entity_id.size() || - memcmp(it.entity_id, msg.entity_id.c_str(), msg.entity_id.size()) != 0) { + if (msg.entity_id != it.entity_id) { continue; } - // Compare attribute: either both have matching attribute, or both have none - size_t sub_attr_len = it.attribute != nullptr ? strlen(it.attribute) : 0; - if (sub_attr_len != msg.attribute.size() || - (sub_attr_len > 0 && memcmp(it.attribute, msg.attribute.c_str(), sub_attr_len) != 0)) { + // If subscriber has attribute filter (non-null), message attribute must match it; + // if subscriber has no filter (nullptr), message must have no attribute. + if (it.attribute != nullptr ? msg.attribute != it.attribute : !msg.attribute.empty()) { continue; } - // Create null-terminated state for callback (parse_number needs null-termination) - // HA state max length is 255 characters, but attributes can be much longer - // Use stack buffer for common case (states), heap fallback for large attributes - size_t state_len = msg.state.size(); - SmallBufferWithHeapFallback state_buf_alloc(state_len + 1); - char *state_buf = reinterpret_cast(state_buf_alloc.get()); - if (state_len > 0) { - memcpy(state_buf, msg.state.c_str(), state_len); - } - state_buf[state_len] = '\0'; - it.callback(StringRef(state_buf, state_len)); + it.callback(msg.state); } } #endif #ifdef USE_API_USER_DEFINED_ACTIONS -void APIConnection::execute_service(const ExecuteServiceRequest &msg) { +void APIConnection::on_execute_service_request(const ExecuteServiceRequest &msg) { + // Null-terminate string args in-place for safe c_str() usage in YAML service triggers. + // Safe: full ExecuteServiceRequest decode is complete, all bytes in rx_buf_ consumed, + // and frame helpers reserve RX_BUF_NULL_TERMINATOR extra byte for the last field. + // const_cast is safe: msg references mutable rx_buf_ data; the const& handler + // signature is a generated protobuf pattern, not a true immutability contract. + for (auto &arg : const_cast(msg).args) { + if (!arg.string_.empty()) { + const_cast(arg.string_.c_str())[arg.string_.size()] = '\0'; + } + } bool found = false; #ifdef USE_API_USER_DEFINED_ACTION_RESPONSES // Register the call and get a unique server-generated action_call_id @@ -1761,7 +1899,7 @@ void APIConnection::send_execute_service_response(uint32_t call_id, bool success resp.call_id = call_id; resp.success = success; resp.error_message = error_message; - this->send_message(resp, ExecuteServiceResponse::MESSAGE_TYPE); + this->send_message(resp); } #ifdef USE_API_USER_DEFINED_ACTION_RESPONSES_JSON void APIConnection::send_execute_service_response(uint32_t call_id, bool success, StringRef error_message, @@ -1772,7 +1910,7 @@ void APIConnection::send_execute_service_response(uint32_t call_id, bool success resp.error_message = error_message; resp.response_data = response_data; resp.response_data_len = response_data_len; - this->send_message(resp, ExecuteServiceResponse::MESSAGE_TYPE); + this->send_message(resp); } #endif // USE_API_USER_DEFINED_ACTION_RESPONSES_JSON #endif // USE_API_USER_DEFINED_ACTION_RESPONSES @@ -1792,7 +1930,7 @@ void APIConnection::on_homeassistant_action_response(const HomeassistantActionRe }; #endif #ifdef USE_API_NOISE -bool APIConnection::send_noise_encryption_set_key_response(const NoiseEncryptionSetKeyRequest &msg) { +bool APIConnection::send_noise_encryption_set_key_response_(const NoiseEncryptionSetKeyRequest &msg) { NoiseEncryptionSetKeyResponse resp; resp.success = false; @@ -1811,19 +1949,18 @@ bool APIConnection::send_noise_encryption_set_key_response(const NoiseEncryption resp.success = true; } - return this->send_message(resp, NoiseEncryptionSetKeyResponse::MESSAGE_TYPE); + return this->send_message(resp); +} +void APIConnection::on_noise_encryption_set_key_request(const NoiseEncryptionSetKeyRequest &msg) { + if (!this->send_noise_encryption_set_key_response_(msg)) { + this->on_fatal_error(); + } } #endif #ifdef USE_API_HOMEASSISTANT_STATES -void APIConnection::subscribe_home_assistant_states(const SubscribeHomeAssistantStatesRequest &msg) { - state_subs_at_ = 0; -} +void APIConnection::on_subscribe_home_assistant_states_request() { state_subs_at_ = 0; } #endif -bool APIConnection::try_to_clear_buffer(bool log_out_of_space) { - if (this->flags_.remove) - return false; - if (this->helper_->can_write_without_blocking()) - return true; +bool APIConnection::try_to_clear_buffer_slow_(bool log_out_of_space) { delay(0); APIError err = this->helper_->loop(); if (err != APIError::OK) { @@ -1837,6 +1974,71 @@ bool APIConnection::try_to_clear_buffer(bool log_out_of_space) { } return false; } +bool APIConnection::send_message_(uint32_t payload_size, uint8_t message_type, MessageEncodeFn encode_fn, + const void *msg) { +#ifdef HAS_PROTO_MESSAGE_DUMP + // Skip dump for log messages (recursive logging risk) and camera frames (high-frequency noise) + if (message_type != SubscribeLogsResponse::MESSAGE_TYPE +#ifdef USE_CAMERA + && message_type != CameraImageResponse::MESSAGE_TYPE +#endif + ) { + auto *proto_msg = static_cast(msg); + DumpBuffer dump_buf; + this->log_send_message_(proto_msg->message_name(), proto_msg->dump_to(dump_buf)); + } +#endif + auto &shared_buf = this->parent_->get_shared_buffer_ref(); + this->prepare_first_message_buffer(shared_buf, payload_size); + size_t write_start = shared_buf.size(); + shared_buf.resize(write_start + payload_size); + ProtoWriteBuffer buffer{&shared_buf, write_start}; + encode_fn(msg, buffer); + return this->send_buffer(ProtoWriteBuffer{&shared_buf}, message_type); +} +// Encodes a message to the buffer and returns the total number of bytes used, +// including header and footer overhead. Returns 0 if the message doesn't fit. +uint16_t APIConnection::encode_to_buffer(uint32_t calculated_size, MessageEncodeFn encode_fn, const void *msg, + APIConnection *conn, uint32_t remaining_size) { +#ifdef HAS_PROTO_MESSAGE_DUMP + if (conn->flags_.log_only_mode) { + auto *proto_msg = static_cast(msg); + DumpBuffer dump_buf; + conn->log_send_message_(proto_msg->message_name(), proto_msg->dump_to(dump_buf)); + return 1; + } +#endif + // Cache frame sizes to avoid repeated virtual calls + const uint8_t header_padding = conn->helper_->frame_header_padding(); + const uint8_t footer_size = conn->helper_->frame_footer_size(); + + // Calculate total size with padding for buffer allocation + size_t total_calculated_size = calculated_size + header_padding + footer_size; + + // Check if it fits + if (total_calculated_size > remaining_size) + return 0; // Doesn't fit + + auto &shared_buf = conn->parent_->get_shared_buffer_ref(); + + size_t to_add; + if (conn->flags_.batch_first_message) { + // First message - buffer already prepared by caller, just clear flag + conn->flags_.batch_first_message = false; + to_add = calculated_size; + } else { + // Batch message second or later + // Reserve for full message, resize to include footer gap + header padding + payload + to_add = total_calculated_size; + } + + shared_buf.resize(shared_buf.size() + to_add); + ProtoWriteBuffer buffer{&shared_buf, shared_buf.size() - calculated_size}; + encode_fn(msg, buffer); + + // Return total size (header + payload + footer) + return static_cast(total_calculated_size); +} bool APIConnection::send_buffer(ProtoWriteBuffer buffer, uint8_t message_type) { const bool is_log_message = (message_type == SubscribeLogsResponse::MESSAGE_TYPE); @@ -1862,39 +2064,31 @@ void APIConnection::on_no_setup_connection() { this->log_client_(ESPHOME_LOG_LEVEL_DEBUG, LOG_STR("no connection setup")); } void APIConnection::on_fatal_error() { - this->helper_->close(); + // Don't close socket here - keep it open so getpeername() works for logging + // Socket will be closed when client is removed from the list in APIServer::loop() this->flags_.remove = true; } -void APIConnection::DeferredBatch::add_item(EntityBase *entity, uint8_t message_type, uint8_t estimated_size, - uint8_t aux_data_index) { - // Check if we already have a message of this type for this entity - // This provides deduplication per entity/message_type combination - // O(n) but optimized for RAM and not performance. - // Skip deduplication for events - they are edge-triggered, every occurrence matters -#ifdef USE_EVENT - if (message_type != EventResponse::MESSAGE_TYPE) -#endif - { - for (const auto &item : items) { - if (item.entity == entity && item.message_type == message_type) - return; // Already queued - } - } - // No existing item found (or event), add new one - items.push_back({entity, message_type, estimated_size, aux_data_index}); +bool APIConnection::schedule_message_front_(EntityBase *entity, uint8_t message_type, uint8_t estimated_size) { + this->deferred_batch_.add_item_front(entity, message_type, estimated_size); + return this->schedule_batch_(); } -void APIConnection::DeferredBatch::add_item_front(EntityBase *entity, uint8_t message_type, uint8_t estimated_size) { - // Add high priority message and swap to front - // This avoids expensive vector::insert which shifts all elements - // Note: We only ever have one high-priority message at a time (ping OR disconnect) - // If we're disconnecting, pings are blocked, so this simple swap is sufficient - items.push_back({entity, message_type, estimated_size, AUX_DATA_UNUSED}); - if (items.size() > 1) { - // Swap the new high-priority item to the front - std::swap(items.front(), items.back()); +bool APIConnection::send_message_smart_(EntityBase *entity, uint8_t message_type, uint8_t estimated_size, + uint8_t aux_data_index) { + if (this->should_send_immediately_(message_type) && this->helper_->can_write_without_blocking()) { + auto &shared_buf = this->parent_->get_shared_buffer_ref(); + this->prepare_first_message_buffer(shared_buf, estimated_size); + DeferredBatch::BatchItem item{entity, message_type, estimated_size, aux_data_index}; + if (this->dispatch_message_(item, MAX_BATCH_PACKET_SIZE, true) && + this->send_buffer(ProtoWriteBuffer{&shared_buf}, message_type)) { +#ifdef HAS_PROTO_MESSAGE_DUMP + this->log_batch_item_(item); +#endif + return true; + } } + return this->schedule_message_(entity, message_type, estimated_size, aux_data_index); } bool APIConnection::schedule_batch_() { @@ -1906,10 +2100,6 @@ bool APIConnection::schedule_batch_() { } void APIConnection::process_batch_() { - // Ensure MessageInfo remains trivially destructible for our placement new approach - static_assert(std::is_trivially_destructible::value, - "MessageInfo must remain trivially destructible with this placement-new approach"); - if (this->deferred_batch_.empty()) { this->flags_.batch_scheduled = false; return; @@ -1925,10 +2115,25 @@ void APIConnection::process_batch_() { auto &shared_buf = this->parent_->get_shared_buffer_ref(); size_t num_items = this->deferred_batch_.size(); - // Fast path for single message - allocate exact size needed + // Cache these values to avoid repeated virtual calls + const uint8_t header_padding = this->helper_->frame_header_padding(); + const uint8_t footer_size = this->helper_->frame_footer_size(); + + // Pre-calculate exact buffer size needed based on message types + uint32_t total_estimated_size = num_items * (header_padding + footer_size); + for (size_t i = 0; i < num_items; i++) { + total_estimated_size += this->deferred_batch_[i].estimated_size; + } + // Clamp to MAX_BATCH_PACKET_SIZE — we won't send more than that per batch + if (total_estimated_size > MAX_BATCH_PACKET_SIZE) { + total_estimated_size = MAX_BATCH_PACKET_SIZE; + } + + this->prepare_first_message_buffer(shared_buf, header_padding, total_estimated_size); + + // Fast path for single message - buffer already allocated above if (num_items == 1) { const auto &item = this->deferred_batch_[0]; - // Let dispatch_message_ calculate size and encode if it fits uint16_t payload_size = this->dispatch_message_(item, std::numeric_limits::max(), true); @@ -1946,35 +2151,26 @@ void APIConnection::process_batch_() { return; } - size_t messages_to_process = std::min(num_items, MAX_MESSAGES_PER_BATCH); + // Multi-message path — heavy stack frame isolated in separate noinline function + this->process_batch_multi_(shared_buf, num_items, header_padding, footer_size); +} + +// Separated from process_batch_() so the single-message fast path gets a minimal +// stack frame without the MAX_MESSAGES_PER_BATCH * sizeof(MessageInfo) array. +void APIConnection::process_batch_multi_(APIBuffer &shared_buf, size_t num_items, uint8_t header_padding, + uint8_t footer_size) { + // Ensure MessageInfo remains trivially destructible for our placement new approach + static_assert(std::is_trivially_destructible::value, + "MessageInfo must remain trivially destructible with this placement-new approach"); + + const size_t messages_to_process = std::min(num_items, MAX_MESSAGES_PER_BATCH); + const uint8_t frame_overhead = header_padding + footer_size; // Stack-allocated array for message info alignas(MessageInfo) char message_info_storage[MAX_MESSAGES_PER_BATCH * sizeof(MessageInfo)]; MessageInfo *message_info = reinterpret_cast(message_info_storage); - size_t message_count = 0; - - // Cache these values to avoid repeated virtual calls - const uint8_t header_padding = this->helper_->frame_header_padding(); - const uint8_t footer_size = this->helper_->frame_footer_size(); - - // Initialize buffer and tracking variables - shared_buf.clear(); - - // Pre-calculate exact buffer size needed based on message types - uint32_t total_estimated_size = num_items * (header_padding + footer_size); - for (size_t i = 0; i < this->deferred_batch_.size(); i++) { - const auto &item = this->deferred_batch_[i]; - total_estimated_size += item.estimated_size; - } - - // Calculate total overhead for all messages - // Reserve based on estimated size (much more accurate than 24-byte worst-case) - shared_buf.reserve(total_estimated_size); - this->flags_.batch_first_message = true; - size_t items_processed = 0; uint16_t remaining_size = std::numeric_limits::max(); - // Track where each message's header padding begins in the buffer // For plaintext: this is where the 6-byte header padding starts // For noise: this is where the 7-byte header padding starts @@ -1986,7 +2182,7 @@ void APIConnection::process_batch_() { const auto &item = this->deferred_batch_[i]; // Try to encode message via dispatch // The dispatch function calculates overhead to determine if the message fits - uint16_t payload_size = this->dispatch_message_(item, remaining_size, false); + uint16_t payload_size = this->dispatch_message_(item, remaining_size, i == 0); if (payload_size == 0) { // Message won't fit, stop processing @@ -1995,15 +2191,12 @@ void APIConnection::process_batch_() { // Message was encoded successfully // payload_size is header_padding + actual payload size + footer_size - uint16_t proto_payload_size = payload_size - header_padding - footer_size; + uint16_t proto_payload_size = payload_size - frame_overhead; // Use placement new to construct MessageInfo in pre-allocated stack array // This avoids default-constructing all MAX_MESSAGES_PER_BATCH elements // Explicit destruction is not needed because MessageInfo is trivially destructible, // as ensured by the static_assert in its definition. - new (&message_info[message_count++]) MessageInfo(item.message_type, current_offset, proto_payload_size); - - // Update tracking variables - items_processed++; + new (&message_info[items_processed++]) MessageInfo(item.message_type, current_offset, proto_payload_size); // After first message, set remaining size to MAX_BATCH_PACKET_SIZE to avoid fragmentation if (items_processed == 1) { remaining_size = MAX_BATCH_PACKET_SIZE; @@ -2014,48 +2207,45 @@ void APIConnection::process_batch_() { current_offset = shared_buf.size() + footer_size; } - if (items_processed == 0) { - this->deferred_batch_.clear(); - return; - } + if (items_processed > 0) { + // Add footer space for the last message (for Noise protocol MAC) + if (footer_size > 0) { + shared_buf.resize(shared_buf.size() + footer_size); + } - // Add footer space for the last message (for Noise protocol MAC) - if (footer_size > 0) { - shared_buf.resize(shared_buf.size() + footer_size); - } - - // Send all collected messages - APIError err = this->helper_->write_protobuf_messages(ProtoWriteBuffer{&shared_buf}, - std::span(message_info, message_count)); - if (err != APIError::OK && err != APIError::WOULD_BLOCK) { - this->fatal_error_with_log_(LOG_STR("Batch write failed"), err); - } + // Send all collected messages + APIError err = this->helper_->write_protobuf_messages(ProtoWriteBuffer{&shared_buf}, + std::span(message_info, items_processed)); + if (err != APIError::OK && err != APIError::WOULD_BLOCK) { + this->fatal_error_with_log_(LOG_STR("Batch write failed"), err); + } #ifdef HAS_PROTO_MESSAGE_DUMP - // Log messages after send attempt for VV debugging - // It's safe to use the buffer for logging at this point regardless of send result - for (size_t i = 0; i < items_processed; i++) { - const auto &item = this->deferred_batch_[i]; - this->log_batch_item_(item); - } + // Log messages after send attempt for VV debugging + // It's safe to use the buffer for logging at this point regardless of send result + for (size_t i = 0; i < items_processed; i++) { + const auto &item = this->deferred_batch_[i]; + this->log_batch_item_(item); + } #endif - // Handle remaining items more efficiently - if (items_processed < this->deferred_batch_.size()) { - // Remove processed items from the beginning - this->deferred_batch_.remove_front(items_processed); - // Reschedule for remaining items - this->schedule_batch_(); - } else { - // All items processed - this->clear_batch_(); + // Partial batch — remove processed items and reschedule + if (items_processed < this->deferred_batch_.size()) { + this->deferred_batch_.remove_front(items_processed); + this->schedule_batch_(); + return; + } } + + // All items processed (or none could be processed) + this->clear_batch_(); } // Dispatch message encoding based on message_type // Switch assigns function pointer, single call site for smaller code size uint16_t APIConnection::dispatch_message_(const DeferredBatch::BatchItem &item, uint32_t remaining_size, - bool is_single) { + bool batch_first) { + this->flags_.batch_first_message = batch_first; #ifdef USE_EVENT // Events need aux_data_index to look up event type from entity if (item.message_type == EventResponse::MESSAGE_TYPE) { @@ -2064,7 +2254,7 @@ uint16_t APIConnection::dispatch_message_(const DeferredBatch::BatchItem &item, return 0; auto *event = static_cast(item.entity); return try_send_event_response(event, StringRef::from_maybe_nullptr(event->get_event_type(item.aux_data_index)), - this, remaining_size, is_single); + this, remaining_size); } #endif @@ -2174,25 +2364,22 @@ uint16_t APIConnection::dispatch_message_(const DeferredBatch::BatchItem &item, #undef CASE_STATE_INFO #undef CASE_INFO_ONLY - return func(item.entity, this, remaining_size, is_single); + return func(item.entity, this, remaining_size); } -uint16_t APIConnection::try_send_list_info_done(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, - bool is_single) { +uint16_t APIConnection::try_send_list_info_done(EntityBase *entity, APIConnection *conn, uint32_t remaining_size) { ListEntitiesDoneResponse resp; - return encode_message_to_buffer(resp, ListEntitiesDoneResponse::MESSAGE_TYPE, conn, remaining_size, is_single); + return encode_message_to_buffer(resp, conn, remaining_size); } -uint16_t APIConnection::try_send_disconnect_request(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, - bool is_single) { +uint16_t APIConnection::try_send_disconnect_request(EntityBase *entity, APIConnection *conn, uint32_t remaining_size) { DisconnectRequest req; - return encode_message_to_buffer(req, DisconnectRequest::MESSAGE_TYPE, conn, remaining_size, is_single); + return encode_message_to_buffer(req, conn, remaining_size); } -uint16_t APIConnection::try_send_ping_request(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, - bool is_single) { +uint16_t APIConnection::try_send_ping_request(EntityBase *entity, APIConnection *conn, uint32_t remaining_size) { PingRequest req; - return encode_message_to_buffer(req, PingRequest::MESSAGE_TYPE, conn, remaining_size, is_single); + return encode_message_to_buffer(req, conn, remaining_size); } #ifdef USE_API_HOMEASSISTANT_STATES @@ -2211,19 +2398,21 @@ void APIConnection::process_state_subscriptions_() { resp.attribute = it.attribute != nullptr ? StringRef(it.attribute) : StringRef(""); resp.once = it.once; - if (this->send_message(resp, SubscribeHomeAssistantStateResponse::MESSAGE_TYPE)) { + if (this->send_message(resp)) { this->state_subs_at_++; } } #endif // USE_API_HOMEASSISTANT_STATES void APIConnection::log_client_(int level, const LogString *message) { + char peername[socket::SOCKADDR_STR_LEN]; esp_log_printf_(level, TAG, __LINE__, ESPHOME_LOG_FORMAT("%s (%s): %s"), this->helper_->get_client_name(), - this->helper_->get_client_peername(), LOG_STR_ARG(message)); + this->helper_->get_peername_to(peername), LOG_STR_ARG(message)); } void APIConnection::log_warning_(const LogString *message, APIError err) { - ESP_LOGW(TAG, "%s (%s): %s %s errno=%d", this->helper_->get_client_name(), this->helper_->get_client_peername(), + char peername[socket::SOCKADDR_STR_LEN]; + ESP_LOGW(TAG, "%s (%s): %s %s errno=%d", this->helper_->get_client_name(), this->helper_->get_peername_to(peername), LOG_STR_ARG(message), LOG_STR_ARG(api_error_to_logstr(err)), errno); } diff --git a/esphome/components/api/api_connection.h b/esphome/components/api/api_connection.h index 21bf4c4073..13d5273ecb 100644 --- a/esphome/components/api/api_connection.h +++ b/esphome/components/api/api_connection.h @@ -3,11 +3,23 @@ #include "esphome/core/defines.h" #ifdef USE_API #include "api_frame_helper.h" +#ifdef USE_API_NOISE +#include "api_frame_helper_noise.h" +#endif +#ifdef USE_API_PLAINTEXT +#include "api_frame_helper_plaintext.h" +#endif #include "api_pb2.h" #include "api_pb2_service.h" #include "api_server.h" #include "esphome/core/application.h" #include "esphome/core/component.h" +#ifdef USE_ESP32_CRASH_HANDLER +#include "esphome/components/esp32/crash_handler.h" +#endif +#ifdef USE_RP2040_CRASH_HANDLER +#include "esphome/components/rp2040/crash_handler.h" +#endif #include "esphome/core/entity_base.h" #include "esphome/core/string_ref.h" @@ -15,6 +27,10 @@ #include #include +namespace esphome { +class ComponentIterator; +} // namespace esphome + namespace esphome::api { // Keepalive timeout in milliseconds @@ -28,16 +44,46 @@ static constexpr size_t MAX_INITIAL_PER_BATCH = 34; // For clients >= AP static_assert(MAX_MESSAGES_PER_BATCH >= MAX_INITIAL_PER_BATCH, "MAX_MESSAGES_PER_BATCH must be >= MAX_INITIAL_PER_BATCH"); -class APIConnection final : public APIServerConnection { +#ifdef USE_BENCHMARK +class APIConnection; +void bench_enable_immediate_send(APIConnection *conn); +void bench_clear_batch(APIConnection *conn); +void bench_process_batch(APIConnection *conn); +#endif + +class APIConnection final : public APIServerConnectionBase { public: friend class APIServer; friend class ListEntitiesIterator; +#ifdef USE_BENCHMARK + friend void bench_enable_immediate_send(APIConnection *conn); + friend void bench_clear_batch(APIConnection *conn); + friend void bench_process_batch(APIConnection *conn); +#endif APIConnection(std::unique_ptr socket, APIServer *parent); - virtual ~APIConnection(); + ~APIConnection(); void start(); void loop(); + protected: + // read_message_ is defined here (instead of in APIServerConnectionBase) so the + // compiler can devirtualize and inline on_* handler calls within this final class. + void read_message_(uint32_t msg_size, uint32_t msg_type, const uint8_t *msg_data); + + // Auth helpers defined here (not in ProtoService) so the compiler can + // devirtualize is_connection_setup()/on_no_setup_connection() calls + // within this final class. + inline bool check_connection_setup_() { + if (!this->is_connection_setup()) { + this->on_no_setup_connection(); + return false; + } + return true; + } + inline bool check_authenticated_() { return this->check_connection_setup_(); } + + public: bool send_list_info_done() { return this->schedule_message_(nullptr, ListEntitiesDoneResponse::MESSAGE_TYPE, ListEntitiesDoneResponse::ESTIMATED_SIZE); @@ -47,163 +93,173 @@ class APIConnection final : public APIServerConnection { #endif #ifdef USE_COVER bool send_cover_state(cover::Cover *cover); - void cover_command(const CoverCommandRequest &msg) override; + void on_cover_command_request(const CoverCommandRequest &msg); #endif #ifdef USE_FAN bool send_fan_state(fan::Fan *fan); - void fan_command(const FanCommandRequest &msg) override; + void on_fan_command_request(const FanCommandRequest &msg); #endif #ifdef USE_LIGHT bool send_light_state(light::LightState *light); - void light_command(const LightCommandRequest &msg) override; + void on_light_command_request(const LightCommandRequest &msg); #endif #ifdef USE_SENSOR bool send_sensor_state(sensor::Sensor *sensor); #endif #ifdef USE_SWITCH bool send_switch_state(switch_::Switch *a_switch); - void switch_command(const SwitchCommandRequest &msg) override; + void on_switch_command_request(const SwitchCommandRequest &msg); #endif #ifdef USE_TEXT_SENSOR bool send_text_sensor_state(text_sensor::TextSensor *text_sensor); #endif #ifdef USE_CAMERA void set_camera_state(std::shared_ptr image); - void camera_image(const CameraImageRequest &msg) override; + void on_camera_image_request(const CameraImageRequest &msg); #endif #ifdef USE_CLIMATE bool send_climate_state(climate::Climate *climate); - void climate_command(const ClimateCommandRequest &msg) override; + void on_climate_command_request(const ClimateCommandRequest &msg); #endif #ifdef USE_NUMBER bool send_number_state(number::Number *number); - void number_command(const NumberCommandRequest &msg) override; + void on_number_command_request(const NumberCommandRequest &msg); #endif #ifdef USE_DATETIME_DATE bool send_date_state(datetime::DateEntity *date); - void date_command(const DateCommandRequest &msg) override; + void on_date_command_request(const DateCommandRequest &msg); #endif #ifdef USE_DATETIME_TIME bool send_time_state(datetime::TimeEntity *time); - void time_command(const TimeCommandRequest &msg) override; + void on_time_command_request(const TimeCommandRequest &msg); #endif #ifdef USE_DATETIME_DATETIME bool send_datetime_state(datetime::DateTimeEntity *datetime); - void datetime_command(const DateTimeCommandRequest &msg) override; + void on_date_time_command_request(const DateTimeCommandRequest &msg); #endif #ifdef USE_TEXT bool send_text_state(text::Text *text); - void text_command(const TextCommandRequest &msg) override; + void on_text_command_request(const TextCommandRequest &msg); #endif #ifdef USE_SELECT bool send_select_state(select::Select *select); - void select_command(const SelectCommandRequest &msg) override; + void on_select_command_request(const SelectCommandRequest &msg); #endif #ifdef USE_BUTTON - void button_command(const ButtonCommandRequest &msg) override; + void on_button_command_request(const ButtonCommandRequest &msg); #endif #ifdef USE_LOCK bool send_lock_state(lock::Lock *a_lock); - void lock_command(const LockCommandRequest &msg) override; + void on_lock_command_request(const LockCommandRequest &msg); #endif #ifdef USE_VALVE bool send_valve_state(valve::Valve *valve); - void valve_command(const ValveCommandRequest &msg) override; + void on_valve_command_request(const ValveCommandRequest &msg); #endif #ifdef USE_MEDIA_PLAYER bool send_media_player_state(media_player::MediaPlayer *media_player); - void media_player_command(const MediaPlayerCommandRequest &msg) override; + void on_media_player_command_request(const MediaPlayerCommandRequest &msg); #endif bool try_send_log_message(int level, const char *tag, const char *line, size_t message_len); #ifdef USE_API_HOMEASSISTANT_SERVICES void send_homeassistant_action(const HomeassistantActionRequest &call) { if (!this->flags_.service_call_subscription) return; - this->send_message(call, HomeassistantActionRequest::MESSAGE_TYPE); + this->send_message(call); } #ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES - void on_homeassistant_action_response(const HomeassistantActionResponse &msg) override; + void on_homeassistant_action_response(const HomeassistantActionResponse &msg); #endif // USE_API_HOMEASSISTANT_ACTION_RESPONSES #endif // USE_API_HOMEASSISTANT_SERVICES #ifdef USE_BLUETOOTH_PROXY - void subscribe_bluetooth_le_advertisements(const SubscribeBluetoothLEAdvertisementsRequest &msg) override; - void unsubscribe_bluetooth_le_advertisements(const UnsubscribeBluetoothLEAdvertisementsRequest &msg) override; + void on_subscribe_bluetooth_le_advertisements_request(const SubscribeBluetoothLEAdvertisementsRequest &msg); + void on_unsubscribe_bluetooth_le_advertisements_request(); - void bluetooth_device_request(const BluetoothDeviceRequest &msg) override; - void bluetooth_gatt_read(const BluetoothGATTReadRequest &msg) override; - void bluetooth_gatt_write(const BluetoothGATTWriteRequest &msg) override; - void bluetooth_gatt_read_descriptor(const BluetoothGATTReadDescriptorRequest &msg) override; - void bluetooth_gatt_write_descriptor(const BluetoothGATTWriteDescriptorRequest &msg) override; - void bluetooth_gatt_get_services(const BluetoothGATTGetServicesRequest &msg) override; - void bluetooth_gatt_notify(const BluetoothGATTNotifyRequest &msg) override; - bool send_subscribe_bluetooth_connections_free_response(const SubscribeBluetoothConnectionsFreeRequest &msg) override; - void bluetooth_scanner_set_mode(const BluetoothScannerSetModeRequest &msg) override; + void on_bluetooth_device_request(const BluetoothDeviceRequest &msg); + void on_bluetooth_gatt_read_request(const BluetoothGATTReadRequest &msg); + void on_bluetooth_gatt_write_request(const BluetoothGATTWriteRequest &msg); + void on_bluetooth_gatt_read_descriptor_request(const BluetoothGATTReadDescriptorRequest &msg); + void on_bluetooth_gatt_write_descriptor_request(const BluetoothGATTWriteDescriptorRequest &msg); + void on_bluetooth_gatt_get_services_request(const BluetoothGATTGetServicesRequest &msg); + void on_bluetooth_gatt_notify_request(const BluetoothGATTNotifyRequest &msg); + void on_subscribe_bluetooth_connections_free_request(); + void on_bluetooth_scanner_set_mode_request(const BluetoothScannerSetModeRequest &msg); + void on_bluetooth_set_connection_params_request(const BluetoothSetConnectionParamsRequest &msg); #endif #ifdef USE_HOMEASSISTANT_TIME void send_time_request() { GetTimeRequest req; - this->send_message(req, GetTimeRequest::MESSAGE_TYPE); + this->send_message(req); } #endif #ifdef USE_VOICE_ASSISTANT - void subscribe_voice_assistant(const SubscribeVoiceAssistantRequest &msg) override; - void on_voice_assistant_response(const VoiceAssistantResponse &msg) override; - void on_voice_assistant_event_response(const VoiceAssistantEventResponse &msg) override; - void on_voice_assistant_audio(const VoiceAssistantAudio &msg) override; - void on_voice_assistant_timer_event_response(const VoiceAssistantTimerEventResponse &msg) override; - void on_voice_assistant_announce_request(const VoiceAssistantAnnounceRequest &msg) override; - bool send_voice_assistant_get_configuration_response(const VoiceAssistantConfigurationRequest &msg) override; - void voice_assistant_set_configuration(const VoiceAssistantSetConfiguration &msg) override; + void on_subscribe_voice_assistant_request(const SubscribeVoiceAssistantRequest &msg); + void on_voice_assistant_response(const VoiceAssistantResponse &msg); + void on_voice_assistant_event_response(const VoiceAssistantEventResponse &msg); + void on_voice_assistant_audio(const VoiceAssistantAudio &msg); + void on_voice_assistant_timer_event_response(const VoiceAssistantTimerEventResponse &msg); + void on_voice_assistant_announce_request(const VoiceAssistantAnnounceRequest &msg); + void on_voice_assistant_configuration_request(const VoiceAssistantConfigurationRequest &msg); + void on_voice_assistant_set_configuration(const VoiceAssistantSetConfiguration &msg); #endif #ifdef USE_ZWAVE_PROXY - void zwave_proxy_frame(const ZWaveProxyFrame &msg) override; - void zwave_proxy_request(const ZWaveProxyRequest &msg) override; + void on_z_wave_proxy_frame(const ZWaveProxyFrame &msg); + void on_z_wave_proxy_request(const ZWaveProxyRequest &msg); #endif #ifdef USE_ALARM_CONTROL_PANEL bool send_alarm_control_panel_state(alarm_control_panel::AlarmControlPanel *a_alarm_control_panel); - void alarm_control_panel_command(const AlarmControlPanelCommandRequest &msg) override; + void on_alarm_control_panel_command_request(const AlarmControlPanelCommandRequest &msg); #endif #ifdef USE_WATER_HEATER bool send_water_heater_state(water_heater::WaterHeater *water_heater); - void on_water_heater_command_request(const WaterHeaterCommandRequest &msg) override; + void on_water_heater_command_request(const WaterHeaterCommandRequest &msg); #endif #ifdef USE_IR_RF - void infrared_rf_transmit_raw_timings(const InfraredRFTransmitRawTimingsRequest &msg) override; + void on_infrared_rf_transmit_raw_timings_request(const InfraredRFTransmitRawTimingsRequest &msg); void send_infrared_rf_receive_event(const InfraredRFReceiveEvent &msg); #endif +#ifdef USE_SERIAL_PROXY + void on_serial_proxy_configure_request(const SerialProxyConfigureRequest &msg); + void on_serial_proxy_write_request(const SerialProxyWriteRequest &msg); + void on_serial_proxy_set_modem_pins_request(const SerialProxySetModemPinsRequest &msg); + void on_serial_proxy_get_modem_pins_request(const SerialProxyGetModemPinsRequest &msg); + void on_serial_proxy_request(const SerialProxyRequest &msg); + void send_serial_proxy_data(const SerialProxyDataReceived &msg); +#endif + #ifdef USE_EVENT void send_event(event::Event *event); #endif #ifdef USE_UPDATE bool send_update_state(update::UpdateEntity *update); - void update_command(const UpdateCommandRequest &msg) override; + void on_update_command_request(const UpdateCommandRequest &msg); #endif - void on_disconnect_response(const DisconnectResponse &value) override; - void on_ping_response(const PingResponse &value) override { + void on_disconnect_response(); + void on_ping_response() { // we initiated ping this->flags_.sent_ping = false; } #ifdef USE_API_HOMEASSISTANT_STATES - void on_home_assistant_state_response(const HomeAssistantStateResponse &msg) override; + void on_home_assistant_state_response(const HomeAssistantStateResponse &msg); #endif #ifdef USE_HOMEASSISTANT_TIME - void on_get_time_response(const GetTimeResponse &value) override; + void on_get_time_response(const GetTimeResponse &value); #endif - bool send_hello_response(const HelloRequest &msg) override; - bool send_disconnect_response(const DisconnectRequest &msg) override; - bool send_ping_response(const PingRequest &msg) override; - bool send_device_info_response(const DeviceInfoRequest &msg) override; - void list_entities(const ListEntitiesRequest &msg) override { this->begin_iterator_(ActiveIterator::LIST_ENTITIES); } - void subscribe_states(const SubscribeStatesRequest &msg) override { + void on_hello_request(const HelloRequest &msg); + void on_disconnect_request(); + void on_ping_request(); + void on_device_info_request(); + void on_list_entities_request() { this->begin_iterator_(ActiveIterator::LIST_ENTITIES); } + void on_subscribe_states_request() { this->flags_.state_subscription = true; // Start initial state iterator only if no iterator is active // If list_entities is running, we'll start initial_state when it completes @@ -211,21 +267,25 @@ class APIConnection final : public APIServerConnection { this->begin_iterator_(ActiveIterator::INITIAL_STATE); } } - void subscribe_logs(const SubscribeLogsRequest &msg) override { + void on_subscribe_logs_request(const SubscribeLogsRequest &msg) { this->flags_.log_subscription = msg.level; if (msg.dump_config) App.schedule_dump_config(); +#ifdef USE_ESP32_CRASH_HANDLER + esp32::crash_handler_log(); +#endif +#ifdef USE_RP2040_CRASH_HANDLER + rp2040::crash_handler_log(); +#endif } #ifdef USE_API_HOMEASSISTANT_SERVICES - void subscribe_homeassistant_services(const SubscribeHomeassistantServicesRequest &msg) override { - this->flags_.service_call_subscription = true; - } + void on_subscribe_homeassistant_services_request() { this->flags_.service_call_subscription = true; } #endif #ifdef USE_API_HOMEASSISTANT_STATES - void subscribe_home_assistant_states(const SubscribeHomeAssistantStatesRequest &msg) override; + void on_subscribe_home_assistant_states_request(); #endif #ifdef USE_API_USER_DEFINED_ACTIONS - void execute_service(const ExecuteServiceRequest &msg) override; + void on_execute_service_request(const ExecuteServiceRequest &msg); #ifdef USE_API_USER_DEFINED_ACTION_RESPONSES void send_execute_service_response(uint32_t call_id, bool success, StringRef error_message); #ifdef USE_API_USER_DEFINED_ACTION_RESPONSES_JSON @@ -235,16 +295,17 @@ class APIConnection final : public APIServerConnection { #endif // USE_API_USER_DEFINED_ACTION_RESPONSES #endif #ifdef USE_API_NOISE - bool send_noise_encryption_set_key_response(const NoiseEncryptionSetKeyRequest &msg) override; + void on_noise_encryption_set_key_request(const NoiseEncryptionSetKeyRequest &msg); #endif - bool is_authenticated() override { + bool is_authenticated() { return static_cast(this->flags_.connection_state) == ConnectionState::AUTHENTICATED; } - bool is_connection_setup() override { + bool is_connection_setup() { return static_cast(this->flags_.connection_state) == ConnectionState::CONNECTED || this->is_authenticated(); } + bool is_marked_for_removal() const { return this->flags_.remove; } uint8_t get_log_subscription_level() const { return this->flags_.log_subscription; } // Get client API version for feature detection @@ -253,41 +314,75 @@ class APIConnection final : public APIServerConnection { (this->client_api_version_major_ == major && this->client_api_version_minor_ >= minor); } - void on_fatal_error() override; - void on_no_setup_connection() override; - ProtoWriteBuffer create_buffer(uint32_t reserve_size) override { - // FIXME: ensure no recursive writes can happen + void on_fatal_error(); + void on_no_setup_connection(); - // Get header padding size - used for both reserve and insert - uint8_t header_padding = this->helper_->frame_header_padding(); - // Get shared buffer from parent server - std::vector &shared_buf = this->parent_->get_shared_buffer_ref(); - this->prepare_first_message_buffer(shared_buf, header_padding, - reserve_size + header_padding + this->helper_->frame_footer_size()); - return {&shared_buf}; + // Function pointer type for type-erased message encoding + using MessageEncodeFn = void (*)(const void *, ProtoWriteBuffer &); + // Function pointer type for type-erased size calculation + using CalculateSizeFn = uint32_t (*)(const void *); + + template bool send_message(const T &msg) { + if constexpr (T::ESTIMATED_SIZE == 0) { + return this->send_message_(0, T::MESSAGE_TYPE, &encode_msg_noop, &msg); + } else { + return this->send_message_(msg.calculate_size(), T::MESSAGE_TYPE, &proto_encode_msg, &msg); + } } - void prepare_first_message_buffer(std::vector &shared_buf, size_t header_padding, size_t total_size) { + void prepare_first_message_buffer(APIBuffer &shared_buf, size_t header_padding, size_t total_size) { shared_buf.clear(); // Reserve space for header padding + message + footer // - Header padding: space for protocol headers (7 bytes for Noise, 6 for Plaintext) // - Footer: space for MAC (16 bytes for Noise, 0 for Plaintext) - shared_buf.reserve(total_size); - // Resize to add header padding so message encoding starts at the correct position - shared_buf.resize(header_padding); + // Reserve full size but only set initial size to header padding + // so message encoding starts at the correct position + shared_buf.reserve_and_resize(total_size, header_padding); } - bool try_to_clear_buffer(bool log_out_of_space); - bool send_buffer(ProtoWriteBuffer buffer, uint8_t message_type) override; + // Convenience overload - computes frame overhead internally + void prepare_first_message_buffer(APIBuffer &shared_buf, size_t payload_size) { + const uint8_t header_padding = this->helper_->frame_header_padding(); + const uint8_t footer_size = this->helper_->frame_footer_size(); + this->prepare_first_message_buffer(shared_buf, header_padding, payload_size + header_padding + footer_size); + } + + bool try_to_clear_buffer(bool log_out_of_space) { + if (this->flags_.remove) + return false; + if (this->helper_->can_write_without_blocking()) + return true; + return this->try_to_clear_buffer_slow_(log_out_of_space); + } + bool send_buffer(ProtoWriteBuffer buffer, uint8_t message_type); const char *get_name() const { return this->helper_->get_client_name(); } - /// Get peer name (IP address) - cached at connection init time - const char *get_peername() const { return this->helper_->get_client_peername(); } + /// Get peer name (IP address) into caller-provided buffer, returns buf for convenience + const char *get_peername_to(std::span buf) const { + return this->helper_->get_peername_to(buf); + } protected: + bool try_to_clear_buffer_slow_(bool log_out_of_space); + // Helper function to handle authentication completion void complete_authentication_(); + // Pattern B helpers: send response and return success/failure + bool send_hello_response_(const HelloRequest &msg); + bool send_disconnect_response_(); + bool send_ping_response_(); + bool send_device_info_response_(); +#ifdef USE_API_NOISE + bool send_noise_encryption_set_key_response_(const NoiseEncryptionSetKeyRequest &msg); +#endif +#ifdef USE_BLUETOOTH_PROXY + bool send_subscribe_bluetooth_connections_free_response_(); +#endif +#ifdef USE_VOICE_ASSISTANT + bool send_voice_assistant_get_configuration_response_(const VoiceAssistantConfigurationRequest &msg); +#endif + #ifdef USE_CAMERA void try_send_camera_image_(); #endif @@ -296,50 +391,67 @@ class APIConnection final : public APIServerConnection { void process_state_subscriptions_(); #endif - // Non-template helper to encode any ProtoMessage - static uint16_t encode_message_to_buffer(ProtoMessage &msg, uint8_t message_type, APIConnection *conn, - uint32_t remaining_size, bool is_single); - - // Helper to fill entity state base and encode message - static uint16_t fill_and_encode_entity_state(EntityBase *entity, StateResponseProtoMessage &msg, uint8_t message_type, - APIConnection *conn, uint32_t remaining_size, bool is_single) { - msg.key = entity->get_object_id_hash(); -#ifdef USE_DEVICES - msg.device_id = entity->get_device_id(); -#endif - return encode_message_to_buffer(msg, message_type, conn, remaining_size, is_single); + // Size thunk — converts void* back to concrete type for direct calculate_size() call + template static uint32_t calc_size(const void *msg) { + return static_cast(msg)->calculate_size(); } - // Helper to fill entity info base and encode message - static uint16_t fill_and_encode_entity_info(EntityBase *entity, InfoResponseProtoMessage &msg, uint8_t message_type, - APIConnection *conn, uint32_t remaining_size, bool is_single) { - // Set common fields that are shared by all entity types - msg.key = entity->get_object_id_hash(); + // Shared no-op encode thunk for empty messages (ESTIMATED_SIZE == 0) + static void encode_msg_noop(const void *, ProtoWriteBuffer &) {} - // API 1.14+ clients compute object_id client-side from the entity name - // For older clients, we must send object_id for backward compatibility - // See: https://github.com/esphome/backlog/issues/76 - // TODO: Remove this backward compat code before 2026.7.0 - all clients should support API 1.14 by then - // Buffer must remain in scope until encode_message_to_buffer is called - char object_id_buf[OBJECT_ID_MAX_LEN]; - if (!conn->client_supports_api_version(1, 14)) { - msg.object_id = entity->get_object_id_to(object_id_buf); + // Non-template buffer management for send_message + bool send_message_(uint32_t payload_size, uint8_t message_type, MessageEncodeFn encode_fn, const void *msg); + + // Non-template buffer management for batch encoding + static uint16_t encode_to_buffer(uint32_t calculated_size, MessageEncodeFn encode_fn, const void *msg, + APIConnection *conn, uint32_t remaining_size); + + // Thin template wrapper — computes size, delegates buffer work to non-template helper + template static uint16_t encode_message_to_buffer(T &msg, APIConnection *conn, uint32_t remaining_size) { + if constexpr (T::ESTIMATED_SIZE == 0) { + return encode_to_buffer(0, &encode_msg_noop, &msg, conn, remaining_size); + } else { + return encode_to_buffer(msg.calculate_size(), &proto_encode_msg, &msg, conn, remaining_size); } + } - if (entity->has_own_name()) { - msg.name = entity->get_name(); - } + // Non-template core — fills state fields and encodes + static uint16_t fill_and_encode_entity_state(EntityBase *entity, StateResponseProtoMessage &msg, + CalculateSizeFn size_fn, MessageEncodeFn encode_fn, APIConnection *conn, + uint32_t remaining_size); - // Set common EntityBase properties -#ifdef USE_ENTITY_ICON - msg.icon = entity->get_icon_ref(); -#endif - msg.disabled_by_default = entity->is_disabled_by_default(); - msg.entity_category = static_cast(entity->get_entity_category()); -#ifdef USE_DEVICES - msg.device_id = entity->get_device_id(); -#endif - return encode_message_to_buffer(msg, message_type, conn, remaining_size, is_single); + // Thin template wrapper + template + static uint16_t fill_and_encode_entity_state(EntityBase *entity, T &msg, APIConnection *conn, + uint32_t remaining_size) { + return fill_and_encode_entity_state(entity, msg, &calc_size, &proto_encode_msg, conn, remaining_size); + } + + // Non-template core — fills info fields, allocates buffers, and encodes + static uint16_t fill_and_encode_entity_info(EntityBase *entity, InfoResponseProtoMessage &msg, + CalculateSizeFn size_fn, MessageEncodeFn encode_fn, APIConnection *conn, + uint32_t remaining_size); + + // Thin template wrapper + template + static uint16_t fill_and_encode_entity_info(EntityBase *entity, T &msg, APIConnection *conn, + uint32_t remaining_size) { + return fill_and_encode_entity_info(entity, msg, &calc_size, &proto_encode_msg, conn, remaining_size); + } + + // Non-template core — fills device_class, then delegates to fill_and_encode_entity_info + static uint16_t fill_and_encode_entity_info_with_device_class(EntityBase *entity, InfoResponseProtoMessage &msg, + StringRef &device_class_field, CalculateSizeFn size_fn, + MessageEncodeFn encode_fn, APIConnection *conn, + uint32_t remaining_size); + + // Thin template wrapper + template + static uint16_t fill_and_encode_entity_info_with_device_class(EntityBase *entity, T &msg, + StringRef &device_class_field, APIConnection *conn, + uint32_t remaining_size) { + return fill_and_encode_entity_info_with_device_class(entity, msg, device_class_field, &calc_size, + &proto_encode_msg, conn, remaining_size); } #ifdef USE_VOICE_ASSISTANT @@ -354,162 +466,132 @@ class APIConnection final : public APIServerConnection { return this->client_supports_api_version(1, 14) ? MAX_INITIAL_PER_BATCH : MAX_INITIAL_PER_BATCH_LEGACY; } - // Helper method to process multiple entities from an iterator in a batch - template void process_iterator_batch_(Iterator &iterator) { - size_t initial_size = this->deferred_batch_.size(); - size_t max_batch = this->get_max_batch_size_(); - while (!iterator.completed() && (this->deferred_batch_.size() - initial_size) < max_batch) { - iterator.advance(); - } + // Send keepalive ping or disconnect unresponsive client. + // Cold path — extracted from loop() to reduce instruction cache pressure. + void __attribute__((noinline)) check_keepalive_(uint32_t now); - // If the batch is full, process it immediately - // Note: iterator.advance() already calls schedule_batch_() via schedule_message_() - if (this->deferred_batch_.size() >= max_batch) { - this->process_batch_(); - } - } + // Process active iterator (list_entities/initial_state) during connection setup. + // Extracted from loop() — only runs during initial handshake, NONE in steady state. + void __attribute__((noinline)) process_active_iterator_(); + + // Helper method to process multiple entities from an iterator in a batch. + // Takes ComponentIterator base class reference to avoid duplicate template instantiations. + void process_iterator_batch_(ComponentIterator &iterator); #ifdef USE_BINARY_SENSOR - static uint16_t try_send_binary_sensor_state(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, - bool is_single); - static uint16_t try_send_binary_sensor_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, - bool is_single); + static uint16_t try_send_binary_sensor_state(EntityBase *entity, APIConnection *conn, uint32_t remaining_size); + static uint16_t try_send_binary_sensor_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size); #endif #ifdef USE_COVER - static uint16_t try_send_cover_state(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, - bool is_single); - static uint16_t try_send_cover_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, bool is_single); + static uint16_t try_send_cover_state(EntityBase *entity, APIConnection *conn, uint32_t remaining_size); + static uint16_t try_send_cover_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size); #endif #ifdef USE_FAN - static uint16_t try_send_fan_state(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, bool is_single); - static uint16_t try_send_fan_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, bool is_single); + static uint16_t try_send_fan_state(EntityBase *entity, APIConnection *conn, uint32_t remaining_size); + static uint16_t try_send_fan_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size); #endif #ifdef USE_LIGHT - static uint16_t try_send_light_state(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, - bool is_single); - static uint16_t try_send_light_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, bool is_single); + static uint16_t try_send_light_state(EntityBase *entity, APIConnection *conn, uint32_t remaining_size); + static uint16_t try_send_light_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size); #endif #ifdef USE_SENSOR - static uint16_t try_send_sensor_state(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, - bool is_single); - static uint16_t try_send_sensor_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, - bool is_single); + static uint16_t try_send_sensor_state(EntityBase *entity, APIConnection *conn, uint32_t remaining_size); + static uint16_t try_send_sensor_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size); #endif #ifdef USE_SWITCH - static uint16_t try_send_switch_state(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, - bool is_single); - static uint16_t try_send_switch_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, - bool is_single); + static uint16_t try_send_switch_state(EntityBase *entity, APIConnection *conn, uint32_t remaining_size); + static uint16_t try_send_switch_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size); #endif #ifdef USE_TEXT_SENSOR - static uint16_t try_send_text_sensor_state(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, - bool is_single); - static uint16_t try_send_text_sensor_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, - bool is_single); + static uint16_t try_send_text_sensor_state(EntityBase *entity, APIConnection *conn, uint32_t remaining_size); + static uint16_t try_send_text_sensor_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size); #endif #ifdef USE_CLIMATE - static uint16_t try_send_climate_state(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, - bool is_single); - static uint16_t try_send_climate_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, - bool is_single); + static uint16_t try_send_climate_state(EntityBase *entity, APIConnection *conn, uint32_t remaining_size); + static uint16_t try_send_climate_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size); #endif #ifdef USE_NUMBER - static uint16_t try_send_number_state(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, - bool is_single); - static uint16_t try_send_number_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, - bool is_single); + static uint16_t try_send_number_state(EntityBase *entity, APIConnection *conn, uint32_t remaining_size); + static uint16_t try_send_number_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size); #endif #ifdef USE_DATETIME_DATE - static uint16_t try_send_date_state(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, bool is_single); - static uint16_t try_send_date_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, bool is_single); + static uint16_t try_send_date_state(EntityBase *entity, APIConnection *conn, uint32_t remaining_size); + static uint16_t try_send_date_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size); #endif #ifdef USE_DATETIME_TIME - static uint16_t try_send_time_state(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, bool is_single); - static uint16_t try_send_time_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, bool is_single); + static uint16_t try_send_time_state(EntityBase *entity, APIConnection *conn, uint32_t remaining_size); + static uint16_t try_send_time_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size); #endif #ifdef USE_DATETIME_DATETIME - static uint16_t try_send_datetime_state(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, - bool is_single); - static uint16_t try_send_datetime_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, - bool is_single); + static uint16_t try_send_datetime_state(EntityBase *entity, APIConnection *conn, uint32_t remaining_size); + static uint16_t try_send_datetime_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size); #endif #ifdef USE_TEXT - static uint16_t try_send_text_state(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, bool is_single); - static uint16_t try_send_text_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, bool is_single); + static uint16_t try_send_text_state(EntityBase *entity, APIConnection *conn, uint32_t remaining_size); + static uint16_t try_send_text_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size); #endif #ifdef USE_SELECT - static uint16_t try_send_select_state(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, - bool is_single); - static uint16_t try_send_select_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, - bool is_single); + static uint16_t try_send_select_state(EntityBase *entity, APIConnection *conn, uint32_t remaining_size); + static uint16_t try_send_select_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size); #endif #ifdef USE_BUTTON - static uint16_t try_send_button_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, - bool is_single); + static uint16_t try_send_button_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size); #endif #ifdef USE_LOCK - static uint16_t try_send_lock_state(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, bool is_single); - static uint16_t try_send_lock_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, bool is_single); + static uint16_t try_send_lock_state(EntityBase *entity, APIConnection *conn, uint32_t remaining_size); + static uint16_t try_send_lock_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size); #endif #ifdef USE_VALVE - static uint16_t try_send_valve_state(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, - bool is_single); - static uint16_t try_send_valve_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, bool is_single); + static uint16_t try_send_valve_state(EntityBase *entity, APIConnection *conn, uint32_t remaining_size); + static uint16_t try_send_valve_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size); #endif #ifdef USE_MEDIA_PLAYER - static uint16_t try_send_media_player_state(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, - bool is_single); - static uint16_t try_send_media_player_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, - bool is_single); + static uint16_t try_send_media_player_state(EntityBase *entity, APIConnection *conn, uint32_t remaining_size); + static uint16_t try_send_media_player_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size); #endif #ifdef USE_ALARM_CONTROL_PANEL - static uint16_t try_send_alarm_control_panel_state(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, - bool is_single); - static uint16_t try_send_alarm_control_panel_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, - bool is_single); + static uint16_t try_send_alarm_control_panel_state(EntityBase *entity, APIConnection *conn, uint32_t remaining_size); + static uint16_t try_send_alarm_control_panel_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size); #endif #ifdef USE_WATER_HEATER - static uint16_t try_send_water_heater_state(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, - bool is_single); - static uint16_t try_send_water_heater_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, - bool is_single); + static uint16_t try_send_water_heater_state(EntityBase *entity, APIConnection *conn, uint32_t remaining_size); + static uint16_t try_send_water_heater_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size); #endif #ifdef USE_INFRARED - static uint16_t try_send_infrared_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, - bool is_single); + static uint16_t try_send_infrared_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size); #endif #ifdef USE_EVENT static uint16_t try_send_event_response(event::Event *event, StringRef event_type, APIConnection *conn, - uint32_t remaining_size, bool is_single); - static uint16_t try_send_event_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, bool is_single); + uint32_t remaining_size); + static uint16_t try_send_event_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size); #endif #ifdef USE_UPDATE - static uint16_t try_send_update_state(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, - bool is_single); - static uint16_t try_send_update_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, - bool is_single); + static uint16_t try_send_update_state(EntityBase *entity, APIConnection *conn, uint32_t remaining_size); + static uint16_t try_send_update_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size); #endif #ifdef USE_CAMERA - static uint16_t try_send_camera_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, - bool is_single); + static uint16_t try_send_camera_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size); #endif // Method for ListEntitiesDone batching - static uint16_t try_send_list_info_done(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, - bool is_single); + static uint16_t try_send_list_info_done(EntityBase *entity, APIConnection *conn, uint32_t remaining_size); // Method for DisconnectRequest batching - static uint16_t try_send_disconnect_request(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, - bool is_single); + static uint16_t try_send_disconnect_request(EntityBase *entity, APIConnection *conn, uint32_t remaining_size); // Batch message method for ping requests - static uint16_t try_send_ping_request(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, - bool is_single); + static uint16_t try_send_ping_request(EntityBase *entity, APIConnection *conn, uint32_t remaining_size); // === Optimal member ordering for 32-bit systems === // Group 1: Pointers (4 bytes each on 32-bit) +#if defined(USE_API_NOISE) && defined(USE_API_PLAINTEXT) std::unique_ptr helper_; +#elif defined(USE_API_NOISE) + std::unique_ptr helper_; +#elif defined(USE_API_PLAINTEXT) + std::unique_ptr helper_; +#endif APIServer *parent_; // Group 2: Iterator union (saves ~16 bytes vs separate iterators) @@ -539,7 +621,7 @@ class APIConnection final : public APIServerConnection { #endif // Function pointer type for message encoding - using MessageCreatorPtr = uint16_t (*)(EntityBase *, APIConnection *, uint32_t remaining_size, bool is_single); + using MessageCreatorPtr = uint16_t (*)(EntityBase *, APIConnection *, uint32_t remaining_size); // Generic batching mechanism for both state updates and entity info struct DeferredBatch { @@ -562,9 +644,28 @@ class APIConnection final : public APIServerConnection { // Add item to the batch (with deduplication) void add_item(EntityBase *entity, uint8_t message_type, uint8_t estimated_size, - uint8_t aux_data_index = AUX_DATA_UNUSED); + uint8_t aux_data_index = AUX_DATA_UNUSED) { + // Dedup: O(n) scan but optimized for RAM over performance + // Skip deduplication for events - they are edge-triggered, every occurrence matters +#ifdef USE_EVENT + if (message_type != EventResponse::MESSAGE_TYPE) +#endif + { + for (const auto &item : this->items) { + if (item.entity == entity && item.message_type == message_type) + return; // Already queued + } + } + this->items.push_back({entity, message_type, estimated_size, aux_data_index}); + } // Add item to the front of the batch (for high priority messages like ping) - void add_item_front(EntityBase *entity, uint8_t message_type, uint8_t estimated_size); + void add_item_front(EntityBase *entity, uint8_t message_type, uint8_t estimated_size) { + // Swap to front avoids expensive vector::insert which shifts all elements + this->items.push_back({entity, message_type, estimated_size, AUX_DATA_UNUSED}); + if (this->items.size() > 1) { + std::swap(this->items.front(), this->items.back()); + } + } // Clear all items void clear() { @@ -572,8 +673,8 @@ class APIConnection final : public APIServerConnection { batch_start_time = 0; } - // Remove processed items from the front - void remove_front(size_t count) { items.erase(items.begin(), items.begin() + count); } + // Remove processed items from the front — noinline to keep memmove out of warm callers + void remove_front(size_t count) __attribute__((noinline)) { items.erase(items.begin(), items.begin() + count); } bool empty() const { return items.empty(); } size_t size() const { return items.size(); } @@ -629,7 +730,7 @@ class APIConnection final : public APIServerConnection { ActiveIterator active_iterator_{ActiveIterator::NONE}; // Total: 2 (flags) + 2 + 2 + 1 = 7 bytes, then 1 byte padding to next 4-byte boundary - uint32_t get_batch_delay_ms_() const; + uint32_t get_batch_delay_ms_() const { return this->parent_->get_batch_delay(); } // Message will use 8 more bytes than the minimum size, and typical // MTU is 1500. Sometimes users will see as low as 1460 MTU. // If its IPv6 the header is 40 bytes, and if its IPv4 @@ -645,6 +746,8 @@ class APIConnection final : public APIServerConnection { bool schedule_batch_(); void process_batch_(); + void process_batch_multi_(APIBuffer &shared_buf, size_t num_items, uint8_t header_padding, uint8_t footer_size) + __attribute__((noinline)); void clear_batch_() { this->deferred_batch_.clear(); this->flags_.batch_scheduled = false; @@ -652,7 +755,7 @@ class APIConnection final : public APIServerConnection { // Dispatch message encoding based on message_type - replaces function pointer storage // Switch assigns pointer, single call site for smaller code size - uint16_t dispatch_message_(const DeferredBatch::BatchItem &item, uint32_t remaining_size, bool is_single); + uint16_t dispatch_message_(const DeferredBatch::BatchItem &item, uint32_t remaining_size, bool batch_first); #ifdef HAS_PROTO_MESSAGE_DUMP void log_batch_item_(const DeferredBatch::BatchItem &item) { @@ -684,19 +787,7 @@ class APIConnection final : public APIServerConnection { // Tries immediate send if should_send_immediately_() returns true and buffer has space // Falls back to batching if immediate send fails or isn't applicable bool send_message_smart_(EntityBase *entity, uint8_t message_type, uint8_t estimated_size, - uint8_t aux_data_index = DeferredBatch::AUX_DATA_UNUSED) { - if (this->should_send_immediately_(message_type) && this->helper_->can_write_without_blocking()) { - DeferredBatch::BatchItem item{entity, message_type, estimated_size, aux_data_index}; - if (this->dispatch_message_(item, MAX_BATCH_PACKET_SIZE, true) && - this->send_buffer(ProtoWriteBuffer{&this->parent_->get_shared_buffer_ref()}, message_type)) { -#ifdef HAS_PROTO_MESSAGE_DUMP - this->log_batch_item_(item); -#endif - return true; - } - } - return this->schedule_message_(entity, message_type, estimated_size, aux_data_index); - } + uint8_t aux_data_index = DeferredBatch::AUX_DATA_UNUSED); // Helper function to schedule a deferred message with known message type bool schedule_message_(EntityBase *entity, uint8_t message_type, uint8_t estimated_size, @@ -706,10 +797,8 @@ class APIConnection final : public APIServerConnection { } // Helper function to schedule a high priority message at the front of the batch - bool schedule_message_front_(EntityBase *entity, uint8_t message_type, uint8_t estimated_size) { - this->deferred_batch_.add_item_front(entity, message_type, estimated_size); - return this->schedule_batch_(); - } + // Out-of-line: callers (on_shutdown, check_keepalive_) are cold paths + bool schedule_message_front_(EntityBase *entity, uint8_t message_type, uint8_t estimated_size); // Helper function to log client messages with name and peername void log_client_(int level, const LogString *message); diff --git a/esphome/components/api/api_frame_helper.cpp b/esphome/components/api/api_frame_helper.cpp index dd44fe9e17..6d3bd51b58 100644 --- a/esphome/components/api/api_frame_helper.cpp +++ b/esphome/components/api/api_frame_helper.cpp @@ -16,7 +16,12 @@ static const char *const TAG = "api.frame_helper"; static constexpr size_t API_MAX_LOG_BYTES = 168; #if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERY_VERBOSE -#define HELPER_LOG(msg, ...) ESP_LOGVV(TAG, "%s (%s): " msg, this->client_name_, this->client_peername_, ##__VA_ARGS__) +#define HELPER_LOG(msg, ...) \ + do { \ + char peername_buf[socket::SOCKADDR_STR_LEN]; \ + this->get_peername_to(peername_buf); \ + ESP_LOGVV(TAG, "%s (%s): " msg, this->client_name_, peername_buf, ##__VA_ARGS__); \ + } while (0) #else #define HELPER_LOG(msg, ...) ((void) 0) #endif @@ -95,149 +100,70 @@ const LogString *api_error_to_logstr(APIError err) { return LOG_STR("UNKNOWN"); } -// Default implementation for loop - handles sending buffered data -APIError APIFrameHelper::loop() { - if (this->tx_buf_count_ > 0) { - APIError err = try_send_tx_buf_(); - if (err != APIError::OK && err != APIError::WOULD_BLOCK) { - return err; +APIError APIFrameHelper::drain_overflow_and_handle_errors_() { + if (this->overflow_buf_.try_drain(this->socket_.get()) == -1) { + int err = errno; + if (this->check_socket_write_err_(err) != APIError::WOULD_BLOCK) { + HELPER_LOG("Socket write failed with errno %d", err); + return APIError::SOCKET_WRITE_FAILED; } } - return APIError::OK; // Convert WOULD_BLOCK to OK to avoid connection termination + return APIError::OK; } -// Common socket write error handling -APIError APIFrameHelper::handle_socket_write_error_() { - if (errno == EWOULDBLOCK || errno == EAGAIN) { - return APIError::WOULD_BLOCK; - } - HELPER_LOG("Socket write failed with errno %d", errno); - this->state_ = State::FAILED; - return APIError::SOCKET_WRITE_FAILED; -} - -// Helper method to buffer data from IOVs -void APIFrameHelper::buffer_data_from_iov_(const struct iovec *iov, int iovcnt, uint16_t total_write_len, - uint16_t offset) { - // Check if queue is full - if (this->tx_buf_count_ >= API_MAX_SEND_QUEUE) { - HELPER_LOG("Send queue full (%u buffers), dropping connection", this->tx_buf_count_); - this->state_ = State::FAILED; - return; - } - - uint16_t buffer_size = total_write_len - offset; - auto &buffer = this->tx_buf_[this->tx_buf_tail_]; - buffer = std::make_unique(SendBuffer{ - .data = std::make_unique(buffer_size), - .size = buffer_size, - .offset = 0, - }); - - uint16_t to_skip = offset; - uint16_t write_pos = 0; - - for (int i = 0; i < iovcnt; i++) { - if (to_skip >= iov[i].iov_len) { - // Skip this entire segment - to_skip -= static_cast(iov[i].iov_len); - } else { - // Include this segment (partially or fully) - const uint8_t *src = reinterpret_cast(iov[i].iov_base) + to_skip; - uint16_t len = static_cast(iov[i].iov_len) - to_skip; - std::memcpy(buffer->data.get() + write_pos, src, len); - write_pos += len; - to_skip = 0; - } - } - - // Update circular buffer tracking - this->tx_buf_tail_ = (this->tx_buf_tail_ + 1) % API_MAX_SEND_QUEUE; - this->tx_buf_count_++; -} - -// This method writes data to socket or buffers it +// Write data to socket, overflow to backlog buffer if LWIP TCP send buffer is full. +// Returns OK if all data was sent or successfully queued. +// Returns SOCKET_WRITE_FAILED on hard error (sets state to FAILED). APIError APIFrameHelper::write_raw_(const struct iovec *iov, int iovcnt, uint16_t total_write_len) { - // Returns APIError::OK if successful (or would block, but data has been buffered) - // Returns APIError::SOCKET_WRITE_FAILED if socket write failed, and sets state to FAILED - - if (iovcnt == 0) - return APIError::OK; // Nothing to do, success - #ifdef HELPER_LOG_PACKETS for (int i = 0; i < iovcnt; i++) { LOG_PACKET_SENDING(reinterpret_cast(iov[i].iov_base), iov[i].iov_len); } #endif - // Try to send any existing buffered data first if there is any - if (this->tx_buf_count_ > 0) { - APIError send_result = try_send_tx_buf_(); - // If real error occurred (not just WOULD_BLOCK), return it - if (send_result != APIError::OK && send_result != APIError::WOULD_BLOCK) { - return send_result; - } + uint16_t skip = 0; - // If there is still data in the buffer, we can't send, buffer - // the new data and return - if (this->tx_buf_count_ > 0) { - this->buffer_data_from_iov_(iov, iovcnt, total_write_len, 0); - return APIError::OK; // Success, data buffered + // Drain any existing backlog first + if (!this->overflow_buf_.empty()) [[unlikely]] { + APIError err = this->drain_overflow_and_handle_errors_(); + if (err != APIError::OK) + return err; + } + + // If backlog is clear, try direct send + if (this->overflow_buf_.empty()) [[likely]] { + ssize_t sent = + (iovcnt == 1) ? this->socket_->write(iov[0].iov_base, iov[0].iov_len) : this->socket_->writev(iov, iovcnt); + + if (sent == -1) [[unlikely]] { + int err = errno; + if (this->check_socket_write_err_(err) != APIError::WOULD_BLOCK) { + HELPER_LOG("Socket write failed with errno %d", err); + return APIError::SOCKET_WRITE_FAILED; + } + } else if (static_cast(sent) >= total_write_len) [[likely]] { + return APIError::OK; + } else { + skip = static_cast(sent); } } - // Try to send directly if no buffered data - // Optimize for single iovec case (common for plaintext API) - ssize_t sent = - (iovcnt == 1) ? this->socket_->write(iov[0].iov_base, iov[0].iov_len) : this->socket_->writev(iov, iovcnt); - - if (sent == -1) { - APIError err = this->handle_socket_write_error_(); - if (err == APIError::WOULD_BLOCK) { - // Socket would block, buffer the data - this->buffer_data_from_iov_(iov, iovcnt, total_write_len, 0); - return APIError::OK; // Success, data buffered - } - return err; // Socket write failed - } else if (static_cast(sent) < total_write_len) { - // Partially sent, buffer the remaining data - this->buffer_data_from_iov_(iov, iovcnt, total_write_len, static_cast(sent)); + // Queue unsent data into overflow buffer + if (!this->overflow_buf_.enqueue_iov(iov, iovcnt, total_write_len, skip)) { + HELPER_LOG("Overflow buffer full, dropping connection"); + this->state_ = State::FAILED; + return APIError::SOCKET_WRITE_FAILED; } - - return APIError::OK; // Success, all data sent or buffered + return APIError::OK; } -// Common implementation for trying to send buffered data -// IMPORTANT: Caller MUST ensure tx_buf_count_ > 0 before calling this method -APIError APIFrameHelper::try_send_tx_buf_() { - // Try to send from tx_buf - we assume it's not empty as it's the caller's responsibility to check - while (this->tx_buf_count_ > 0) { - // Get the first buffer in the queue - SendBuffer *front_buffer = this->tx_buf_[this->tx_buf_head_].get(); - - // Try to send the remaining data in this buffer - ssize_t sent = this->socket_->write(front_buffer->current_data(), front_buffer->remaining()); - - if (sent == -1) { - return this->handle_socket_write_error_(); - } else if (sent == 0) { - // Nothing sent but not an error - return APIError::WOULD_BLOCK; - } else if (static_cast(sent) < front_buffer->remaining()) { - // Partially sent, update offset - // Cast to ensure no overflow issues with uint16_t - front_buffer->offset += static_cast(sent); - return APIError::WOULD_BLOCK; // Stop processing more buffers if we couldn't send a complete buffer - } else { - // Buffer completely sent, remove it from the queue - this->tx_buf_[this->tx_buf_head_].reset(); - this->tx_buf_head_ = (this->tx_buf_head_ + 1) % API_MAX_SEND_QUEUE; - this->tx_buf_count_--; - // Continue loop to try sending the next buffer - } +const char *APIFrameHelper::get_peername_to(std::span buf) const { + if (this->socket_) { + this->socket_->getpeername_to(buf); + } else { + buf[0] = '\0'; } - - return APIError::OK; // All buffers sent successfully + return buf.data(); } APIError APIFrameHelper::init_common_() { @@ -245,8 +171,6 @@ APIError APIFrameHelper::init_common_() { HELPER_LOG("Bad state for init %d", (int) state_); return APIError::BAD_STATE; } - // Cache peername now while socket is valid - needed for error logging after socket failure - this->socket_->getpeername_to(this->client_peername_); int err = this->socket_->setblocking(false); if (err != 0) { state_ = State::FAILED; @@ -266,11 +190,12 @@ APIError APIFrameHelper::init_common_() { APIError APIFrameHelper::handle_socket_read_result_(ssize_t received) { if (received == -1) { - if (errno == EWOULDBLOCK || errno == EAGAIN) { + const int err = errno; + if (err == EWOULDBLOCK || err == EAGAIN) { return APIError::WOULD_BLOCK; } state_ = State::FAILED; - HELPER_LOG("Socket read failed with errno %d", errno); + HELPER_LOG("Socket read failed with errno %d", err); return APIError::SOCKET_READ_FAILED; } else if (received == 0) { state_ = State::FAILED; diff --git a/esphome/components/api/api_frame_helper.h b/esphome/components/api/api_frame_helper.h index f311e34fd7..72ccf8aa56 100644 --- a/esphome/components/api/api_frame_helper.h +++ b/esphome/components/api/api_frame_helper.h @@ -5,13 +5,15 @@ #include #include #include -#include #include "esphome/core/defines.h" #ifdef USE_API +#include "esphome/components/api/api_buffer.h" +#include "esphome/components/api/api_overflow_buffer.h" #include "esphome/components/socket/socket.h" #include "esphome/core/application.h" #include "esphome/core/log.h" +#include "proto.h" namespace esphome::api { @@ -29,12 +31,14 @@ static constexpr uint16_t MAX_MESSAGE_SIZE = 8192; // 8 KiB for ESP8266 static constexpr uint16_t MAX_MESSAGE_SIZE = 32768; // 32 KiB for ESP32 and other platforms #endif +// Extra byte reserved in rx_buf_ beyond the message size so protobuf +// StringRef fields can be null-terminated in-place after decode. +static constexpr uint16_t RX_BUF_NULL_TERMINATOR = 1; + // Maximum number of messages to batch in a single write operation // Must be >= MAX_INITIAL_PER_BATCH in api_connection.h (enforced by static_assert there) static constexpr size_t MAX_MESSAGES_PER_BATCH = 34; -class ProtoWriteBuffer; - // Max client name length (e.g., "Home Assistant 2026.1.0.dev0" = 28 chars) static constexpr size_t CLIENT_INFO_NAME_MAX_LEN = 32; @@ -90,8 +94,9 @@ class APIFrameHelper { // Get client name (null-terminated) const char *get_client_name() const { return this->client_name_; } - // Get client peername/IP (null-terminated, cached at init time for availability after socket failure) - const char *get_client_peername() const { return this->client_peername_; } + // Get client peername/IP into caller-provided buffer (fetches on-demand from socket) + // Returns pointer to buf for convenience in printf-style calls + const char *get_peername_to(std::span buf) const; // Set client name from buffer with length (truncates if needed) void set_client_name(const char *name, size_t len) { size_t copy_len = std::min(len, sizeof(this->client_name_) - 1); @@ -100,11 +105,13 @@ class APIFrameHelper { } virtual ~APIFrameHelper() = default; virtual APIError init() = 0; - virtual APIError loop(); + virtual APIError loop() = 0; virtual APIError read_packet(ReadPacketBuffer *buffer) = 0; - bool can_write_without_blocking() { return this->state_ == State::DATA && this->tx_buf_count_ == 0; } + bool can_write_without_blocking() { return this->state_ == State::DATA && this->overflow_buf_.empty(); } int getpeername(struct sockaddr *addr, socklen_t *addrlen) { return socket_->getpeername(addr, addrlen); } APIError close() { + if (state_ == State::CLOSED) + return APIError::OK; // Already closed state_ = State::CLOSED; int err = this->socket_->close(); if (err == -1) @@ -127,34 +134,41 @@ class APIFrameHelper { // // For log messages: Use Nagle to coalesce multiple small log packets into // fewer larger packets, reducing WiFi overhead. However, we limit batching - // to 3 messages to avoid excessive LWIP buffer pressure on memory-constrained - // devices like ESP8266. LWIP's TCP_OVERSIZE option coalesces the data into - // shared pbufs, but holding data too long waiting for Nagle's timer causes - // buffer exhaustion and dropped messages. + // to avoid excessive LWIP buffer pressure on memory-constrained devices. + // LWIP's TCP_OVERSIZE option coalesces the data into shared pbufs, but + // holding data too long waiting for Nagle's timer causes buffer exhaustion + // and dropped messages. // - // Flow: Log 1 (Nagle on) -> Log 2 (Nagle on) -> Log 3 (NODELAY, flush all) + // ESP32 (TCP_SND_BUF=4×MSS+) / RP2040 (8×MSS) / LibreTiny (4×MSS): 4 logs per cycle + // ESP8266 (2×MSS): 3 logs per cycle (tightest buffers) + // + // Flow (ESP32/RP2040/LT): Log 1 (Nagle on) -> Log 2 -> Log 3 -> Log 4 (NODELAY, flush) + // Flow (ESP8266): Log 1 (Nagle on) -> Log 2 -> Log 3 (NODELAY, flush all) // void set_nodelay_for_message(bool is_log_message) { if (!is_log_message) { - if (this->nodelay_state_ != NODELAY_ON) { + if (this->nodelay_counter_) { this->set_nodelay_raw_(true); - this->nodelay_state_ = NODELAY_ON; + this->nodelay_counter_ = 0; } return; } - - // Log messages 1-3: state transitions -1 -> 1 -> 2 -> -1 (flush on 3rd) - if (this->nodelay_state_ == NODELAY_ON) { + // Log message: enable Nagle on first, flush after LOG_NAGLE_COUNT + if (!this->nodelay_counter_) this->set_nodelay_raw_(false); - this->nodelay_state_ = 1; - } else if (this->nodelay_state_ >= LOG_NAGLE_COUNT) { + if (++this->nodelay_counter_ > LOG_NAGLE_COUNT) { this->set_nodelay_raw_(true); - this->nodelay_state_ = NODELAY_ON; - } else { - this->nodelay_state_++; + this->nodelay_counter_ = 0; } } - virtual APIError write_protobuf_packet(uint8_t type, ProtoWriteBuffer buffer) = 0; + APIError write_protobuf_packet(uint8_t type, ProtoWriteBuffer buffer) { + // Resize buffer to include footer space if needed (e.g. Noise MAC) + if (frame_footer_size_) + buffer.get_buffer()->resize(buffer.get_buffer()->size() + frame_footer_size_); + MessageInfo msg{type, 0, + static_cast(buffer.get_buffer()->size() - frame_header_padding_ - frame_footer_size_)}; + return write_protobuf_messages(buffer, std::span(&msg, 1)); + } // Write multiple protobuf messages in a single operation // messages contains (message_type, offset, length) for each message in the buffer // The buffer contains all messages with appropriate padding before each @@ -171,37 +185,28 @@ class APIFrameHelper { // rx_buf_len_ tracks bytes read so far; if non-zero, we're mid-frame // and clearing would lose partially received data. if (this->rx_buf_len_ == 0) { - // Use swap trick since shrink_to_fit() is non-binding and may be ignored - std::vector().swap(this->rx_buf_); + this->rx_buf_.release(); } } protected: - // Buffer containing data to be sent - struct SendBuffer { - std::unique_ptr data; - uint16_t size{0}; // Total size of the buffer - uint16_t offset{0}; // Current offset within the buffer - - // Using uint16_t reduces memory usage since ESPHome API messages are limited to UINT16_MAX (65535) bytes - uint16_t remaining() const { return size - offset; } - const uint8_t *current_data() const { return data.get() + offset; } - }; + // Drain backlogged overflow data to the socket and handle errors. + // Called when overflow_buf_.empty() is false. Out-of-line to keep the + // fast path (empty check) inline at call sites. + // Returns OK for transient errors (WOULD_BLOCK), SOCKET_WRITE_FAILED for hard errors. + APIError drain_overflow_and_handle_errors_(); // Common implementation for writing raw data to socket APIError write_raw_(const struct iovec *iov, int iovcnt, uint16_t total_write_len); - // Try to send data from the tx buffer - APIError try_send_tx_buf_(); - - // Helper method to buffer data from IOVs - void buffer_data_from_iov_(const struct iovec *iov, int iovcnt, uint16_t total_write_len, uint16_t offset); - - // Common socket write error handling - APIError handle_socket_write_error_(); - template - APIError write_raw_(const struct iovec *iov, int iovcnt, socket::Socket *socket, std::vector &tx_buf, - const std::string &info, StateEnum &state, StateEnum failed_state); + // Check if a socket write errno is a hard error (not WOULD_BLOCK/EAGAIN). + // Returns WOULD_BLOCK for transient errors, SOCKET_WRITE_FAILED for hard errors. + APIError check_socket_write_err_(int err) { + if (err == EWOULDBLOCK || err == EAGAIN) + return APIError::WOULD_BLOCK; + this->state_ = State::FAILED; + return APIError::SOCKET_WRITE_FAILED; + } // Socket ownership (4 bytes on 32-bit, 8 bytes on 64-bit) std::unique_ptr socket_; @@ -225,29 +230,40 @@ class APIFrameHelper { EXPLICIT_REJECT = 8, // Noise only }; - // Containers (size varies, but typically 12+ bytes on 32-bit) - std::array, API_MAX_SEND_QUEUE> tx_buf_; - std::vector rx_buf_; + // Fast inline state check for read_packet/write_protobuf_messages hot path. + // Returns OK only in DATA state; maps CLOSED/FAILED to BAD_STATE and any + // other intermediate state to WOULD_BLOCK. + inline APIError ESPHOME_ALWAYS_INLINE check_data_state_() const { + if (this->state_ == State::DATA) + return APIError::OK; + if (this->state_ == State::CLOSED || this->state_ == State::FAILED) + return APIError::BAD_STATE; + return APIError::WOULD_BLOCK; + } + + // Backlog for unsent data when TCP send buffer is full (rarely used in production) + APIOverflowBuffer overflow_buf_; + APIBuffer rx_buf_; // Client name buffer - stores name from Hello message or initial peername char client_name_[CLIENT_INFO_NAME_MAX_LEN]{}; - // Cached peername/IP address - captured at init time for availability after socket failure - char client_peername_[socket::SOCKADDR_STR_LEN]{}; // Group smaller types together uint16_t rx_buf_len_ = 0; State state_{State::INITIALIZE}; uint8_t frame_header_padding_{0}; uint8_t frame_footer_size_{0}; - uint8_t tx_buf_head_{0}; - uint8_t tx_buf_tail_{0}; - uint8_t tx_buf_count_{0}; - // Nagle batching state for log messages. NODELAY_ON (-1) means NODELAY is enabled - // (immediate send). Values 1-2 count log messages in the current Nagle batch. - // After LOG_NAGLE_COUNT logs, we switch to NODELAY to flush and reset. - static constexpr int8_t NODELAY_ON = -1; - static constexpr int8_t LOG_NAGLE_COUNT = 2; - int8_t nodelay_state_{NODELAY_ON}; + // Nagle batching counter for log messages. 0 means NODELAY is enabled (immediate send). + // Values 1..LOG_NAGLE_COUNT count log messages in the current Nagle batch. + // After LOG_NAGLE_COUNT logs, we flush by re-enabling NODELAY and resetting to 0. + // ESP8266 has the tightest TCP send buffer (2×MSS) and needs conservative batching. + // ESP32 (4×MSS+), RP2040 (8×MSS), and LibreTiny (4×MSS) can coalesce more. +#ifdef USE_ESP8266 + static constexpr uint8_t LOG_NAGLE_COUNT = 2; +#else + static constexpr uint8_t LOG_NAGLE_COUNT = 3; +#endif + uint8_t nodelay_counter_{0}; // Internal helper to set TCP_NODELAY socket option void set_nodelay_raw_(bool enable) { diff --git a/esphome/components/api/api_frame_helper_noise.cpp b/esphome/components/api/api_frame_helper_noise.cpp index 4a9257231d..78e87793fc 100644 --- a/esphome/components/api/api_frame_helper_noise.cpp +++ b/esphome/components/api/api_frame_helper_noise.cpp @@ -19,7 +19,7 @@ namespace esphome::api { static const char *const TAG = "api.noise"; #ifdef USE_ESP8266 -static const char PROLOGUE_INIT[] PROGMEM = "NoiseAPIInit"; +static constexpr char PROLOGUE_INIT[] PROGMEM = "NoiseAPIInit"; #else static const char *const PROLOGUE_INIT = "NoiseAPIInit"; #endif @@ -29,7 +29,12 @@ static constexpr size_t PROLOGUE_INIT_LEN = 12; // strlen("NoiseAPIInit") static constexpr size_t API_MAX_LOG_BYTES = 168; #if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERY_VERBOSE -#define HELPER_LOG(msg, ...) ESP_LOGVV(TAG, "%s (%s): " msg, this->client_name_, this->client_peername_, ##__VA_ARGS__) +#define HELPER_LOG(msg, ...) \ + do { \ + char peername_buf[socket::SOCKADDR_STR_LEN]; \ + this->get_peername_to(peername_buf); \ + ESP_LOGVV(TAG, "%s (%s): " msg, this->client_name_, peername_buf, ##__VA_ARGS__); \ + } while (0) #else #define HELPER_LOG(msg, ...) ((void) 0) #endif @@ -133,10 +138,12 @@ APIError APINoiseFrameHelper::handle_noise_error_(int err, const LogString *func /// Run through handshake messages (if in that phase) APIError APINoiseFrameHelper::loop() { - // During handshake phase, process as many actions as possible until we can't progress - // socket_->ready() stays true until next main loop, but state_action() will return - // WOULD_BLOCK when no more data is available to read - while (state_ != State::DATA && this->socket_->ready()) { + // Cache ready() outside the loop. On ESP8266 LWIP raw TCP, ready() returns false once + // the rx buffer is consumed. Re-checking each iteration would block handshake writes + // that must follow reads, deadlocking the handshake. state_action() will return + // WOULD_BLOCK when no more data is available to read. + bool socket_ready = this->socket_->ready(); + while (state_ != State::DATA && socket_ready) { APIError err = state_action_(); if (err == APIError::WOULD_BLOCK) { break; @@ -146,8 +153,10 @@ APIError APINoiseFrameHelper::loop() { } } - // Use base class implementation for buffer sending - return APIFrameHelper::loop(); + if (!this->overflow_buf_.empty()) [[unlikely]] { + return this->drain_overflow_and_handle_errors_(); + } + return APIError::OK; } /** Read a packet into the rx_buf_. @@ -187,17 +196,20 @@ APIError APINoiseFrameHelper::try_read_frame_() { uint16_t msg_size = (((uint16_t) rx_header_buf_[1]) << 8) | rx_header_buf_[2]; // Check against size limits to prevent OOM: MAX_HANDSHAKE_SIZE for handshake, MAX_MESSAGE_SIZE for data - uint16_t limit = (state_ == State::DATA) ? MAX_MESSAGE_SIZE : MAX_HANDSHAKE_SIZE; + bool is_data = (state_ == State::DATA); + uint16_t limit = is_data ? MAX_MESSAGE_SIZE : MAX_HANDSHAKE_SIZE; if (msg_size > limit) { state_ = State::FAILED; HELPER_LOG("Bad packet: message size %u exceeds maximum %u", msg_size, limit); - return (state_ == State::DATA) ? APIError::BAD_DATA_PACKET : APIError::BAD_HANDSHAKE_PACKET_LEN; + return is_data ? APIError::BAD_DATA_PACKET : APIError::BAD_HANDSHAKE_PACKET_LEN; } - // Reserve space for body - if (this->rx_buf_.size() != msg_size) { - this->rx_buf_.resize(msg_size); - } + // Reserve space for body (+ null terminator in DATA state so protobuf + // StringRef fields can be safely null-terminated in-place after decode. + // During handshake, rx_buf_.size() is used in prologue construction, so + // the buffer must be exactly msg_size to avoid prologue mismatch.) + uint16_t alloc_size = msg_size + (is_data ? RX_BUF_NULL_TERMINATOR : 0); + this->rx_buf_.resize(alloc_size); if (rx_buf_len_ < msg_size) { // more data to read @@ -248,16 +260,19 @@ APIError APINoiseFrameHelper::state_action_() { // ignore contents, may be used in future for flags // Resize for: existing prologue + 2 size bytes + frame data size_t old_size = this->prologue_.size(); - this->prologue_.resize(old_size + 2 + this->rx_buf_.size()); - this->prologue_[old_size] = (uint8_t) (this->rx_buf_.size() >> 8); - this->prologue_[old_size + 1] = (uint8_t) this->rx_buf_.size(); - std::memcpy(this->prologue_.data() + old_size + 2, this->rx_buf_.data(), this->rx_buf_.size()); + size_t rx_size = this->rx_buf_.size(); + this->prologue_.resize(old_size + 2 + rx_size); + this->prologue_[old_size] = (uint8_t) (rx_size >> 8); + this->prologue_[old_size + 1] = (uint8_t) rx_size; + if (rx_size > 0) { + std::memcpy(this->prologue_.data() + old_size + 2, this->rx_buf_.data(), rx_size); + } state_ = State::SERVER_HELLO; } if (state_ == State::SERVER_HELLO) { // send server hello - const std::string &name = App.get_name(); + const auto &name = App.get_name(); char mac[MAC_ADDRESS_BUFFER_SIZE]; get_mac_address_into_buffer(mac); @@ -363,6 +378,7 @@ void APINoiseFrameHelper::send_explicit_handshake_reject_(const LogString *reaso #ifdef USE_STORE_LOG_STR_IN_FLASH // On ESP8266 with flash strings, we need to use PROGMEM-aware functions size_t reason_len = strlen_P(reinterpret_cast(reason)); + reason_len = std::min(reason_len, sizeof(data) - 1); if (reason_len > 0) { memcpy_P(data + 1, reinterpret_cast(reason), reason_len); } @@ -370,6 +386,7 @@ void APINoiseFrameHelper::send_explicit_handshake_reject_(const LogString *reaso // Normal memory access const char *reason_str = LOG_STR_ARG(reason); size_t reason_len = strlen(reason_str); + reason_len = std::min(reason_len, sizeof(data) - 1); if (reason_len > 0) { // NOLINTNEXTLINE(bugprone-not-null-terminated-result) - binary protocol, not a C string std::memcpy(data + 1, reason_str, reason_len); @@ -385,14 +402,9 @@ void APINoiseFrameHelper::send_explicit_handshake_reject_(const LogString *reaso state_ = orig_state; } APIError APINoiseFrameHelper::read_packet(ReadPacketBuffer *buffer) { - APIError aerr = this->state_action_(); - if (aerr != APIError::OK) { + APIError aerr = this->check_data_state_(); + if (aerr != APIError::OK) return aerr; - } - - if (this->state_ != State::DATA) { - return APIError::WOULD_BLOCK; - } aerr = this->try_read_frame_(); if (aerr != APIError::OK) @@ -400,7 +412,18 @@ APIError APINoiseFrameHelper::read_packet(ReadPacketBuffer *buffer) { NoiseBuffer mbuf; noise_buffer_init(mbuf); - noise_buffer_set_inout(mbuf, this->rx_buf_.data(), this->rx_buf_.size(), this->rx_buf_.size()); + // read_packet() must only be called in DATA state; the extra + // RX_BUF_NULL_TERMINATOR byte is only allocated in DATA state + // (see try_read_frame_), so calling this during handshake would + // underflow the size calculation below. +#ifdef ESPHOME_DEBUG_API + assert(this->state_ == State::DATA); +#endif + // rx_buf_ has RX_BUF_NULL_TERMINATOR extra byte for null termination + // (only added in DATA state — see try_read_frame_), so subtract it + // to get the actual encrypted data size for decryption. + size_t encrypted_size = this->rx_buf_.size() - RX_BUF_NULL_TERMINATOR; + noise_buffer_set_inout(mbuf, this->rx_buf_.data(), encrypted_size, encrypted_size); int err = noise_cipherstate_decrypt(this->recv_cipher_, &mbuf); APIError decrypt_err = handle_noise_error_(err, LOG_STR("noise_cipherstate_decrypt"), APIError::CIPHERSTATE_DECRYPT_FAILED); @@ -429,23 +452,10 @@ APIError APINoiseFrameHelper::read_packet(ReadPacketBuffer *buffer) { buffer->type = type; return APIError::OK; } -APIError APINoiseFrameHelper::write_protobuf_packet(uint8_t type, ProtoWriteBuffer buffer) { - // Resize to include MAC space (required for Noise encryption) - buffer.get_buffer()->resize(buffer.get_buffer()->size() + frame_footer_size_); - MessageInfo msg{type, 0, - static_cast(buffer.get_buffer()->size() - frame_header_padding_ - frame_footer_size_)}; - return write_protobuf_messages(buffer, std::span(&msg, 1)); -} - APIError APINoiseFrameHelper::write_protobuf_messages(ProtoWriteBuffer buffer, std::span messages) { - APIError aerr = state_action_(); - if (aerr != APIError::OK) { + APIError aerr = this->check_data_state_(); + if (aerr != APIError::OK) return aerr; - } - - if (state_ != State::DATA) { - return APIError::WOULD_BLOCK; - } if (messages.empty()) { return APIError::OK; @@ -467,7 +477,7 @@ APIError APINoiseFrameHelper::write_protobuf_messages(ProtoWriteBuffer buffer, s // buf_start[1], buf_start[2] to be set after encryption // Write message header (to be encrypted) - const uint8_t msg_offset = 3; + constexpr uint8_t msg_offset = 3; buf_start[msg_offset] = static_cast(msg.message_type >> 8); // type high byte buf_start[msg_offset + 1] = static_cast(msg.message_type); // type low byte buf_start[msg_offset + 2] = static_cast(msg.payload_size >> 8); // data_len high byte @@ -556,8 +566,7 @@ APIError APINoiseFrameHelper::init_handshake_() { if (aerr != APIError::OK) return aerr; // set_prologue copies it into handshakestate, so we can get rid of it now - // Use swap idiom to actually release memory (= {} only clears size, not capacity) - std::vector().swap(prologue_); + prologue_.release(); err = noise_handshakestate_start(handshake_); aerr = handle_noise_error_(err, LOG_STR("noise_handshakestate_start"), APIError::HANDSHAKESTATE_SETUP_FAILED); @@ -567,7 +576,9 @@ APIError APINoiseFrameHelper::init_handshake_() { } APIError APINoiseFrameHelper::check_handshake_finished_() { +#ifdef ESPHOME_DEBUG_API assert(state_ == State::HANDSHAKE); +#endif int action = noise_handshakestate_get_action(handshake_); if (action == NOISE_ACTION_READ_MESSAGE || action == NOISE_ACTION_WRITE_MESSAGE) diff --git a/esphome/components/api/api_frame_helper_noise.h b/esphome/components/api/api_frame_helper_noise.h index 183b8c8a51..a6b17ff3b9 100644 --- a/esphome/components/api/api_frame_helper_noise.h +++ b/esphome/components/api/api_frame_helper_noise.h @@ -22,7 +22,6 @@ class APINoiseFrameHelper final : public APIFrameHelper { APIError init() override; APIError loop() override; APIError read_packet(ReadPacketBuffer *buffer) override; - APIError write_protobuf_packet(uint8_t type, ProtoWriteBuffer buffer) override; APIError write_protobuf_messages(ProtoWriteBuffer buffer, std::span messages) override; protected: @@ -43,8 +42,8 @@ class APINoiseFrameHelper final : public APIFrameHelper { // Reference to noise context (4 bytes on 32-bit) APINoiseContext &ctx_; - // Vector (12 bytes on 32-bit) - std::vector prologue_; + // Buffer for noise handshake prologue (released after handshake) + APIBuffer prologue_; // NoiseProtocolId (size depends on implementation) NoiseProtocolId nid_; diff --git a/esphome/components/api/api_frame_helper_plaintext.cpp b/esphome/components/api/api_frame_helper_plaintext.cpp index 3dfd683929..9e669b31ee 100644 --- a/esphome/components/api/api_frame_helper_plaintext.cpp +++ b/esphome/components/api/api_frame_helper_plaintext.cpp @@ -21,7 +21,12 @@ static const char *const TAG = "api.plaintext"; static constexpr size_t API_MAX_LOG_BYTES = 168; #if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERY_VERBOSE -#define HELPER_LOG(msg, ...) ESP_LOGVV(TAG, "%s (%s): " msg, this->client_name_, this->client_peername_, ##__VA_ARGS__) +#define HELPER_LOG(msg, ...) \ + do { \ + char peername_buf[socket::SOCKADDR_STR_LEN]; \ + this->get_peername_to(peername_buf); \ + ESP_LOGVV(TAG, "%s (%s): " msg, this->client_name_, peername_buf, ##__VA_ARGS__); \ + } while (0) #else #define HELPER_LOG(msg, ...) ((void) 0) #endif @@ -59,8 +64,10 @@ APIError APIPlaintextFrameHelper::loop() { if (state_ != State::DATA) { return APIError::BAD_STATE; } - // Use base class implementation for buffer sending - return APIFrameHelper::loop(); + if (!this->overflow_buf_.empty()) [[unlikely]] { + return this->drain_overflow_and_handle_errors_(); + } + return APIError::OK; } /** Read a packet into the rx_buf_. @@ -123,45 +130,44 @@ APIError APIPlaintextFrameHelper::try_read_frame_() { // Skip indicator byte at position 0 uint8_t varint_pos = 1; - uint32_t consumed = 0; - auto msg_size_varint = ProtoVarInt::parse(&rx_header_buf_[varint_pos], rx_header_buf_pos_ - varint_pos, &consumed); + // rx_header_buf_pos_ >= 3 and varint_pos == 1, so len >= 2 + auto msg_size_varint = ProtoVarInt::parse_non_empty(&rx_header_buf_[varint_pos], rx_header_buf_pos_ - varint_pos); if (!msg_size_varint.has_value()) { // not enough data there yet continue; } - if (msg_size_varint->as_uint32() > MAX_MESSAGE_SIZE) { + if (msg_size_varint.value > MAX_MESSAGE_SIZE) { state_ = State::FAILED; - HELPER_LOG("Bad packet: message size %" PRIu32 " exceeds maximum %u", msg_size_varint->as_uint32(), - MAX_MESSAGE_SIZE); + HELPER_LOG("Bad packet: message size %" PRIu32 " exceeds maximum %u", + static_cast(msg_size_varint.value), MAX_MESSAGE_SIZE); return APIError::BAD_DATA_PACKET; } - rx_header_parsed_len_ = msg_size_varint->as_uint16(); + rx_header_parsed_len_ = static_cast(msg_size_varint.value); // Move to next varint position - varint_pos += consumed; + varint_pos += msg_size_varint.consumed; - auto msg_type_varint = ProtoVarInt::parse(&rx_header_buf_[varint_pos], rx_header_buf_pos_ - varint_pos, &consumed); + auto msg_type_varint = ProtoVarInt::parse(&rx_header_buf_[varint_pos], rx_header_buf_pos_ - varint_pos); if (!msg_type_varint.has_value()) { // not enough data there yet continue; } - if (msg_type_varint->as_uint32() > std::numeric_limits::max()) { + if (msg_type_varint.value > std::numeric_limits::max()) { state_ = State::FAILED; - HELPER_LOG("Bad packet: message type %" PRIu32 " exceeds maximum %u", msg_type_varint->as_uint32(), - std::numeric_limits::max()); + HELPER_LOG("Bad packet: message type %" PRIu32 " exceeds maximum %u", + static_cast(msg_type_varint.value), std::numeric_limits::max()); return APIError::BAD_DATA_PACKET; } - rx_header_parsed_type_ = msg_type_varint->as_uint16(); + rx_header_parsed_type_ = static_cast(msg_type_varint.value); rx_header_parsed_ = true; } // header reading done - // Reserve space for body - if (this->rx_buf_.size() != this->rx_header_parsed_len_) { - this->rx_buf_.resize(this->rx_header_parsed_len_); - } + // Reserve space for body (+ null terminator so protobuf StringRef fields + // can be safely null-terminated in-place after decode) + this->rx_buf_.resize(this->rx_header_parsed_len_ + RX_BUF_NULL_TERMINATOR); if (rx_buf_len_ < rx_header_parsed_len_) { // more data to read @@ -189,11 +195,11 @@ APIError APIPlaintextFrameHelper::try_read_frame_() { } APIError APIPlaintextFrameHelper::read_packet(ReadPacketBuffer *buffer) { - if (this->state_ != State::DATA) { - return APIError::WOULD_BLOCK; - } + APIError aerr = this->check_data_state_(); + if (aerr != APIError::OK) + return aerr; - APIError aerr = this->try_read_frame_(); + aerr = this->try_read_frame_(); if (aerr != APIError::OK) { if (aerr == APIError::BAD_INDICATOR) { // Make sure to tell the remote that we don't @@ -231,16 +237,11 @@ APIError APIPlaintextFrameHelper::read_packet(ReadPacketBuffer *buffer) { buffer->type = this->rx_header_parsed_type_; return APIError::OK; } -APIError APIPlaintextFrameHelper::write_protobuf_packet(uint8_t type, ProtoWriteBuffer buffer) { - MessageInfo msg{type, 0, static_cast(buffer.get_buffer()->size() - frame_header_padding_)}; - return write_protobuf_messages(buffer, std::span(&msg, 1)); -} - APIError APIPlaintextFrameHelper::write_protobuf_messages(ProtoWriteBuffer buffer, std::span messages) { - if (state_ != State::DATA) { - return APIError::BAD_STATE; - } + APIError aerr = this->check_data_state_(); + if (aerr != APIError::OK) + return aerr; if (messages.empty()) { return APIError::OK; @@ -253,9 +254,11 @@ APIError APIPlaintextFrameHelper::write_protobuf_messages(ProtoWriteBuffer buffe uint16_t total_write_len = 0; for (const auto &msg : messages) { - // Calculate varint sizes for header layout - uint8_t size_varint_len = api::ProtoSize::varint(static_cast(msg.payload_size)); - uint8_t type_varint_len = api::ProtoSize::varint(static_cast(msg.message_type)); + // Calculate varint sizes for header layout using inline ternary to avoid varint_slow call overhead + uint8_t size_varint_len = msg.payload_size < ProtoSize::VARINT_THRESHOLD_1_BYTE + ? 1 + : (msg.payload_size < ProtoSize::VARINT_THRESHOLD_2_BYTE ? 2 : 3); + uint8_t type_varint_len = msg.message_type < ProtoSize::VARINT_THRESHOLD_1_BYTE ? 1 : 2; uint8_t total_header_len = 1 + size_varint_len + type_varint_len; // Calculate where to start writing the header @@ -277,8 +280,8 @@ APIError APIPlaintextFrameHelper::write_protobuf_messages(ProtoWriteBuffer buffe // // Example 3 (large values): total_header_len = 6, header_offset = 6 - 6 = 0 // [0] - 0x00 indicator byte - // [1-3] - Payload size varint (3 bytes, for sizes 16384-2097151) - // [4-5] - Message type varint (2 bytes, for types 128-32767) + // [1-3] - Payload size varint (3 bytes, for sizes 16384-65535) + // [4-5] - Message type varint (2 bytes, for types 128-16383) // [6...] - Actual payload data // // The message starts at offset + frame_header_padding_ @@ -290,9 +293,8 @@ APIError APIPlaintextFrameHelper::write_protobuf_messages(ProtoWriteBuffer buffe buf_start[header_offset] = 0x00; // indicator // Encode varints directly into buffer - ProtoVarInt(msg.payload_size).encode_to_buffer_unchecked(buf_start + header_offset + 1, size_varint_len); - ProtoVarInt(msg.message_type) - .encode_to_buffer_unchecked(buf_start + header_offset + 1 + size_varint_len, type_varint_len); + encode_varint_to_buffer(msg.payload_size, buf_start + header_offset + 1); + encode_varint_to_buffer(msg.message_type, buf_start + header_offset + 1 + size_varint_len); // Add iovec for this message (header + payload) size_t msg_len = static_cast(total_header_len + msg.payload_size); diff --git a/esphome/components/api/api_frame_helper_plaintext.h b/esphome/components/api/api_frame_helper_plaintext.h index 96d47e9c7b..f8161c039d 100644 --- a/esphome/components/api/api_frame_helper_plaintext.h +++ b/esphome/components/api/api_frame_helper_plaintext.h @@ -19,7 +19,6 @@ class APIPlaintextFrameHelper final : public APIFrameHelper { APIError init() override; APIError loop() override; APIError read_packet(ReadPacketBuffer *buffer) override; - APIError write_protobuf_packet(uint8_t type, ProtoWriteBuffer buffer) override; APIError write_protobuf_messages(ProtoWriteBuffer buffer, std::span messages) override; protected: diff --git a/esphome/components/api/api_options.proto b/esphome/components/api/api_options.proto index a863f2c7a8..02600f0977 100644 --- a/esphome/components/api/api_options.proto +++ b/esphome/components/api/api_options.proto @@ -90,4 +90,10 @@ extend google.protobuf.FieldOptions { // - uint16_t _length_{0}; // - uint16_t _count_{0}; optional bool packed_buffer = 50015 [default=false]; + + // force: Always encode this field, even when its value equals the proto3 default. + // Skips the zero/empty check in calculate_size() and encode(), using the _force + // variant of the calc_ method. Use on fields that are almost always non-default + // to eliminate dead branches on hot paths. + optional bool force = 50016 [default=false]; } diff --git a/esphome/components/api/api_overflow_buffer.cpp b/esphome/components/api/api_overflow_buffer.cpp new file mode 100644 index 0000000000..e242d4553e --- /dev/null +++ b/esphome/components/api/api_overflow_buffer.cpp @@ -0,0 +1,73 @@ +#include "api_overflow_buffer.h" +#ifdef USE_API +#include + +namespace esphome::api { + +APIOverflowBuffer::~APIOverflowBuffer() { + for (auto *entry : this->queue_) { + if (entry != nullptr) + Entry::destroy(entry); + } +} + +ssize_t APIOverflowBuffer::try_drain(socket::Socket *socket) { + while (this->count_ > 0) { + Entry *front = this->queue_[this->head_]; + + ssize_t sent = socket->write(front->current_data(), front->remaining()); + + if (sent <= 0) { + // -1 = error (caller checks errno for EWOULDBLOCK vs hard error) + // 0 = nothing sent (treat as no progress) + return sent; + } + + if (static_cast(sent) < front->remaining()) { + // Partially sent, update offset and stop + front->offset += static_cast(sent); + return sent; + } + + // Entry fully sent — free it and advance + Entry::destroy(front); + this->queue_[this->head_] = nullptr; + this->head_ = (this->head_ + 1) % API_MAX_SEND_QUEUE; + this->count_--; + } + + return 0; // All drained +} + +bool APIOverflowBuffer::enqueue_iov(const struct iovec *iov, int iovcnt, uint16_t total_len, uint16_t skip) { + if (this->count_ >= API_MAX_SEND_QUEUE) + return false; + + uint16_t buffer_size = total_len - skip; + // NOLINTNEXTLINE(cppcoreguidelines-owning-memory) + auto *entry = new Entry{new uint8_t[buffer_size], buffer_size, 0}; + this->queue_[this->tail_] = entry; + + uint16_t to_skip = skip; + uint16_t write_pos = 0; + + for (int i = 0; i < iovcnt; i++) { + if (to_skip >= iov[i].iov_len) { + to_skip -= static_cast(iov[i].iov_len); + } else { + const uint8_t *src = reinterpret_cast(iov[i].iov_base) + to_skip; + uint16_t len = static_cast(iov[i].iov_len) - to_skip; + std::memcpy(entry->data + write_pos, src, len); + write_pos += len; + to_skip = 0; + } + } + + this->tail_ = (this->tail_ + 1) % API_MAX_SEND_QUEUE; + this->count_++; + return true; +} + +} // namespace esphome::api + +#endif // USE_API diff --git a/esphome/components/api/api_overflow_buffer.h b/esphome/components/api/api_overflow_buffer.h new file mode 100644 index 0000000000..19aae680f0 --- /dev/null +++ b/esphome/components/api/api_overflow_buffer.h @@ -0,0 +1,76 @@ +#pragma once +#include +#include +#include + +#include "esphome/core/defines.h" +#ifdef USE_API + +#include "esphome/components/socket/headers.h" +#include "esphome/components/socket/socket.h" +#include "esphome/core/helpers.h" + +namespace esphome::api { + +/// Circular queue of heap-allocated byte buffers used as a TCP send backlog. +/// +/// Under normal operation this buffer is **never used** — data goes straight +/// from the frame helper to the socket. It only fills when the LWIP TCP +/// send buffer is full (slow client, congested network, heavy logging). +/// The queue drains automatically on subsequent write/loop calls once the +/// socket becomes writable again. +/// +/// Capacity is compile-time-fixed via API_MAX_SEND_QUEUE (set from Python +/// config). If the queue fills completely the connection is marked failed. +class APIOverflowBuffer { + public: + /// A single heap-allocated send-backlog entry. + /// Lifetime is manually managed — see destroy(). + struct Entry { + uint8_t *data; + uint16_t size; // Total size of the buffer + uint16_t offset; // Current send offset within the buffer + + uint16_t remaining() const { return this->size - this->offset; } + const uint8_t *current_data() const { return this->data + this->offset; } + + /// Free this entry and its data buffer. + static ESPHOME_ALWAYS_INLINE void destroy(Entry *entry) { + delete[] entry->data; + delete entry; // NOLINT(cppcoreguidelines-owning-memory) + } + }; + + ~APIOverflowBuffer(); + + /// True when no backlogged data is waiting. + bool empty() const { return this->count_ == 0; } + + /// True when the queue has no room for another entry. + bool full() const { return this->count_ >= API_MAX_SEND_QUEUE; } + + /// Number of entries currently queued. + uint8_t count() const { return this->count_; } + + /// Try to drain queued data to the socket. + /// Returns bytes-written > 0 on success/partial, 0 if all drained or no progress, + /// -1 on error (caller must check errno to distinguish EWOULDBLOCK from hard errors). + /// Callers only need to act on -1; 0 and positive values both mean "no error". + /// Frees entries as they are fully sent. + ssize_t try_drain(socket::Socket *socket); + + /// Enqueue unsent IOV data into the backlog. + /// Copies iov data starting at byte offset `skip` into a new entry. + /// Returns false if the queue is full (caller should fail the connection). + bool enqueue_iov(const struct iovec *iov, int iovcnt, uint16_t total_len, uint16_t skip); + + protected: + std::array queue_{}; + uint8_t head_{0}; + uint8_t tail_{0}; + uint8_t count_{0}; +}; + +} // namespace esphome::api + +#endif // USE_API diff --git a/esphome/components/api/api_pb2.cpp b/esphome/components/api/api_pb2.cpp index 743f51dac7..ae2cd2bae8 100644 --- a/esphome/components/api/api_pb2.cpp +++ b/esphome/components/api/api_pb2.cpp @@ -7,13 +7,13 @@ namespace esphome::api { -bool HelloRequest::decode_varint(uint32_t field_id, ProtoVarInt value) { +bool HelloRequest::decode_varint(uint32_t field_id, proto_varint_value_t value) { switch (field_id) { case 2: - this->api_version_major = value.as_uint32(); + this->api_version_major = value; break; case 3: - this->api_version_minor = value.as_uint32(); + this->api_version_minor = value; break; default: return false; @@ -31,41 +31,59 @@ bool HelloRequest::decode_length(uint32_t field_id, ProtoLengthDelimited value) } return true; } -void HelloResponse::encode(ProtoWriteBuffer buffer) const { +void HelloResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_uint32(1, this->api_version_major); buffer.encode_uint32(2, this->api_version_minor); buffer.encode_string(3, this->server_info); buffer.encode_string(4, this->name); } -void HelloResponse::calculate_size(ProtoSize &size) const { - size.add_uint32(1, this->api_version_major); - size.add_uint32(1, this->api_version_minor); - size.add_length(1, this->server_info.size()); - size.add_length(1, this->name.size()); +uint32_t HelloResponse::calculate_size() const { + uint32_t size = 0; + size += ProtoSize::calc_uint32(1, this->api_version_major); + size += ProtoSize::calc_uint32(1, this->api_version_minor); + size += ProtoSize::calc_length(1, this->server_info.size()); + size += ProtoSize::calc_length(1, this->name.size()); + return size; } #ifdef USE_AREAS -void AreaInfo::encode(ProtoWriteBuffer buffer) const { +void AreaInfo::encode(ProtoWriteBuffer &buffer) const { buffer.encode_uint32(1, this->area_id); buffer.encode_string(2, this->name); } -void AreaInfo::calculate_size(ProtoSize &size) const { - size.add_uint32(1, this->area_id); - size.add_length(1, this->name.size()); +uint32_t AreaInfo::calculate_size() const { + uint32_t size = 0; + size += ProtoSize::calc_uint32(1, this->area_id); + size += ProtoSize::calc_length(1, this->name.size()); + return size; } #endif #ifdef USE_DEVICES -void DeviceInfo::encode(ProtoWriteBuffer buffer) const { +void DeviceInfo::encode(ProtoWriteBuffer &buffer) const { buffer.encode_uint32(1, this->device_id); buffer.encode_string(2, this->name); buffer.encode_uint32(3, this->area_id); } -void DeviceInfo::calculate_size(ProtoSize &size) const { - size.add_uint32(1, this->device_id); - size.add_length(1, this->name.size()); - size.add_uint32(1, this->area_id); +uint32_t DeviceInfo::calculate_size() const { + uint32_t size = 0; + size += ProtoSize::calc_uint32(1, this->device_id); + size += ProtoSize::calc_length(1, this->name.size()); + size += ProtoSize::calc_uint32(1, this->area_id); + return size; } #endif -void DeviceInfoResponse::encode(ProtoWriteBuffer buffer) const { +#ifdef USE_SERIAL_PROXY +void SerialProxyInfo::encode(ProtoWriteBuffer &buffer) const { + buffer.encode_string(1, this->name); + buffer.encode_uint32(2, static_cast(this->port_type)); +} +uint32_t SerialProxyInfo::calculate_size() const { + uint32_t size = 0; + size += ProtoSize::calc_length(1, this->name.size()); + size += ProtoSize::calc_uint32(1, static_cast(this->port_type)); + return size; +} +#endif +void DeviceInfoResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_string(2, this->name); buffer.encode_string(3, this->mac_address); buffer.encode_string(4, this->esphome_version); @@ -102,16 +120,16 @@ void DeviceInfoResponse::encode(ProtoWriteBuffer buffer) const { #endif #ifdef USE_DEVICES for (const auto &it : this->devices) { - buffer.encode_message(20, it); + buffer.encode_sub_message(20, it); } #endif #ifdef USE_AREAS for (const auto &it : this->areas) { - buffer.encode_message(21, it); + buffer.encode_sub_message(21, it); } #endif #ifdef USE_AREAS - buffer.encode_message(22, this->area); + buffer.encode_optional_sub_message(22, this->area); #endif #ifdef USE_ZWAVE_PROXY buffer.encode_uint32(23, this->zwave_proxy_feature_flags); @@ -119,66 +137,78 @@ void DeviceInfoResponse::encode(ProtoWriteBuffer buffer) const { #ifdef USE_ZWAVE_PROXY buffer.encode_uint32(24, this->zwave_home_id); #endif +#ifdef USE_SERIAL_PROXY + for (const auto &it : this->serial_proxies) { + buffer.encode_sub_message(25, it); + } +#endif } -void DeviceInfoResponse::calculate_size(ProtoSize &size) const { - size.add_length(1, this->name.size()); - size.add_length(1, this->mac_address.size()); - size.add_length(1, this->esphome_version.size()); - size.add_length(1, this->compilation_time.size()); - size.add_length(1, this->model.size()); +uint32_t DeviceInfoResponse::calculate_size() const { + uint32_t size = 0; + size += ProtoSize::calc_length(1, this->name.size()); + size += ProtoSize::calc_length(1, this->mac_address.size()); + size += ProtoSize::calc_length(1, this->esphome_version.size()); + size += ProtoSize::calc_length(1, this->compilation_time.size()); + size += ProtoSize::calc_length(1, this->model.size()); #ifdef USE_DEEP_SLEEP - size.add_bool(1, this->has_deep_sleep); + size += ProtoSize::calc_bool(1, this->has_deep_sleep); #endif #ifdef ESPHOME_PROJECT_NAME - size.add_length(1, this->project_name.size()); + size += ProtoSize::calc_length(1, this->project_name.size()); #endif #ifdef ESPHOME_PROJECT_NAME - size.add_length(1, this->project_version.size()); + size += ProtoSize::calc_length(1, this->project_version.size()); #endif #ifdef USE_WEBSERVER - size.add_uint32(1, this->webserver_port); + size += ProtoSize::calc_uint32(1, this->webserver_port); #endif #ifdef USE_BLUETOOTH_PROXY - size.add_uint32(1, this->bluetooth_proxy_feature_flags); + size += ProtoSize::calc_uint32(1, this->bluetooth_proxy_feature_flags); #endif - size.add_length(1, this->manufacturer.size()); - size.add_length(1, this->friendly_name.size()); + size += ProtoSize::calc_length(1, this->manufacturer.size()); + size += ProtoSize::calc_length(1, this->friendly_name.size()); #ifdef USE_VOICE_ASSISTANT - size.add_uint32(2, this->voice_assistant_feature_flags); + size += ProtoSize::calc_uint32(2, this->voice_assistant_feature_flags); #endif #ifdef USE_AREAS - size.add_length(2, this->suggested_area.size()); + size += ProtoSize::calc_length(2, this->suggested_area.size()); #endif #ifdef USE_BLUETOOTH_PROXY - size.add_length(2, this->bluetooth_mac_address.size()); + size += ProtoSize::calc_length(2, this->bluetooth_mac_address.size()); #endif #ifdef USE_API_NOISE - size.add_bool(2, this->api_encryption_supported); + size += ProtoSize::calc_bool(2, this->api_encryption_supported); #endif #ifdef USE_DEVICES for (const auto &it : this->devices) { - size.add_message_object_force(2, it); + size += ProtoSize::calc_message_force(2, it.calculate_size()); } #endif #ifdef USE_AREAS for (const auto &it : this->areas) { - size.add_message_object_force(2, it); + size += ProtoSize::calc_message_force(2, it.calculate_size()); } #endif #ifdef USE_AREAS - size.add_message_object(2, this->area); + size += ProtoSize::calc_message(2, this->area.calculate_size()); #endif #ifdef USE_ZWAVE_PROXY - size.add_uint32(2, this->zwave_proxy_feature_flags); + size += ProtoSize::calc_uint32(2, this->zwave_proxy_feature_flags); #endif #ifdef USE_ZWAVE_PROXY - size.add_uint32(2, this->zwave_home_id); + size += ProtoSize::calc_uint32(2, this->zwave_home_id); #endif +#ifdef USE_SERIAL_PROXY + for (const auto &it : this->serial_proxies) { + size += ProtoSize::calc_message_force(2, it.calculate_size()); + } +#endif + return size; } #ifdef USE_BINARY_SENSOR -void ListEntitiesBinarySensorResponse::encode(ProtoWriteBuffer buffer) const { +void ListEntitiesBinarySensorResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_string(1, this->object_id); - buffer.encode_fixed32(2, this->key); + buffer.write_tag_and_fixed32(21, this->key); buffer.encode_string(3, this->name); buffer.encode_string(5, this->device_class); buffer.encode_bool(6, this->is_status_binary_sensor); @@ -191,42 +221,46 @@ void ListEntitiesBinarySensorResponse::encode(ProtoWriteBuffer buffer) const { buffer.encode_uint32(10, this->device_id); #endif } -void ListEntitiesBinarySensorResponse::calculate_size(ProtoSize &size) const { - size.add_length(1, this->object_id.size()); - size.add_fixed32(1, this->key); - size.add_length(1, this->name.size()); - size.add_length(1, this->device_class.size()); - size.add_bool(1, this->is_status_binary_sensor); - size.add_bool(1, this->disabled_by_default); +uint32_t ListEntitiesBinarySensorResponse::calculate_size() const { + uint32_t size = 0; + size += ProtoSize::calc_length(1, this->object_id.size()); + size += 5; + size += ProtoSize::calc_length(1, this->name.size()); + size += ProtoSize::calc_length(1, this->device_class.size()); + size += ProtoSize::calc_bool(1, this->is_status_binary_sensor); + size += ProtoSize::calc_bool(1, this->disabled_by_default); #ifdef USE_ENTITY_ICON - size.add_length(1, this->icon.size()); + size += ProtoSize::calc_length(1, this->icon.size()); #endif - size.add_uint32(1, static_cast(this->entity_category)); + size += ProtoSize::calc_uint32(1, static_cast(this->entity_category)); #ifdef USE_DEVICES - size.add_uint32(1, this->device_id); + size += ProtoSize::calc_uint32(1, this->device_id); #endif + return size; } -void BinarySensorStateResponse::encode(ProtoWriteBuffer buffer) const { - buffer.encode_fixed32(1, this->key); +void BinarySensorStateResponse::encode(ProtoWriteBuffer &buffer) const { + buffer.write_tag_and_fixed32(13, this->key); buffer.encode_bool(2, this->state); buffer.encode_bool(3, this->missing_state); #ifdef USE_DEVICES buffer.encode_uint32(4, this->device_id); #endif } -void BinarySensorStateResponse::calculate_size(ProtoSize &size) const { - size.add_fixed32(1, this->key); - size.add_bool(1, this->state); - size.add_bool(1, this->missing_state); +uint32_t BinarySensorStateResponse::calculate_size() const { + uint32_t size = 0; + size += 5; + size += ProtoSize::calc_bool(1, this->state); + size += ProtoSize::calc_bool(1, this->missing_state); #ifdef USE_DEVICES - size.add_uint32(1, this->device_id); + size += ProtoSize::calc_uint32(1, this->device_id); #endif + return size; } #endif #ifdef USE_COVER -void ListEntitiesCoverResponse::encode(ProtoWriteBuffer buffer) const { +void ListEntitiesCoverResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_string(1, this->object_id); - buffer.encode_fixed32(2, this->key); + buffer.write_tag_and_fixed32(21, this->key); buffer.encode_string(3, this->name); buffer.encode_bool(5, this->assumed_state); buffer.encode_bool(6, this->supports_position); @@ -242,26 +276,28 @@ void ListEntitiesCoverResponse::encode(ProtoWriteBuffer buffer) const { buffer.encode_uint32(13, this->device_id); #endif } -void ListEntitiesCoverResponse::calculate_size(ProtoSize &size) const { - size.add_length(1, this->object_id.size()); - size.add_fixed32(1, this->key); - size.add_length(1, this->name.size()); - size.add_bool(1, this->assumed_state); - size.add_bool(1, this->supports_position); - size.add_bool(1, this->supports_tilt); - size.add_length(1, this->device_class.size()); - size.add_bool(1, this->disabled_by_default); +uint32_t ListEntitiesCoverResponse::calculate_size() const { + uint32_t size = 0; + size += ProtoSize::calc_length(1, this->object_id.size()); + size += 5; + size += ProtoSize::calc_length(1, this->name.size()); + size += ProtoSize::calc_bool(1, this->assumed_state); + size += ProtoSize::calc_bool(1, this->supports_position); + size += ProtoSize::calc_bool(1, this->supports_tilt); + size += ProtoSize::calc_length(1, this->device_class.size()); + size += ProtoSize::calc_bool(1, this->disabled_by_default); #ifdef USE_ENTITY_ICON - size.add_length(1, this->icon.size()); + size += ProtoSize::calc_length(1, this->icon.size()); #endif - size.add_uint32(1, static_cast(this->entity_category)); - size.add_bool(1, this->supports_stop); + size += ProtoSize::calc_uint32(1, static_cast(this->entity_category)); + size += ProtoSize::calc_bool(1, this->supports_stop); #ifdef USE_DEVICES - size.add_uint32(1, this->device_id); + size += ProtoSize::calc_uint32(1, this->device_id); #endif + return size; } -void CoverStateResponse::encode(ProtoWriteBuffer buffer) const { - buffer.encode_fixed32(1, this->key); +void CoverStateResponse::encode(ProtoWriteBuffer &buffer) const { + buffer.write_tag_and_fixed32(13, this->key); buffer.encode_float(3, this->position); buffer.encode_float(4, this->tilt); buffer.encode_uint32(5, static_cast(this->current_operation)); @@ -269,29 +305,31 @@ void CoverStateResponse::encode(ProtoWriteBuffer buffer) const { buffer.encode_uint32(6, this->device_id); #endif } -void CoverStateResponse::calculate_size(ProtoSize &size) const { - size.add_fixed32(1, this->key); - size.add_float(1, this->position); - size.add_float(1, this->tilt); - size.add_uint32(1, static_cast(this->current_operation)); +uint32_t CoverStateResponse::calculate_size() const { + uint32_t size = 0; + size += 5; + size += ProtoSize::calc_float(1, this->position); + size += ProtoSize::calc_float(1, this->tilt); + size += ProtoSize::calc_uint32(1, static_cast(this->current_operation)); #ifdef USE_DEVICES - size.add_uint32(1, this->device_id); + size += ProtoSize::calc_uint32(1, this->device_id); #endif + return size; } -bool CoverCommandRequest::decode_varint(uint32_t field_id, ProtoVarInt value) { +bool CoverCommandRequest::decode_varint(uint32_t field_id, proto_varint_value_t value) { switch (field_id) { case 4: - this->has_position = value.as_bool(); + this->has_position = value != 0; break; case 6: - this->has_tilt = value.as_bool(); + this->has_tilt = value != 0; break; case 8: - this->stop = value.as_bool(); + this->stop = value != 0; break; #ifdef USE_DEVICES case 9: - this->device_id = value.as_uint32(); + this->device_id = value; break; #endif default: @@ -317,9 +355,9 @@ bool CoverCommandRequest::decode_32bit(uint32_t field_id, Proto32Bit value) { } #endif #ifdef USE_FAN -void ListEntitiesFanResponse::encode(ProtoWriteBuffer buffer) const { +void ListEntitiesFanResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_string(1, this->object_id); - buffer.encode_fixed32(2, this->key); + buffer.write_tag_and_fixed32(21, this->key); buffer.encode_string(3, this->name); buffer.encode_bool(5, this->supports_oscillation); buffer.encode_bool(6, this->supports_speed); @@ -337,30 +375,32 @@ void ListEntitiesFanResponse::encode(ProtoWriteBuffer buffer) const { buffer.encode_uint32(13, this->device_id); #endif } -void ListEntitiesFanResponse::calculate_size(ProtoSize &size) const { - size.add_length(1, this->object_id.size()); - size.add_fixed32(1, this->key); - size.add_length(1, this->name.size()); - size.add_bool(1, this->supports_oscillation); - size.add_bool(1, this->supports_speed); - size.add_bool(1, this->supports_direction); - size.add_int32(1, this->supported_speed_count); - size.add_bool(1, this->disabled_by_default); +uint32_t ListEntitiesFanResponse::calculate_size() const { + uint32_t size = 0; + size += ProtoSize::calc_length(1, this->object_id.size()); + size += 5; + size += ProtoSize::calc_length(1, this->name.size()); + size += ProtoSize::calc_bool(1, this->supports_oscillation); + size += ProtoSize::calc_bool(1, this->supports_speed); + size += ProtoSize::calc_bool(1, this->supports_direction); + size += ProtoSize::calc_int32(1, this->supported_speed_count); + size += ProtoSize::calc_bool(1, this->disabled_by_default); #ifdef USE_ENTITY_ICON - size.add_length(1, this->icon.size()); + size += ProtoSize::calc_length(1, this->icon.size()); #endif - size.add_uint32(1, static_cast(this->entity_category)); + size += ProtoSize::calc_uint32(1, static_cast(this->entity_category)); if (!this->supported_preset_modes->empty()) { for (const char *it : *this->supported_preset_modes) { - size.add_length_force(1, strlen(it)); + size += ProtoSize::calc_length_force(1, strlen(it)); } } #ifdef USE_DEVICES - size.add_uint32(1, this->device_id); + size += ProtoSize::calc_uint32(1, this->device_id); #endif + return size; } -void FanStateResponse::encode(ProtoWriteBuffer buffer) const { - buffer.encode_fixed32(1, this->key); +void FanStateResponse::encode(ProtoWriteBuffer &buffer) const { + buffer.write_tag_and_fixed32(13, this->key); buffer.encode_bool(2, this->state); buffer.encode_bool(3, this->oscillating); buffer.encode_uint32(5, static_cast(this->direction)); @@ -370,49 +410,51 @@ void FanStateResponse::encode(ProtoWriteBuffer buffer) const { buffer.encode_uint32(8, this->device_id); #endif } -void FanStateResponse::calculate_size(ProtoSize &size) const { - size.add_fixed32(1, this->key); - size.add_bool(1, this->state); - size.add_bool(1, this->oscillating); - size.add_uint32(1, static_cast(this->direction)); - size.add_int32(1, this->speed_level); - size.add_length(1, this->preset_mode.size()); +uint32_t FanStateResponse::calculate_size() const { + uint32_t size = 0; + size += 5; + size += ProtoSize::calc_bool(1, this->state); + size += ProtoSize::calc_bool(1, this->oscillating); + size += ProtoSize::calc_uint32(1, static_cast(this->direction)); + size += ProtoSize::calc_int32(1, this->speed_level); + size += ProtoSize::calc_length(1, this->preset_mode.size()); #ifdef USE_DEVICES - size.add_uint32(1, this->device_id); + size += ProtoSize::calc_uint32(1, this->device_id); #endif + return size; } -bool FanCommandRequest::decode_varint(uint32_t field_id, ProtoVarInt value) { +bool FanCommandRequest::decode_varint(uint32_t field_id, proto_varint_value_t value) { switch (field_id) { case 2: - this->has_state = value.as_bool(); + this->has_state = value != 0; break; case 3: - this->state = value.as_bool(); + this->state = value != 0; break; case 6: - this->has_oscillating = value.as_bool(); + this->has_oscillating = value != 0; break; case 7: - this->oscillating = value.as_bool(); + this->oscillating = value != 0; break; case 8: - this->has_direction = value.as_bool(); + this->has_direction = value != 0; break; case 9: - this->direction = static_cast(value.as_uint32()); + this->direction = static_cast(value); break; case 10: - this->has_speed_level = value.as_bool(); + this->has_speed_level = value != 0; break; case 11: - this->speed_level = value.as_int32(); + this->speed_level = static_cast(value); break; case 12: - this->has_preset_mode = value.as_bool(); + this->has_preset_mode = value != 0; break; #ifdef USE_DEVICES case 14: - this->device_id = value.as_uint32(); + this->device_id = value; break; #endif default: @@ -443,9 +485,9 @@ bool FanCommandRequest::decode_32bit(uint32_t field_id, Proto32Bit value) { } #endif #ifdef USE_LIGHT -void ListEntitiesLightResponse::encode(ProtoWriteBuffer buffer) const { +void ListEntitiesLightResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_string(1, this->object_id); - buffer.encode_fixed32(2, this->key); + buffer.write_tag_and_fixed32(21, this->key); buffer.encode_string(3, this->name); for (const auto &it : *this->supported_color_modes) { buffer.encode_uint32(12, static_cast(it), true); @@ -464,33 +506,35 @@ void ListEntitiesLightResponse::encode(ProtoWriteBuffer buffer) const { buffer.encode_uint32(16, this->device_id); #endif } -void ListEntitiesLightResponse::calculate_size(ProtoSize &size) const { - size.add_length(1, this->object_id.size()); - size.add_fixed32(1, this->key); - size.add_length(1, this->name.size()); +uint32_t ListEntitiesLightResponse::calculate_size() const { + uint32_t size = 0; + size += ProtoSize::calc_length(1, this->object_id.size()); + size += 5; + size += ProtoSize::calc_length(1, this->name.size()); if (!this->supported_color_modes->empty()) { for (const auto &it : *this->supported_color_modes) { - size.add_uint32_force(1, static_cast(it)); + size += ProtoSize::calc_uint32_force(1, static_cast(it)); } } - size.add_float(1, this->min_mireds); - size.add_float(1, this->max_mireds); + size += ProtoSize::calc_float(1, this->min_mireds); + size += ProtoSize::calc_float(1, this->max_mireds); if (!this->effects->empty()) { for (const char *it : *this->effects) { - size.add_length_force(1, strlen(it)); + size += ProtoSize::calc_length_force(1, strlen(it)); } } - size.add_bool(1, this->disabled_by_default); + size += ProtoSize::calc_bool(1, this->disabled_by_default); #ifdef USE_ENTITY_ICON - size.add_length(1, this->icon.size()); + size += ProtoSize::calc_length(1, this->icon.size()); #endif - size.add_uint32(1, static_cast(this->entity_category)); + size += ProtoSize::calc_uint32(1, static_cast(this->entity_category)); #ifdef USE_DEVICES - size.add_uint32(2, this->device_id); + size += ProtoSize::calc_uint32(2, this->device_id); #endif + return size; } -void LightStateResponse::encode(ProtoWriteBuffer buffer) const { - buffer.encode_fixed32(1, this->key); +void LightStateResponse::encode(ProtoWriteBuffer &buffer) const { + buffer.write_tag_and_fixed32(13, this->key); buffer.encode_bool(2, this->state); buffer.encode_float(3, this->brightness); buffer.encode_uint32(11, static_cast(this->color_mode)); @@ -507,77 +551,79 @@ void LightStateResponse::encode(ProtoWriteBuffer buffer) const { buffer.encode_uint32(14, this->device_id); #endif } -void LightStateResponse::calculate_size(ProtoSize &size) const { - size.add_fixed32(1, this->key); - size.add_bool(1, this->state); - size.add_float(1, this->brightness); - size.add_uint32(1, static_cast(this->color_mode)); - size.add_float(1, this->color_brightness); - size.add_float(1, this->red); - size.add_float(1, this->green); - size.add_float(1, this->blue); - size.add_float(1, this->white); - size.add_float(1, this->color_temperature); - size.add_float(1, this->cold_white); - size.add_float(1, this->warm_white); - size.add_length(1, this->effect.size()); +uint32_t LightStateResponse::calculate_size() const { + uint32_t size = 0; + size += 5; + size += ProtoSize::calc_bool(1, this->state); + size += ProtoSize::calc_float(1, this->brightness); + size += ProtoSize::calc_uint32(1, static_cast(this->color_mode)); + size += ProtoSize::calc_float(1, this->color_brightness); + size += ProtoSize::calc_float(1, this->red); + size += ProtoSize::calc_float(1, this->green); + size += ProtoSize::calc_float(1, this->blue); + size += ProtoSize::calc_float(1, this->white); + size += ProtoSize::calc_float(1, this->color_temperature); + size += ProtoSize::calc_float(1, this->cold_white); + size += ProtoSize::calc_float(1, this->warm_white); + size += ProtoSize::calc_length(1, this->effect.size()); #ifdef USE_DEVICES - size.add_uint32(1, this->device_id); + size += ProtoSize::calc_uint32(1, this->device_id); #endif + return size; } -bool LightCommandRequest::decode_varint(uint32_t field_id, ProtoVarInt value) { +bool LightCommandRequest::decode_varint(uint32_t field_id, proto_varint_value_t value) { switch (field_id) { case 2: - this->has_state = value.as_bool(); + this->has_state = value != 0; break; case 3: - this->state = value.as_bool(); + this->state = value != 0; break; case 4: - this->has_brightness = value.as_bool(); + this->has_brightness = value != 0; break; case 22: - this->has_color_mode = value.as_bool(); + this->has_color_mode = value != 0; break; case 23: - this->color_mode = static_cast(value.as_uint32()); + this->color_mode = static_cast(value); break; case 20: - this->has_color_brightness = value.as_bool(); + this->has_color_brightness = value != 0; break; case 6: - this->has_rgb = value.as_bool(); + this->has_rgb = value != 0; break; case 10: - this->has_white = value.as_bool(); + this->has_white = value != 0; break; case 12: - this->has_color_temperature = value.as_bool(); + this->has_color_temperature = value != 0; break; case 24: - this->has_cold_white = value.as_bool(); + this->has_cold_white = value != 0; break; case 26: - this->has_warm_white = value.as_bool(); + this->has_warm_white = value != 0; break; case 14: - this->has_transition_length = value.as_bool(); + this->has_transition_length = value != 0; break; case 15: - this->transition_length = value.as_uint32(); + this->transition_length = value; break; case 16: - this->has_flash_length = value.as_bool(); + this->has_flash_length = value != 0; break; case 17: - this->flash_length = value.as_uint32(); + this->flash_length = value; break; case 18: - this->has_effect = value.as_bool(); + this->has_effect = value != 0; break; #ifdef USE_DEVICES case 28: - this->device_id = value.as_uint32(); + this->device_id = value; break; #endif default: @@ -635,9 +681,9 @@ bool LightCommandRequest::decode_32bit(uint32_t field_id, Proto32Bit value) { } #endif #ifdef USE_SENSOR -void ListEntitiesSensorResponse::encode(ProtoWriteBuffer buffer) const { +void ListEntitiesSensorResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_string(1, this->object_id); - buffer.encode_fixed32(2, this->key); + buffer.write_tag_and_fixed32(21, this->key); buffer.encode_string(3, this->name); #ifdef USE_ENTITY_ICON buffer.encode_string(5, this->icon); @@ -653,45 +699,49 @@ void ListEntitiesSensorResponse::encode(ProtoWriteBuffer buffer) const { buffer.encode_uint32(14, this->device_id); #endif } -void ListEntitiesSensorResponse::calculate_size(ProtoSize &size) const { - size.add_length(1, this->object_id.size()); - size.add_fixed32(1, this->key); - size.add_length(1, this->name.size()); +uint32_t ListEntitiesSensorResponse::calculate_size() const { + uint32_t size = 0; + size += ProtoSize::calc_length(1, this->object_id.size()); + size += 5; + size += ProtoSize::calc_length(1, this->name.size()); #ifdef USE_ENTITY_ICON - size.add_length(1, this->icon.size()); + size += ProtoSize::calc_length(1, this->icon.size()); #endif - size.add_length(1, this->unit_of_measurement.size()); - size.add_int32(1, this->accuracy_decimals); - size.add_bool(1, this->force_update); - size.add_length(1, this->device_class.size()); - size.add_uint32(1, static_cast(this->state_class)); - size.add_bool(1, this->disabled_by_default); - size.add_uint32(1, static_cast(this->entity_category)); + size += ProtoSize::calc_length(1, this->unit_of_measurement.size()); + size += ProtoSize::calc_int32(1, this->accuracy_decimals); + size += ProtoSize::calc_bool(1, this->force_update); + size += ProtoSize::calc_length(1, this->device_class.size()); + size += ProtoSize::calc_uint32(1, static_cast(this->state_class)); + size += ProtoSize::calc_bool(1, this->disabled_by_default); + size += ProtoSize::calc_uint32(1, static_cast(this->entity_category)); #ifdef USE_DEVICES - size.add_uint32(1, this->device_id); + size += ProtoSize::calc_uint32(1, this->device_id); #endif + return size; } -void SensorStateResponse::encode(ProtoWriteBuffer buffer) const { - buffer.encode_fixed32(1, this->key); +void SensorStateResponse::encode(ProtoWriteBuffer &buffer) const { + buffer.write_tag_and_fixed32(13, this->key); buffer.encode_float(2, this->state); buffer.encode_bool(3, this->missing_state); #ifdef USE_DEVICES buffer.encode_uint32(4, this->device_id); #endif } -void SensorStateResponse::calculate_size(ProtoSize &size) const { - size.add_fixed32(1, this->key); - size.add_float(1, this->state); - size.add_bool(1, this->missing_state); +uint32_t SensorStateResponse::calculate_size() const { + uint32_t size = 0; + size += 5; + size += ProtoSize::calc_float(1, this->state); + size += ProtoSize::calc_bool(1, this->missing_state); #ifdef USE_DEVICES - size.add_uint32(1, this->device_id); + size += ProtoSize::calc_uint32(1, this->device_id); #endif + return size; } #endif #ifdef USE_SWITCH -void ListEntitiesSwitchResponse::encode(ProtoWriteBuffer buffer) const { +void ListEntitiesSwitchResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_string(1, this->object_id); - buffer.encode_fixed32(2, this->key); + buffer.write_tag_and_fixed32(21, this->key); buffer.encode_string(3, this->name); #ifdef USE_ENTITY_ICON buffer.encode_string(5, this->icon); @@ -704,43 +754,47 @@ void ListEntitiesSwitchResponse::encode(ProtoWriteBuffer buffer) const { buffer.encode_uint32(10, this->device_id); #endif } -void ListEntitiesSwitchResponse::calculate_size(ProtoSize &size) const { - size.add_length(1, this->object_id.size()); - size.add_fixed32(1, this->key); - size.add_length(1, this->name.size()); +uint32_t ListEntitiesSwitchResponse::calculate_size() const { + uint32_t size = 0; + size += ProtoSize::calc_length(1, this->object_id.size()); + size += 5; + size += ProtoSize::calc_length(1, this->name.size()); #ifdef USE_ENTITY_ICON - size.add_length(1, this->icon.size()); + size += ProtoSize::calc_length(1, this->icon.size()); #endif - size.add_bool(1, this->assumed_state); - size.add_bool(1, this->disabled_by_default); - size.add_uint32(1, static_cast(this->entity_category)); - size.add_length(1, this->device_class.size()); + size += ProtoSize::calc_bool(1, this->assumed_state); + size += ProtoSize::calc_bool(1, this->disabled_by_default); + size += ProtoSize::calc_uint32(1, static_cast(this->entity_category)); + size += ProtoSize::calc_length(1, this->device_class.size()); #ifdef USE_DEVICES - size.add_uint32(1, this->device_id); + size += ProtoSize::calc_uint32(1, this->device_id); #endif + return size; } -void SwitchStateResponse::encode(ProtoWriteBuffer buffer) const { - buffer.encode_fixed32(1, this->key); +void SwitchStateResponse::encode(ProtoWriteBuffer &buffer) const { + buffer.write_tag_and_fixed32(13, this->key); buffer.encode_bool(2, this->state); #ifdef USE_DEVICES buffer.encode_uint32(3, this->device_id); #endif } -void SwitchStateResponse::calculate_size(ProtoSize &size) const { - size.add_fixed32(1, this->key); - size.add_bool(1, this->state); +uint32_t SwitchStateResponse::calculate_size() const { + uint32_t size = 0; + size += 5; + size += ProtoSize::calc_bool(1, this->state); #ifdef USE_DEVICES - size.add_uint32(1, this->device_id); + size += ProtoSize::calc_uint32(1, this->device_id); #endif + return size; } -bool SwitchCommandRequest::decode_varint(uint32_t field_id, ProtoVarInt value) { +bool SwitchCommandRequest::decode_varint(uint32_t field_id, proto_varint_value_t value) { switch (field_id) { case 2: - this->state = value.as_bool(); + this->state = value != 0; break; #ifdef USE_DEVICES case 3: - this->device_id = value.as_uint32(); + this->device_id = value; break; #endif default: @@ -760,9 +814,9 @@ bool SwitchCommandRequest::decode_32bit(uint32_t field_id, Proto32Bit value) { } #endif #ifdef USE_TEXT_SENSOR -void ListEntitiesTextSensorResponse::encode(ProtoWriteBuffer buffer) const { +void ListEntitiesTextSensorResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_string(1, this->object_id); - buffer.encode_fixed32(2, this->key); + buffer.write_tag_and_fixed32(21, this->key); buffer.encode_string(3, this->name); #ifdef USE_ENTITY_ICON buffer.encode_string(5, this->icon); @@ -774,57 +828,63 @@ void ListEntitiesTextSensorResponse::encode(ProtoWriteBuffer buffer) const { buffer.encode_uint32(9, this->device_id); #endif } -void ListEntitiesTextSensorResponse::calculate_size(ProtoSize &size) const { - size.add_length(1, this->object_id.size()); - size.add_fixed32(1, this->key); - size.add_length(1, this->name.size()); +uint32_t ListEntitiesTextSensorResponse::calculate_size() const { + uint32_t size = 0; + size += ProtoSize::calc_length(1, this->object_id.size()); + size += 5; + size += ProtoSize::calc_length(1, this->name.size()); #ifdef USE_ENTITY_ICON - size.add_length(1, this->icon.size()); + size += ProtoSize::calc_length(1, this->icon.size()); #endif - size.add_bool(1, this->disabled_by_default); - size.add_uint32(1, static_cast(this->entity_category)); - size.add_length(1, this->device_class.size()); + size += ProtoSize::calc_bool(1, this->disabled_by_default); + size += ProtoSize::calc_uint32(1, static_cast(this->entity_category)); + size += ProtoSize::calc_length(1, this->device_class.size()); #ifdef USE_DEVICES - size.add_uint32(1, this->device_id); + size += ProtoSize::calc_uint32(1, this->device_id); #endif + return size; } -void TextSensorStateResponse::encode(ProtoWriteBuffer buffer) const { - buffer.encode_fixed32(1, this->key); +void TextSensorStateResponse::encode(ProtoWriteBuffer &buffer) const { + buffer.write_tag_and_fixed32(13, this->key); buffer.encode_string(2, this->state); buffer.encode_bool(3, this->missing_state); #ifdef USE_DEVICES buffer.encode_uint32(4, this->device_id); #endif } -void TextSensorStateResponse::calculate_size(ProtoSize &size) const { - size.add_fixed32(1, this->key); - size.add_length(1, this->state.size()); - size.add_bool(1, this->missing_state); +uint32_t TextSensorStateResponse::calculate_size() const { + uint32_t size = 0; + size += 5; + size += ProtoSize::calc_length(1, this->state.size()); + size += ProtoSize::calc_bool(1, this->missing_state); #ifdef USE_DEVICES - size.add_uint32(1, this->device_id); + size += ProtoSize::calc_uint32(1, this->device_id); #endif + return size; } #endif -bool SubscribeLogsRequest::decode_varint(uint32_t field_id, ProtoVarInt value) { +bool SubscribeLogsRequest::decode_varint(uint32_t field_id, proto_varint_value_t value) { switch (field_id) { case 1: - this->level = static_cast(value.as_uint32()); + this->level = static_cast(value); break; case 2: - this->dump_config = value.as_bool(); + this->dump_config = value != 0; break; default: return false; } return true; } -void SubscribeLogsResponse::encode(ProtoWriteBuffer buffer) const { +void SubscribeLogsResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_uint32(1, static_cast(this->level)); buffer.encode_bytes(3, this->message_ptr_, this->message_len_); } -void SubscribeLogsResponse::calculate_size(ProtoSize &size) const { - size.add_uint32(1, static_cast(this->level)); - size.add_length(1, this->message_len_); +uint32_t SubscribeLogsResponse::calculate_size() const { + uint32_t size = 0; + size += ProtoSize::calc_uint32(1, static_cast(this->level)); + size += ProtoSize::calc_length(1, this->message_len_); + return size; } #ifdef USE_API_NOISE bool NoiseEncryptionSetKeyRequest::decode_length(uint32_t field_id, ProtoLengthDelimited value) { @@ -839,28 +899,34 @@ bool NoiseEncryptionSetKeyRequest::decode_length(uint32_t field_id, ProtoLengthD } return true; } -void NoiseEncryptionSetKeyResponse::encode(ProtoWriteBuffer buffer) const { buffer.encode_bool(1, this->success); } -void NoiseEncryptionSetKeyResponse::calculate_size(ProtoSize &size) const { size.add_bool(1, this->success); } +void NoiseEncryptionSetKeyResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_bool(1, this->success); } +uint32_t NoiseEncryptionSetKeyResponse::calculate_size() const { + uint32_t size = 0; + size += ProtoSize::calc_bool(1, this->success); + return size; +} #endif #ifdef USE_API_HOMEASSISTANT_SERVICES -void HomeassistantServiceMap::encode(ProtoWriteBuffer buffer) const { +void HomeassistantServiceMap::encode(ProtoWriteBuffer &buffer) const { buffer.encode_string(1, this->key); buffer.encode_string(2, this->value); } -void HomeassistantServiceMap::calculate_size(ProtoSize &size) const { - size.add_length(1, this->key.size()); - size.add_length(1, this->value.size()); +uint32_t HomeassistantServiceMap::calculate_size() const { + uint32_t size = 0; + size += ProtoSize::calc_length(1, this->key.size()); + size += ProtoSize::calc_length(1, this->value.size()); + return size; } -void HomeassistantActionRequest::encode(ProtoWriteBuffer buffer) const { +void HomeassistantActionRequest::encode(ProtoWriteBuffer &buffer) const { buffer.encode_string(1, this->service); for (auto &it : this->data) { - buffer.encode_message(2, it); + buffer.encode_sub_message(2, it); } for (auto &it : this->data_template) { - buffer.encode_message(3, it); + buffer.encode_sub_message(3, it); } for (auto &it : this->variables) { - buffer.encode_message(4, it); + buffer.encode_sub_message(4, it); } buffer.encode_bool(5, this->is_event); #ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES @@ -873,31 +939,45 @@ void HomeassistantActionRequest::encode(ProtoWriteBuffer buffer) const { buffer.encode_string(8, this->response_template); #endif } -void HomeassistantActionRequest::calculate_size(ProtoSize &size) const { - size.add_length(1, this->service.size()); - size.add_repeated_message(1, this->data); - size.add_repeated_message(1, this->data_template); - size.add_repeated_message(1, this->variables); - size.add_bool(1, this->is_event); +uint32_t HomeassistantActionRequest::calculate_size() const { + uint32_t size = 0; + size += ProtoSize::calc_length(1, this->service.size()); + if (!this->data.empty()) { + for (const auto &it : this->data) { + size += ProtoSize::calc_message_force(1, it.calculate_size()); + } + } + if (!this->data_template.empty()) { + for (const auto &it : this->data_template) { + size += ProtoSize::calc_message_force(1, it.calculate_size()); + } + } + if (!this->variables.empty()) { + for (const auto &it : this->variables) { + size += ProtoSize::calc_message_force(1, it.calculate_size()); + } + } + size += ProtoSize::calc_bool(1, this->is_event); #ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES - size.add_uint32(1, this->call_id); + size += ProtoSize::calc_uint32(1, this->call_id); #endif #ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES_JSON - size.add_bool(1, this->wants_response); + size += ProtoSize::calc_bool(1, this->wants_response); #endif #ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES_JSON - size.add_length(1, this->response_template.size()); + size += ProtoSize::calc_length(1, this->response_template.size()); #endif + return size; } #endif #ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES -bool HomeassistantActionResponse::decode_varint(uint32_t field_id, ProtoVarInt value) { +bool HomeassistantActionResponse::decode_varint(uint32_t field_id, proto_varint_value_t value) { switch (field_id) { case 1: - this->call_id = value.as_uint32(); + this->call_id = value; break; case 2: - this->success = value.as_bool(); + this->success = value != 0; break; default: return false; @@ -924,15 +1004,17 @@ bool HomeassistantActionResponse::decode_length(uint32_t field_id, ProtoLengthDe } #endif #ifdef USE_API_HOMEASSISTANT_STATES -void SubscribeHomeAssistantStateResponse::encode(ProtoWriteBuffer buffer) const { +void SubscribeHomeAssistantStateResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_string(1, this->entity_id); buffer.encode_string(2, this->attribute); buffer.encode_bool(3, this->once); } -void SubscribeHomeAssistantStateResponse::calculate_size(ProtoSize &size) const { - size.add_length(1, this->entity_id.size()); - size.add_length(1, this->attribute.size()); - size.add_bool(1, this->once); +uint32_t SubscribeHomeAssistantStateResponse::calculate_size() const { + uint32_t size = 0; + size += ProtoSize::calc_length(1, this->entity_id.size()); + size += ProtoSize::calc_length(1, this->attribute.size()); + size += ProtoSize::calc_bool(1, this->once); + return size; } bool HomeAssistantStateResponse::decode_length(uint32_t field_id, ProtoLengthDelimited value) { switch (field_id) { @@ -954,12 +1036,66 @@ bool HomeAssistantStateResponse::decode_length(uint32_t field_id, ProtoLengthDel return true; } #endif +bool DSTRule::decode_varint(uint32_t field_id, proto_varint_value_t value) { + switch (field_id) { + case 1: + this->time_seconds = decode_zigzag32(static_cast(value)); + break; + case 2: + this->day = value; + break; + case 3: + this->type = static_cast(value); + break; + case 4: + this->month = value; + break; + case 5: + this->week = value; + break; + case 6: + this->day_of_week = value; + break; + default: + return false; + } + return true; +} +bool ParsedTimezone::decode_varint(uint32_t field_id, proto_varint_value_t value) { + switch (field_id) { + case 1: + this->std_offset_seconds = decode_zigzag32(static_cast(value)); + break; + case 2: + this->dst_offset_seconds = decode_zigzag32(static_cast(value)); + break; + default: + return false; + } + return true; +} +bool ParsedTimezone::decode_length(uint32_t field_id, ProtoLengthDelimited value) { + switch (field_id) { + case 3: + value.decode_to_message(this->dst_start); + break; + case 4: + value.decode_to_message(this->dst_end); + break; + default: + return false; + } + return true; +} bool GetTimeResponse::decode_length(uint32_t field_id, ProtoLengthDelimited value) { switch (field_id) { case 2: { this->timezone = StringRef(reinterpret_cast(value.data()), value.size()); break; } + case 3: + value.decode_to_message(this->parsed_timezone); + break; default: return false; } @@ -976,44 +1112,52 @@ bool GetTimeResponse::decode_32bit(uint32_t field_id, Proto32Bit value) { return true; } #ifdef USE_API_USER_DEFINED_ACTIONS -void ListEntitiesServicesArgument::encode(ProtoWriteBuffer buffer) const { +void ListEntitiesServicesArgument::encode(ProtoWriteBuffer &buffer) const { buffer.encode_string(1, this->name); buffer.encode_uint32(2, static_cast(this->type)); } -void ListEntitiesServicesArgument::calculate_size(ProtoSize &size) const { - size.add_length(1, this->name.size()); - size.add_uint32(1, static_cast(this->type)); +uint32_t ListEntitiesServicesArgument::calculate_size() const { + uint32_t size = 0; + size += ProtoSize::calc_length(1, this->name.size()); + size += ProtoSize::calc_uint32(1, static_cast(this->type)); + return size; } -void ListEntitiesServicesResponse::encode(ProtoWriteBuffer buffer) const { +void ListEntitiesServicesResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_string(1, this->name); - buffer.encode_fixed32(2, this->key); + buffer.write_tag_and_fixed32(21, this->key); for (auto &it : this->args) { - buffer.encode_message(3, it); + buffer.encode_sub_message(3, it); } buffer.encode_uint32(4, static_cast(this->supports_response)); } -void ListEntitiesServicesResponse::calculate_size(ProtoSize &size) const { - size.add_length(1, this->name.size()); - size.add_fixed32(1, this->key); - size.add_repeated_message(1, this->args); - size.add_uint32(1, static_cast(this->supports_response)); +uint32_t ListEntitiesServicesResponse::calculate_size() const { + uint32_t size = 0; + size += ProtoSize::calc_length(1, this->name.size()); + size += 5; + if (!this->args.empty()) { + for (const auto &it : this->args) { + size += ProtoSize::calc_message_force(1, it.calculate_size()); + } + } + size += ProtoSize::calc_uint32(1, static_cast(this->supports_response)); + return size; } -bool ExecuteServiceArgument::decode_varint(uint32_t field_id, ProtoVarInt value) { +bool ExecuteServiceArgument::decode_varint(uint32_t field_id, proto_varint_value_t value) { switch (field_id) { case 1: - this->bool_ = value.as_bool(); + this->bool_ = value != 0; break; case 2: - this->legacy_int = value.as_int32(); + this->legacy_int = static_cast(value); break; case 5: - this->int_ = value.as_sint32(); + this->int_ = decode_zigzag32(static_cast(value)); break; case 6: - this->bool_array.push_back(value.as_bool()); + this->bool_array.push_back(value != 0); break; case 7: - this->int_array.push_back(value.as_sint32()); + this->int_array.push_back(decode_zigzag32(static_cast(value))); break; default: return false; @@ -1058,16 +1202,16 @@ void ExecuteServiceArgument::decode(const uint8_t *buffer, size_t length) { this->string_array.init(count_string_array); ProtoDecodableMessage::decode(buffer, length); } -bool ExecuteServiceRequest::decode_varint(uint32_t field_id, ProtoVarInt value) { +bool ExecuteServiceRequest::decode_varint(uint32_t field_id, proto_varint_value_t value) { switch (field_id) { #ifdef USE_API_USER_DEFINED_ACTION_RESPONSES case 3: - this->call_id = value.as_uint32(); + this->call_id = value; break; #endif #ifdef USE_API_USER_DEFINED_ACTION_RESPONSES case 4: - this->return_response = value.as_bool(); + this->return_response = value != 0; break; #endif default: @@ -1103,7 +1247,7 @@ void ExecuteServiceRequest::decode(const uint8_t *buffer, size_t length) { } #endif #ifdef USE_API_USER_DEFINED_ACTION_RESPONSES -void ExecuteServiceResponse::encode(ProtoWriteBuffer buffer) const { +void ExecuteServiceResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_uint32(1, this->call_id); buffer.encode_bool(2, this->success); buffer.encode_string(3, this->error_message); @@ -1111,19 +1255,21 @@ void ExecuteServiceResponse::encode(ProtoWriteBuffer buffer) const { buffer.encode_bytes(4, this->response_data, this->response_data_len); #endif } -void ExecuteServiceResponse::calculate_size(ProtoSize &size) const { - size.add_uint32(1, this->call_id); - size.add_bool(1, this->success); - size.add_length(1, this->error_message.size()); +uint32_t ExecuteServiceResponse::calculate_size() const { + uint32_t size = 0; + size += ProtoSize::calc_uint32(1, this->call_id); + size += ProtoSize::calc_bool(1, this->success); + size += ProtoSize::calc_length(1, this->error_message.size()); #ifdef USE_API_USER_DEFINED_ACTION_RESPONSES_JSON - size.add_length(1, this->response_data_len); + size += ProtoSize::calc_length(1, this->response_data_len); #endif + return size; } #endif #ifdef USE_CAMERA -void ListEntitiesCameraResponse::encode(ProtoWriteBuffer buffer) const { +void ListEntitiesCameraResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_string(1, this->object_id); - buffer.encode_fixed32(2, this->key); + buffer.write_tag_and_fixed32(21, this->key); buffer.encode_string(3, this->name); buffer.encode_bool(5, this->disabled_by_default); #ifdef USE_ENTITY_ICON @@ -1134,42 +1280,46 @@ void ListEntitiesCameraResponse::encode(ProtoWriteBuffer buffer) const { buffer.encode_uint32(8, this->device_id); #endif } -void ListEntitiesCameraResponse::calculate_size(ProtoSize &size) const { - size.add_length(1, this->object_id.size()); - size.add_fixed32(1, this->key); - size.add_length(1, this->name.size()); - size.add_bool(1, this->disabled_by_default); +uint32_t ListEntitiesCameraResponse::calculate_size() const { + uint32_t size = 0; + size += ProtoSize::calc_length(1, this->object_id.size()); + size += 5; + size += ProtoSize::calc_length(1, this->name.size()); + size += ProtoSize::calc_bool(1, this->disabled_by_default); #ifdef USE_ENTITY_ICON - size.add_length(1, this->icon.size()); + size += ProtoSize::calc_length(1, this->icon.size()); #endif - size.add_uint32(1, static_cast(this->entity_category)); + size += ProtoSize::calc_uint32(1, static_cast(this->entity_category)); #ifdef USE_DEVICES - size.add_uint32(1, this->device_id); + size += ProtoSize::calc_uint32(1, this->device_id); #endif + return size; } -void CameraImageResponse::encode(ProtoWriteBuffer buffer) const { - buffer.encode_fixed32(1, this->key); +void CameraImageResponse::encode(ProtoWriteBuffer &buffer) const { + buffer.write_tag_and_fixed32(13, this->key); buffer.encode_bytes(2, this->data_ptr_, this->data_len_); buffer.encode_bool(3, this->done); #ifdef USE_DEVICES buffer.encode_uint32(4, this->device_id); #endif } -void CameraImageResponse::calculate_size(ProtoSize &size) const { - size.add_fixed32(1, this->key); - size.add_length(1, this->data_len_); - size.add_bool(1, this->done); +uint32_t CameraImageResponse::calculate_size() const { + uint32_t size = 0; + size += 5; + size += ProtoSize::calc_length(1, this->data_len_); + size += ProtoSize::calc_bool(1, this->done); #ifdef USE_DEVICES - size.add_uint32(1, this->device_id); + size += ProtoSize::calc_uint32(1, this->device_id); #endif + return size; } -bool CameraImageRequest::decode_varint(uint32_t field_id, ProtoVarInt value) { +bool CameraImageRequest::decode_varint(uint32_t field_id, proto_varint_value_t value) { switch (field_id) { case 1: - this->single = value.as_bool(); + this->single = value != 0; break; case 2: - this->stream = value.as_bool(); + this->stream = value != 0; break; default: return false; @@ -1178,9 +1328,9 @@ bool CameraImageRequest::decode_varint(uint32_t field_id, ProtoVarInt value) { } #endif #ifdef USE_CLIMATE -void ListEntitiesClimateResponse::encode(ProtoWriteBuffer buffer) const { +void ListEntitiesClimateResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_string(1, this->object_id); - buffer.encode_fixed32(2, this->key); + buffer.write_tag_and_fixed32(21, this->key); buffer.encode_string(3, this->name); buffer.encode_bool(5, this->supports_current_temperature); buffer.encode_bool(6, this->supports_two_point_target_temperature); @@ -1221,63 +1371,65 @@ void ListEntitiesClimateResponse::encode(ProtoWriteBuffer buffer) const { #endif buffer.encode_uint32(27, this->feature_flags); } -void ListEntitiesClimateResponse::calculate_size(ProtoSize &size) const { - size.add_length(1, this->object_id.size()); - size.add_fixed32(1, this->key); - size.add_length(1, this->name.size()); - size.add_bool(1, this->supports_current_temperature); - size.add_bool(1, this->supports_two_point_target_temperature); +uint32_t ListEntitiesClimateResponse::calculate_size() const { + uint32_t size = 0; + size += ProtoSize::calc_length(1, this->object_id.size()); + size += 5; + size += ProtoSize::calc_length(1, this->name.size()); + size += ProtoSize::calc_bool(1, this->supports_current_temperature); + size += ProtoSize::calc_bool(1, this->supports_two_point_target_temperature); if (!this->supported_modes->empty()) { for (const auto &it : *this->supported_modes) { - size.add_uint32_force(1, static_cast(it)); + size += ProtoSize::calc_uint32_force(1, static_cast(it)); } } - size.add_float(1, this->visual_min_temperature); - size.add_float(1, this->visual_max_temperature); - size.add_float(1, this->visual_target_temperature_step); - size.add_bool(1, this->supports_action); + size += ProtoSize::calc_float(1, this->visual_min_temperature); + size += ProtoSize::calc_float(1, this->visual_max_temperature); + size += ProtoSize::calc_float(1, this->visual_target_temperature_step); + size += ProtoSize::calc_bool(1, this->supports_action); if (!this->supported_fan_modes->empty()) { for (const auto &it : *this->supported_fan_modes) { - size.add_uint32_force(1, static_cast(it)); + size += ProtoSize::calc_uint32_force(1, static_cast(it)); } } if (!this->supported_swing_modes->empty()) { for (const auto &it : *this->supported_swing_modes) { - size.add_uint32_force(1, static_cast(it)); + size += ProtoSize::calc_uint32_force(1, static_cast(it)); } } if (!this->supported_custom_fan_modes->empty()) { for (const char *it : *this->supported_custom_fan_modes) { - size.add_length_force(1, strlen(it)); + size += ProtoSize::calc_length_force(1, strlen(it)); } } if (!this->supported_presets->empty()) { for (const auto &it : *this->supported_presets) { - size.add_uint32_force(2, static_cast(it)); + size += ProtoSize::calc_uint32_force(2, static_cast(it)); } } if (!this->supported_custom_presets->empty()) { for (const char *it : *this->supported_custom_presets) { - size.add_length_force(2, strlen(it)); + size += ProtoSize::calc_length_force(2, strlen(it)); } } - size.add_bool(2, this->disabled_by_default); + size += ProtoSize::calc_bool(2, this->disabled_by_default); #ifdef USE_ENTITY_ICON - size.add_length(2, this->icon.size()); + size += ProtoSize::calc_length(2, this->icon.size()); #endif - size.add_uint32(2, static_cast(this->entity_category)); - size.add_float(2, this->visual_current_temperature_step); - size.add_bool(2, this->supports_current_humidity); - size.add_bool(2, this->supports_target_humidity); - size.add_float(2, this->visual_min_humidity); - size.add_float(2, this->visual_max_humidity); + size += ProtoSize::calc_uint32(2, static_cast(this->entity_category)); + size += ProtoSize::calc_float(2, this->visual_current_temperature_step); + size += ProtoSize::calc_bool(2, this->supports_current_humidity); + size += ProtoSize::calc_bool(2, this->supports_target_humidity); + size += ProtoSize::calc_float(2, this->visual_min_humidity); + size += ProtoSize::calc_float(2, this->visual_max_humidity); #ifdef USE_DEVICES - size.add_uint32(2, this->device_id); + size += ProtoSize::calc_uint32(2, this->device_id); #endif - size.add_uint32(2, this->feature_flags); + size += ProtoSize::calc_uint32(2, this->feature_flags); + return size; } -void ClimateStateResponse::encode(ProtoWriteBuffer buffer) const { - buffer.encode_fixed32(1, this->key); +void ClimateStateResponse::encode(ProtoWriteBuffer &buffer) const { + buffer.write_tag_and_fixed32(13, this->key); buffer.encode_uint32(2, static_cast(this->mode)); buffer.encode_float(3, this->current_temperature); buffer.encode_float(4, this->target_temperature); @@ -1295,72 +1447,74 @@ void ClimateStateResponse::encode(ProtoWriteBuffer buffer) const { buffer.encode_uint32(16, this->device_id); #endif } -void ClimateStateResponse::calculate_size(ProtoSize &size) const { - size.add_fixed32(1, this->key); - size.add_uint32(1, static_cast(this->mode)); - size.add_float(1, this->current_temperature); - size.add_float(1, this->target_temperature); - size.add_float(1, this->target_temperature_low); - size.add_float(1, this->target_temperature_high); - size.add_uint32(1, static_cast(this->action)); - size.add_uint32(1, static_cast(this->fan_mode)); - size.add_uint32(1, static_cast(this->swing_mode)); - size.add_length(1, this->custom_fan_mode.size()); - size.add_uint32(1, static_cast(this->preset)); - size.add_length(1, this->custom_preset.size()); - size.add_float(1, this->current_humidity); - size.add_float(1, this->target_humidity); +uint32_t ClimateStateResponse::calculate_size() const { + uint32_t size = 0; + size += 5; + size += ProtoSize::calc_uint32(1, static_cast(this->mode)); + size += ProtoSize::calc_float(1, this->current_temperature); + size += ProtoSize::calc_float(1, this->target_temperature); + size += ProtoSize::calc_float(1, this->target_temperature_low); + size += ProtoSize::calc_float(1, this->target_temperature_high); + size += ProtoSize::calc_uint32(1, static_cast(this->action)); + size += ProtoSize::calc_uint32(1, static_cast(this->fan_mode)); + size += ProtoSize::calc_uint32(1, static_cast(this->swing_mode)); + size += ProtoSize::calc_length(1, this->custom_fan_mode.size()); + size += ProtoSize::calc_uint32(1, static_cast(this->preset)); + size += ProtoSize::calc_length(1, this->custom_preset.size()); + size += ProtoSize::calc_float(1, this->current_humidity); + size += ProtoSize::calc_float(1, this->target_humidity); #ifdef USE_DEVICES - size.add_uint32(2, this->device_id); + size += ProtoSize::calc_uint32(2, this->device_id); #endif + return size; } -bool ClimateCommandRequest::decode_varint(uint32_t field_id, ProtoVarInt value) { +bool ClimateCommandRequest::decode_varint(uint32_t field_id, proto_varint_value_t value) { switch (field_id) { case 2: - this->has_mode = value.as_bool(); + this->has_mode = value != 0; break; case 3: - this->mode = static_cast(value.as_uint32()); + this->mode = static_cast(value); break; case 4: - this->has_target_temperature = value.as_bool(); + this->has_target_temperature = value != 0; break; case 6: - this->has_target_temperature_low = value.as_bool(); + this->has_target_temperature_low = value != 0; break; case 8: - this->has_target_temperature_high = value.as_bool(); + this->has_target_temperature_high = value != 0; break; case 12: - this->has_fan_mode = value.as_bool(); + this->has_fan_mode = value != 0; break; case 13: - this->fan_mode = static_cast(value.as_uint32()); + this->fan_mode = static_cast(value); break; case 14: - this->has_swing_mode = value.as_bool(); + this->has_swing_mode = value != 0; break; case 15: - this->swing_mode = static_cast(value.as_uint32()); + this->swing_mode = static_cast(value); break; case 16: - this->has_custom_fan_mode = value.as_bool(); + this->has_custom_fan_mode = value != 0; break; case 18: - this->has_preset = value.as_bool(); + this->has_preset = value != 0; break; case 19: - this->preset = static_cast(value.as_uint32()); + this->preset = static_cast(value); break; case 20: - this->has_custom_preset = value.as_bool(); + this->has_custom_preset = value != 0; break; case 22: - this->has_target_humidity = value.as_bool(); + this->has_target_humidity = value != 0; break; #ifdef USE_DEVICES case 24: - this->device_id = value.as_uint32(); + this->device_id = value; break; #endif default: @@ -1407,9 +1561,9 @@ bool ClimateCommandRequest::decode_32bit(uint32_t field_id, Proto32Bit value) { } #endif #ifdef USE_WATER_HEATER -void ListEntitiesWaterHeaterResponse::encode(ProtoWriteBuffer buffer) const { +void ListEntitiesWaterHeaterResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_string(1, this->object_id); - buffer.encode_fixed32(2, this->key); + buffer.write_tag_and_fixed32(21, this->key); buffer.encode_string(3, this->name); #ifdef USE_ENTITY_ICON buffer.encode_string(4, this->icon); @@ -1427,30 +1581,32 @@ void ListEntitiesWaterHeaterResponse::encode(ProtoWriteBuffer buffer) const { } buffer.encode_uint32(12, this->supported_features); } -void ListEntitiesWaterHeaterResponse::calculate_size(ProtoSize &size) const { - size.add_length(1, this->object_id.size()); - size.add_fixed32(1, this->key); - size.add_length(1, this->name.size()); +uint32_t ListEntitiesWaterHeaterResponse::calculate_size() const { + uint32_t size = 0; + size += ProtoSize::calc_length(1, this->object_id.size()); + size += 5; + size += ProtoSize::calc_length(1, this->name.size()); #ifdef USE_ENTITY_ICON - size.add_length(1, this->icon.size()); + size += ProtoSize::calc_length(1, this->icon.size()); #endif - size.add_bool(1, this->disabled_by_default); - size.add_uint32(1, static_cast(this->entity_category)); + size += ProtoSize::calc_bool(1, this->disabled_by_default); + size += ProtoSize::calc_uint32(1, static_cast(this->entity_category)); #ifdef USE_DEVICES - size.add_uint32(1, this->device_id); + size += ProtoSize::calc_uint32(1, this->device_id); #endif - size.add_float(1, this->min_temperature); - size.add_float(1, this->max_temperature); - size.add_float(1, this->target_temperature_step); + size += ProtoSize::calc_float(1, this->min_temperature); + size += ProtoSize::calc_float(1, this->max_temperature); + size += ProtoSize::calc_float(1, this->target_temperature_step); if (!this->supported_modes->empty()) { for (const auto &it : *this->supported_modes) { - size.add_uint32_force(1, static_cast(it)); + size += ProtoSize::calc_uint32_force(1, static_cast(it)); } } - size.add_uint32(1, this->supported_features); + size += ProtoSize::calc_uint32(1, this->supported_features); + return size; } -void WaterHeaterStateResponse::encode(ProtoWriteBuffer buffer) const { - buffer.encode_fixed32(1, this->key); +void WaterHeaterStateResponse::encode(ProtoWriteBuffer &buffer) const { + buffer.write_tag_and_fixed32(13, this->key); buffer.encode_float(2, this->current_temperature); buffer.encode_float(3, this->target_temperature); buffer.encode_uint32(4, static_cast(this->mode)); @@ -1461,33 +1617,35 @@ void WaterHeaterStateResponse::encode(ProtoWriteBuffer buffer) const { buffer.encode_float(7, this->target_temperature_low); buffer.encode_float(8, this->target_temperature_high); } -void WaterHeaterStateResponse::calculate_size(ProtoSize &size) const { - size.add_fixed32(1, this->key); - size.add_float(1, this->current_temperature); - size.add_float(1, this->target_temperature); - size.add_uint32(1, static_cast(this->mode)); +uint32_t WaterHeaterStateResponse::calculate_size() const { + uint32_t size = 0; + size += 5; + size += ProtoSize::calc_float(1, this->current_temperature); + size += ProtoSize::calc_float(1, this->target_temperature); + size += ProtoSize::calc_uint32(1, static_cast(this->mode)); #ifdef USE_DEVICES - size.add_uint32(1, this->device_id); + size += ProtoSize::calc_uint32(1, this->device_id); #endif - size.add_uint32(1, this->state); - size.add_float(1, this->target_temperature_low); - size.add_float(1, this->target_temperature_high); + size += ProtoSize::calc_uint32(1, this->state); + size += ProtoSize::calc_float(1, this->target_temperature_low); + size += ProtoSize::calc_float(1, this->target_temperature_high); + return size; } -bool WaterHeaterCommandRequest::decode_varint(uint32_t field_id, ProtoVarInt value) { +bool WaterHeaterCommandRequest::decode_varint(uint32_t field_id, proto_varint_value_t value) { switch (field_id) { case 2: - this->has_fields = value.as_uint32(); + this->has_fields = value; break; case 3: - this->mode = static_cast(value.as_uint32()); + this->mode = static_cast(value); break; #ifdef USE_DEVICES case 5: - this->device_id = value.as_uint32(); + this->device_id = value; break; #endif case 6: - this->state = value.as_uint32(); + this->state = value; break; default: return false; @@ -1515,9 +1673,9 @@ bool WaterHeaterCommandRequest::decode_32bit(uint32_t field_id, Proto32Bit value } #endif #ifdef USE_NUMBER -void ListEntitiesNumberResponse::encode(ProtoWriteBuffer buffer) const { +void ListEntitiesNumberResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_string(1, this->object_id); - buffer.encode_fixed32(2, this->key); + buffer.write_tag_and_fixed32(21, this->key); buffer.encode_string(3, this->name); #ifdef USE_ENTITY_ICON buffer.encode_string(5, this->icon); @@ -1534,46 +1692,50 @@ void ListEntitiesNumberResponse::encode(ProtoWriteBuffer buffer) const { buffer.encode_uint32(14, this->device_id); #endif } -void ListEntitiesNumberResponse::calculate_size(ProtoSize &size) const { - size.add_length(1, this->object_id.size()); - size.add_fixed32(1, this->key); - size.add_length(1, this->name.size()); +uint32_t ListEntitiesNumberResponse::calculate_size() const { + uint32_t size = 0; + size += ProtoSize::calc_length(1, this->object_id.size()); + size += 5; + size += ProtoSize::calc_length(1, this->name.size()); #ifdef USE_ENTITY_ICON - size.add_length(1, this->icon.size()); + size += ProtoSize::calc_length(1, this->icon.size()); #endif - size.add_float(1, this->min_value); - size.add_float(1, this->max_value); - size.add_float(1, this->step); - size.add_bool(1, this->disabled_by_default); - size.add_uint32(1, static_cast(this->entity_category)); - size.add_length(1, this->unit_of_measurement.size()); - size.add_uint32(1, static_cast(this->mode)); - size.add_length(1, this->device_class.size()); + size += ProtoSize::calc_float(1, this->min_value); + size += ProtoSize::calc_float(1, this->max_value); + size += ProtoSize::calc_float(1, this->step); + size += ProtoSize::calc_bool(1, this->disabled_by_default); + size += ProtoSize::calc_uint32(1, static_cast(this->entity_category)); + size += ProtoSize::calc_length(1, this->unit_of_measurement.size()); + size += ProtoSize::calc_uint32(1, static_cast(this->mode)); + size += ProtoSize::calc_length(1, this->device_class.size()); #ifdef USE_DEVICES - size.add_uint32(1, this->device_id); + size += ProtoSize::calc_uint32(1, this->device_id); #endif + return size; } -void NumberStateResponse::encode(ProtoWriteBuffer buffer) const { - buffer.encode_fixed32(1, this->key); +void NumberStateResponse::encode(ProtoWriteBuffer &buffer) const { + buffer.write_tag_and_fixed32(13, this->key); buffer.encode_float(2, this->state); buffer.encode_bool(3, this->missing_state); #ifdef USE_DEVICES buffer.encode_uint32(4, this->device_id); #endif } -void NumberStateResponse::calculate_size(ProtoSize &size) const { - size.add_fixed32(1, this->key); - size.add_float(1, this->state); - size.add_bool(1, this->missing_state); +uint32_t NumberStateResponse::calculate_size() const { + uint32_t size = 0; + size += 5; + size += ProtoSize::calc_float(1, this->state); + size += ProtoSize::calc_bool(1, this->missing_state); #ifdef USE_DEVICES - size.add_uint32(1, this->device_id); + size += ProtoSize::calc_uint32(1, this->device_id); #endif + return size; } -bool NumberCommandRequest::decode_varint(uint32_t field_id, ProtoVarInt value) { +bool NumberCommandRequest::decode_varint(uint32_t field_id, proto_varint_value_t value) { switch (field_id) { #ifdef USE_DEVICES case 3: - this->device_id = value.as_uint32(); + this->device_id = value; break; #endif default: @@ -1596,9 +1758,9 @@ bool NumberCommandRequest::decode_32bit(uint32_t field_id, Proto32Bit value) { } #endif #ifdef USE_SELECT -void ListEntitiesSelectResponse::encode(ProtoWriteBuffer buffer) const { +void ListEntitiesSelectResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_string(1, this->object_id); - buffer.encode_fixed32(2, this->key); + buffer.write_tag_and_fixed32(21, this->key); buffer.encode_string(3, this->name); #ifdef USE_ENTITY_ICON buffer.encode_string(5, this->icon); @@ -1612,45 +1774,49 @@ void ListEntitiesSelectResponse::encode(ProtoWriteBuffer buffer) const { buffer.encode_uint32(9, this->device_id); #endif } -void ListEntitiesSelectResponse::calculate_size(ProtoSize &size) const { - size.add_length(1, this->object_id.size()); - size.add_fixed32(1, this->key); - size.add_length(1, this->name.size()); +uint32_t ListEntitiesSelectResponse::calculate_size() const { + uint32_t size = 0; + size += ProtoSize::calc_length(1, this->object_id.size()); + size += 5; + size += ProtoSize::calc_length(1, this->name.size()); #ifdef USE_ENTITY_ICON - size.add_length(1, this->icon.size()); + size += ProtoSize::calc_length(1, this->icon.size()); #endif if (!this->options->empty()) { for (const char *it : *this->options) { - size.add_length_force(1, strlen(it)); + size += ProtoSize::calc_length_force(1, strlen(it)); } } - size.add_bool(1, this->disabled_by_default); - size.add_uint32(1, static_cast(this->entity_category)); + size += ProtoSize::calc_bool(1, this->disabled_by_default); + size += ProtoSize::calc_uint32(1, static_cast(this->entity_category)); #ifdef USE_DEVICES - size.add_uint32(1, this->device_id); + size += ProtoSize::calc_uint32(1, this->device_id); #endif + return size; } -void SelectStateResponse::encode(ProtoWriteBuffer buffer) const { - buffer.encode_fixed32(1, this->key); +void SelectStateResponse::encode(ProtoWriteBuffer &buffer) const { + buffer.write_tag_and_fixed32(13, this->key); buffer.encode_string(2, this->state); buffer.encode_bool(3, this->missing_state); #ifdef USE_DEVICES buffer.encode_uint32(4, this->device_id); #endif } -void SelectStateResponse::calculate_size(ProtoSize &size) const { - size.add_fixed32(1, this->key); - size.add_length(1, this->state.size()); - size.add_bool(1, this->missing_state); +uint32_t SelectStateResponse::calculate_size() const { + uint32_t size = 0; + size += 5; + size += ProtoSize::calc_length(1, this->state.size()); + size += ProtoSize::calc_bool(1, this->missing_state); #ifdef USE_DEVICES - size.add_uint32(1, this->device_id); + size += ProtoSize::calc_uint32(1, this->device_id); #endif + return size; } -bool SelectCommandRequest::decode_varint(uint32_t field_id, ProtoVarInt value) { +bool SelectCommandRequest::decode_varint(uint32_t field_id, proto_varint_value_t value) { switch (field_id) { #ifdef USE_DEVICES case 3: - this->device_id = value.as_uint32(); + this->device_id = value; break; #endif default: @@ -1681,9 +1847,9 @@ bool SelectCommandRequest::decode_32bit(uint32_t field_id, Proto32Bit value) { } #endif #ifdef USE_SIREN -void ListEntitiesSirenResponse::encode(ProtoWriteBuffer buffer) const { +void ListEntitiesSirenResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_string(1, this->object_id); - buffer.encode_fixed32(2, this->key); + buffer.write_tag_and_fixed32(21, this->key); buffer.encode_string(3, this->name); #ifdef USE_ENTITY_ICON buffer.encode_string(5, this->icon); @@ -1699,63 +1865,67 @@ void ListEntitiesSirenResponse::encode(ProtoWriteBuffer buffer) const { buffer.encode_uint32(11, this->device_id); #endif } -void ListEntitiesSirenResponse::calculate_size(ProtoSize &size) const { - size.add_length(1, this->object_id.size()); - size.add_fixed32(1, this->key); - size.add_length(1, this->name.size()); +uint32_t ListEntitiesSirenResponse::calculate_size() const { + uint32_t size = 0; + size += ProtoSize::calc_length(1, this->object_id.size()); + size += 5; + size += ProtoSize::calc_length(1, this->name.size()); #ifdef USE_ENTITY_ICON - size.add_length(1, this->icon.size()); + size += ProtoSize::calc_length(1, this->icon.size()); #endif - size.add_bool(1, this->disabled_by_default); + size += ProtoSize::calc_bool(1, this->disabled_by_default); if (!this->tones->empty()) { for (const char *it : *this->tones) { - size.add_length_force(1, strlen(it)); + size += ProtoSize::calc_length_force(1, strlen(it)); } } - size.add_bool(1, this->supports_duration); - size.add_bool(1, this->supports_volume); - size.add_uint32(1, static_cast(this->entity_category)); + size += ProtoSize::calc_bool(1, this->supports_duration); + size += ProtoSize::calc_bool(1, this->supports_volume); + size += ProtoSize::calc_uint32(1, static_cast(this->entity_category)); #ifdef USE_DEVICES - size.add_uint32(1, this->device_id); + size += ProtoSize::calc_uint32(1, this->device_id); #endif + return size; } -void SirenStateResponse::encode(ProtoWriteBuffer buffer) const { - buffer.encode_fixed32(1, this->key); +void SirenStateResponse::encode(ProtoWriteBuffer &buffer) const { + buffer.write_tag_and_fixed32(13, this->key); buffer.encode_bool(2, this->state); #ifdef USE_DEVICES buffer.encode_uint32(3, this->device_id); #endif } -void SirenStateResponse::calculate_size(ProtoSize &size) const { - size.add_fixed32(1, this->key); - size.add_bool(1, this->state); +uint32_t SirenStateResponse::calculate_size() const { + uint32_t size = 0; + size += 5; + size += ProtoSize::calc_bool(1, this->state); #ifdef USE_DEVICES - size.add_uint32(1, this->device_id); + size += ProtoSize::calc_uint32(1, this->device_id); #endif + return size; } -bool SirenCommandRequest::decode_varint(uint32_t field_id, ProtoVarInt value) { +bool SirenCommandRequest::decode_varint(uint32_t field_id, proto_varint_value_t value) { switch (field_id) { case 2: - this->has_state = value.as_bool(); + this->has_state = value != 0; break; case 3: - this->state = value.as_bool(); + this->state = value != 0; break; case 4: - this->has_tone = value.as_bool(); + this->has_tone = value != 0; break; case 6: - this->has_duration = value.as_bool(); + this->has_duration = value != 0; break; case 7: - this->duration = value.as_uint32(); + this->duration = value; break; case 8: - this->has_volume = value.as_bool(); + this->has_volume = value != 0; break; #ifdef USE_DEVICES case 10: - this->device_id = value.as_uint32(); + this->device_id = value; break; #endif default: @@ -1789,9 +1959,9 @@ bool SirenCommandRequest::decode_32bit(uint32_t field_id, Proto32Bit value) { } #endif #ifdef USE_LOCK -void ListEntitiesLockResponse::encode(ProtoWriteBuffer buffer) const { +void ListEntitiesLockResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_string(1, this->object_id); - buffer.encode_fixed32(2, this->key); + buffer.write_tag_and_fixed32(21, this->key); buffer.encode_string(3, this->name); #ifdef USE_ENTITY_ICON buffer.encode_string(5, this->icon); @@ -1806,48 +1976,52 @@ void ListEntitiesLockResponse::encode(ProtoWriteBuffer buffer) const { buffer.encode_uint32(12, this->device_id); #endif } -void ListEntitiesLockResponse::calculate_size(ProtoSize &size) const { - size.add_length(1, this->object_id.size()); - size.add_fixed32(1, this->key); - size.add_length(1, this->name.size()); +uint32_t ListEntitiesLockResponse::calculate_size() const { + uint32_t size = 0; + size += ProtoSize::calc_length(1, this->object_id.size()); + size += 5; + size += ProtoSize::calc_length(1, this->name.size()); #ifdef USE_ENTITY_ICON - size.add_length(1, this->icon.size()); + size += ProtoSize::calc_length(1, this->icon.size()); #endif - size.add_bool(1, this->disabled_by_default); - size.add_uint32(1, static_cast(this->entity_category)); - size.add_bool(1, this->assumed_state); - size.add_bool(1, this->supports_open); - size.add_bool(1, this->requires_code); - size.add_length(1, this->code_format.size()); + size += ProtoSize::calc_bool(1, this->disabled_by_default); + size += ProtoSize::calc_uint32(1, static_cast(this->entity_category)); + size += ProtoSize::calc_bool(1, this->assumed_state); + size += ProtoSize::calc_bool(1, this->supports_open); + size += ProtoSize::calc_bool(1, this->requires_code); + size += ProtoSize::calc_length(1, this->code_format.size()); #ifdef USE_DEVICES - size.add_uint32(1, this->device_id); + size += ProtoSize::calc_uint32(1, this->device_id); #endif + return size; } -void LockStateResponse::encode(ProtoWriteBuffer buffer) const { - buffer.encode_fixed32(1, this->key); +void LockStateResponse::encode(ProtoWriteBuffer &buffer) const { + buffer.write_tag_and_fixed32(13, this->key); buffer.encode_uint32(2, static_cast(this->state)); #ifdef USE_DEVICES buffer.encode_uint32(3, this->device_id); #endif } -void LockStateResponse::calculate_size(ProtoSize &size) const { - size.add_fixed32(1, this->key); - size.add_uint32(1, static_cast(this->state)); +uint32_t LockStateResponse::calculate_size() const { + uint32_t size = 0; + size += 5; + size += ProtoSize::calc_uint32(1, static_cast(this->state)); #ifdef USE_DEVICES - size.add_uint32(1, this->device_id); + size += ProtoSize::calc_uint32(1, this->device_id); #endif + return size; } -bool LockCommandRequest::decode_varint(uint32_t field_id, ProtoVarInt value) { +bool LockCommandRequest::decode_varint(uint32_t field_id, proto_varint_value_t value) { switch (field_id) { case 2: - this->command = static_cast(value.as_uint32()); + this->command = static_cast(value); break; case 3: - this->has_code = value.as_bool(); + this->has_code = value != 0; break; #ifdef USE_DEVICES case 5: - this->device_id = value.as_uint32(); + this->device_id = value; break; #endif default: @@ -1878,9 +2052,9 @@ bool LockCommandRequest::decode_32bit(uint32_t field_id, Proto32Bit value) { } #endif #ifdef USE_BUTTON -void ListEntitiesButtonResponse::encode(ProtoWriteBuffer buffer) const { +void ListEntitiesButtonResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_string(1, this->object_id); - buffer.encode_fixed32(2, this->key); + buffer.write_tag_and_fixed32(21, this->key); buffer.encode_string(3, this->name); #ifdef USE_ENTITY_ICON buffer.encode_string(5, this->icon); @@ -1892,25 +2066,27 @@ void ListEntitiesButtonResponse::encode(ProtoWriteBuffer buffer) const { buffer.encode_uint32(9, this->device_id); #endif } -void ListEntitiesButtonResponse::calculate_size(ProtoSize &size) const { - size.add_length(1, this->object_id.size()); - size.add_fixed32(1, this->key); - size.add_length(1, this->name.size()); +uint32_t ListEntitiesButtonResponse::calculate_size() const { + uint32_t size = 0; + size += ProtoSize::calc_length(1, this->object_id.size()); + size += 5; + size += ProtoSize::calc_length(1, this->name.size()); #ifdef USE_ENTITY_ICON - size.add_length(1, this->icon.size()); + size += ProtoSize::calc_length(1, this->icon.size()); #endif - size.add_bool(1, this->disabled_by_default); - size.add_uint32(1, static_cast(this->entity_category)); - size.add_length(1, this->device_class.size()); + size += ProtoSize::calc_bool(1, this->disabled_by_default); + size += ProtoSize::calc_uint32(1, static_cast(this->entity_category)); + size += ProtoSize::calc_length(1, this->device_class.size()); #ifdef USE_DEVICES - size.add_uint32(1, this->device_id); + size += ProtoSize::calc_uint32(1, this->device_id); #endif + return size; } -bool ButtonCommandRequest::decode_varint(uint32_t field_id, ProtoVarInt value) { +bool ButtonCommandRequest::decode_varint(uint32_t field_id, proto_varint_value_t value) { switch (field_id) { #ifdef USE_DEVICES case 2: - this->device_id = value.as_uint32(); + this->device_id = value; break; #endif default: @@ -1930,23 +2106,25 @@ bool ButtonCommandRequest::decode_32bit(uint32_t field_id, Proto32Bit value) { } #endif #ifdef USE_MEDIA_PLAYER -void MediaPlayerSupportedFormat::encode(ProtoWriteBuffer buffer) const { +void MediaPlayerSupportedFormat::encode(ProtoWriteBuffer &buffer) const { buffer.encode_string(1, this->format); buffer.encode_uint32(2, this->sample_rate); buffer.encode_uint32(3, this->num_channels); buffer.encode_uint32(4, static_cast(this->purpose)); buffer.encode_uint32(5, this->sample_bytes); } -void MediaPlayerSupportedFormat::calculate_size(ProtoSize &size) const { - size.add_length(1, this->format.size()); - size.add_uint32(1, this->sample_rate); - size.add_uint32(1, this->num_channels); - size.add_uint32(1, static_cast(this->purpose)); - size.add_uint32(1, this->sample_bytes); +uint32_t MediaPlayerSupportedFormat::calculate_size() const { + uint32_t size = 0; + size += ProtoSize::calc_length(1, this->format.size()); + size += ProtoSize::calc_uint32(1, this->sample_rate); + size += ProtoSize::calc_uint32(1, this->num_channels); + size += ProtoSize::calc_uint32(1, static_cast(this->purpose)); + size += ProtoSize::calc_uint32(1, this->sample_bytes); + return size; } -void ListEntitiesMediaPlayerResponse::encode(ProtoWriteBuffer buffer) const { +void ListEntitiesMediaPlayerResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_string(1, this->object_id); - buffer.encode_fixed32(2, this->key); + buffer.write_tag_and_fixed32(21, this->key); buffer.encode_string(3, this->name); #ifdef USE_ENTITY_ICON buffer.encode_string(5, this->icon); @@ -1955,31 +2133,37 @@ void ListEntitiesMediaPlayerResponse::encode(ProtoWriteBuffer buffer) const { buffer.encode_uint32(7, static_cast(this->entity_category)); buffer.encode_bool(8, this->supports_pause); for (auto &it : this->supported_formats) { - buffer.encode_message(9, it); + buffer.encode_sub_message(9, it); } #ifdef USE_DEVICES buffer.encode_uint32(10, this->device_id); #endif buffer.encode_uint32(11, this->feature_flags); } -void ListEntitiesMediaPlayerResponse::calculate_size(ProtoSize &size) const { - size.add_length(1, this->object_id.size()); - size.add_fixed32(1, this->key); - size.add_length(1, this->name.size()); +uint32_t ListEntitiesMediaPlayerResponse::calculate_size() const { + uint32_t size = 0; + size += ProtoSize::calc_length(1, this->object_id.size()); + size += 5; + size += ProtoSize::calc_length(1, this->name.size()); #ifdef USE_ENTITY_ICON - size.add_length(1, this->icon.size()); + size += ProtoSize::calc_length(1, this->icon.size()); #endif - size.add_bool(1, this->disabled_by_default); - size.add_uint32(1, static_cast(this->entity_category)); - size.add_bool(1, this->supports_pause); - size.add_repeated_message(1, this->supported_formats); + size += ProtoSize::calc_bool(1, this->disabled_by_default); + size += ProtoSize::calc_uint32(1, static_cast(this->entity_category)); + size += ProtoSize::calc_bool(1, this->supports_pause); + if (!this->supported_formats.empty()) { + for (const auto &it : this->supported_formats) { + size += ProtoSize::calc_message_force(1, it.calculate_size()); + } + } #ifdef USE_DEVICES - size.add_uint32(1, this->device_id); + size += ProtoSize::calc_uint32(1, this->device_id); #endif - size.add_uint32(1, this->feature_flags); + size += ProtoSize::calc_uint32(1, this->feature_flags); + return size; } -void MediaPlayerStateResponse::encode(ProtoWriteBuffer buffer) const { - buffer.encode_fixed32(1, this->key); +void MediaPlayerStateResponse::encode(ProtoWriteBuffer &buffer) const { + buffer.write_tag_and_fixed32(13, this->key); buffer.encode_uint32(2, static_cast(this->state)); buffer.encode_float(3, this->volume); buffer.encode_bool(4, this->muted); @@ -1987,38 +2171,40 @@ void MediaPlayerStateResponse::encode(ProtoWriteBuffer buffer) const { buffer.encode_uint32(5, this->device_id); #endif } -void MediaPlayerStateResponse::calculate_size(ProtoSize &size) const { - size.add_fixed32(1, this->key); - size.add_uint32(1, static_cast(this->state)); - size.add_float(1, this->volume); - size.add_bool(1, this->muted); +uint32_t MediaPlayerStateResponse::calculate_size() const { + uint32_t size = 0; + size += 5; + size += ProtoSize::calc_uint32(1, static_cast(this->state)); + size += ProtoSize::calc_float(1, this->volume); + size += ProtoSize::calc_bool(1, this->muted); #ifdef USE_DEVICES - size.add_uint32(1, this->device_id); + size += ProtoSize::calc_uint32(1, this->device_id); #endif + return size; } -bool MediaPlayerCommandRequest::decode_varint(uint32_t field_id, ProtoVarInt value) { +bool MediaPlayerCommandRequest::decode_varint(uint32_t field_id, proto_varint_value_t value) { switch (field_id) { case 2: - this->has_command = value.as_bool(); + this->has_command = value != 0; break; case 3: - this->command = static_cast(value.as_uint32()); + this->command = static_cast(value); break; case 4: - this->has_volume = value.as_bool(); + this->has_volume = value != 0; break; case 6: - this->has_media_url = value.as_bool(); + this->has_media_url = value != 0; break; case 8: - this->has_announcement = value.as_bool(); + this->has_announcement = value != 0; break; case 9: - this->announcement = value.as_bool(); + this->announcement = value != 0; break; #ifdef USE_DEVICES case 10: - this->device_id = value.as_uint32(); + this->device_id = value; break; #endif default: @@ -2052,80 +2238,90 @@ bool MediaPlayerCommandRequest::decode_32bit(uint32_t field_id, Proto32Bit value } #endif #ifdef USE_BLUETOOTH_PROXY -bool SubscribeBluetoothLEAdvertisementsRequest::decode_varint(uint32_t field_id, ProtoVarInt value) { +bool SubscribeBluetoothLEAdvertisementsRequest::decode_varint(uint32_t field_id, proto_varint_value_t value) { switch (field_id) { case 1: - this->flags = value.as_uint32(); + this->flags = value; break; default: return false; } return true; } -void BluetoothLERawAdvertisement::encode(ProtoWriteBuffer buffer) const { - buffer.encode_uint64(1, this->address); - buffer.encode_sint32(2, this->rssi); +void BluetoothLERawAdvertisement::encode(ProtoWriteBuffer &buffer) const { + buffer.write_raw_byte(8); + buffer.encode_varint_raw_64(this->address); + buffer.write_raw_byte(16); + buffer.encode_varint_raw(encode_zigzag32(this->rssi)); buffer.encode_uint32(3, this->address_type); - buffer.encode_bytes(4, this->data, this->data_len); + buffer.write_raw_byte(34); + buffer.encode_varint_raw(this->data_len); + buffer.encode_raw(this->data, this->data_len); } -void BluetoothLERawAdvertisement::calculate_size(ProtoSize &size) const { - size.add_uint64(1, this->address); - size.add_sint32(1, this->rssi); - size.add_uint32(1, this->address_type); - size.add_length(1, this->data_len); +uint32_t BluetoothLERawAdvertisement::calculate_size() const { + uint32_t size = 0; + size += ProtoSize::calc_uint64_force(1, this->address); + size += ProtoSize::calc_sint32_force(1, this->rssi); + size += ProtoSize::calc_uint32(1, this->address_type); + size += ProtoSize::calc_length_force(1, this->data_len); + return size; } -void BluetoothLERawAdvertisementsResponse::encode(ProtoWriteBuffer buffer) const { +void BluetoothLERawAdvertisementsResponse::encode(ProtoWriteBuffer &buffer) const { for (uint16_t i = 0; i < this->advertisements_len; i++) { - buffer.encode_message(1, this->advertisements[i]); + buffer.encode_sub_message(1, this->advertisements[i]); } } -void BluetoothLERawAdvertisementsResponse::calculate_size(ProtoSize &size) const { +uint32_t BluetoothLERawAdvertisementsResponse::calculate_size() const { + uint32_t size = 0; for (uint16_t i = 0; i < this->advertisements_len; i++) { - size.add_message_object_force(1, this->advertisements[i]); + size += ProtoSize::calc_message_force(1, this->advertisements[i].calculate_size()); } + return size; } -bool BluetoothDeviceRequest::decode_varint(uint32_t field_id, ProtoVarInt value) { +bool BluetoothDeviceRequest::decode_varint(uint32_t field_id, proto_varint_value_t value) { switch (field_id) { case 1: - this->address = value.as_uint64(); + this->address = value; break; case 2: - this->request_type = static_cast(value.as_uint32()); + this->request_type = static_cast(value); break; case 3: - this->has_address_type = value.as_bool(); + this->has_address_type = value != 0; break; case 4: - this->address_type = value.as_uint32(); + this->address_type = value; break; default: return false; } return true; } -void BluetoothDeviceConnectionResponse::encode(ProtoWriteBuffer buffer) const { +void BluetoothDeviceConnectionResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_uint64(1, this->address); buffer.encode_bool(2, this->connected); buffer.encode_uint32(3, this->mtu); buffer.encode_int32(4, this->error); } -void BluetoothDeviceConnectionResponse::calculate_size(ProtoSize &size) const { - size.add_uint64(1, this->address); - size.add_bool(1, this->connected); - size.add_uint32(1, this->mtu); - size.add_int32(1, this->error); +uint32_t BluetoothDeviceConnectionResponse::calculate_size() const { + uint32_t size = 0; + size += ProtoSize::calc_uint64(1, this->address); + size += ProtoSize::calc_bool(1, this->connected); + size += ProtoSize::calc_uint32(1, this->mtu); + size += ProtoSize::calc_int32(1, this->error); + return size; } -bool BluetoothGATTGetServicesRequest::decode_varint(uint32_t field_id, ProtoVarInt value) { +bool BluetoothGATTGetServicesRequest::decode_varint(uint32_t field_id, proto_varint_value_t value) { switch (field_id) { case 1: - this->address = value.as_uint64(); + this->address = value; break; default: return false; } return true; } -void BluetoothGATTDescriptor::encode(ProtoWriteBuffer buffer) const { +void BluetoothGATTDescriptor::encode(ProtoWriteBuffer &buffer) const { if (this->uuid[0] != 0 || this->uuid[1] != 0) { buffer.encode_uint64(1, this->uuid[0], true); buffer.encode_uint64(1, this->uuid[1], true); @@ -2133,15 +2329,17 @@ void BluetoothGATTDescriptor::encode(ProtoWriteBuffer buffer) const { buffer.encode_uint32(2, this->handle); buffer.encode_uint32(3, this->short_uuid); } -void BluetoothGATTDescriptor::calculate_size(ProtoSize &size) const { +uint32_t BluetoothGATTDescriptor::calculate_size() const { + uint32_t size = 0; if (this->uuid[0] != 0 || this->uuid[1] != 0) { - size.add_uint64_force(1, this->uuid[0]); - size.add_uint64_force(1, this->uuid[1]); + size += ProtoSize::calc_uint64_force(1, this->uuid[0]); + size += ProtoSize::calc_uint64_force(1, this->uuid[1]); } - size.add_uint32(1, this->handle); - size.add_uint32(1, this->short_uuid); + size += ProtoSize::calc_uint32(1, this->handle); + size += ProtoSize::calc_uint32(1, this->short_uuid); + return size; } -void BluetoothGATTCharacteristic::encode(ProtoWriteBuffer buffer) const { +void BluetoothGATTCharacteristic::encode(ProtoWriteBuffer &buffer) const { if (this->uuid[0] != 0 || this->uuid[1] != 0) { buffer.encode_uint64(1, this->uuid[0], true); buffer.encode_uint64(1, this->uuid[1], true); @@ -2149,87 +2347,111 @@ void BluetoothGATTCharacteristic::encode(ProtoWriteBuffer buffer) const { buffer.encode_uint32(2, this->handle); buffer.encode_uint32(3, this->properties); for (auto &it : this->descriptors) { - buffer.encode_message(4, it); + buffer.encode_sub_message(4, it); } buffer.encode_uint32(5, this->short_uuid); } -void BluetoothGATTCharacteristic::calculate_size(ProtoSize &size) const { +uint32_t BluetoothGATTCharacteristic::calculate_size() const { + uint32_t size = 0; if (this->uuid[0] != 0 || this->uuid[1] != 0) { - size.add_uint64_force(1, this->uuid[0]); - size.add_uint64_force(1, this->uuid[1]); + size += ProtoSize::calc_uint64_force(1, this->uuid[0]); + size += ProtoSize::calc_uint64_force(1, this->uuid[1]); } - size.add_uint32(1, this->handle); - size.add_uint32(1, this->properties); - size.add_repeated_message(1, this->descriptors); - size.add_uint32(1, this->short_uuid); + size += ProtoSize::calc_uint32(1, this->handle); + size += ProtoSize::calc_uint32(1, this->properties); + if (!this->descriptors.empty()) { + for (const auto &it : this->descriptors) { + size += ProtoSize::calc_message_force(1, it.calculate_size()); + } + } + size += ProtoSize::calc_uint32(1, this->short_uuid); + return size; } -void BluetoothGATTService::encode(ProtoWriteBuffer buffer) const { +void BluetoothGATTService::encode(ProtoWriteBuffer &buffer) const { if (this->uuid[0] != 0 || this->uuid[1] != 0) { buffer.encode_uint64(1, this->uuid[0], true); buffer.encode_uint64(1, this->uuid[1], true); } buffer.encode_uint32(2, this->handle); for (auto &it : this->characteristics) { - buffer.encode_message(3, it); + buffer.encode_sub_message(3, it); } buffer.encode_uint32(4, this->short_uuid); } -void BluetoothGATTService::calculate_size(ProtoSize &size) const { +uint32_t BluetoothGATTService::calculate_size() const { + uint32_t size = 0; if (this->uuid[0] != 0 || this->uuid[1] != 0) { - size.add_uint64_force(1, this->uuid[0]); - size.add_uint64_force(1, this->uuid[1]); + size += ProtoSize::calc_uint64_force(1, this->uuid[0]); + size += ProtoSize::calc_uint64_force(1, this->uuid[1]); } - size.add_uint32(1, this->handle); - size.add_repeated_message(1, this->characteristics); - size.add_uint32(1, this->short_uuid); + size += ProtoSize::calc_uint32(1, this->handle); + if (!this->characteristics.empty()) { + for (const auto &it : this->characteristics) { + size += ProtoSize::calc_message_force(1, it.calculate_size()); + } + } + size += ProtoSize::calc_uint32(1, this->short_uuid); + return size; } -void BluetoothGATTGetServicesResponse::encode(ProtoWriteBuffer buffer) const { +void BluetoothGATTGetServicesResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_uint64(1, this->address); for (auto &it : this->services) { - buffer.encode_message(2, it); + buffer.encode_sub_message(2, it); } } -void BluetoothGATTGetServicesResponse::calculate_size(ProtoSize &size) const { - size.add_uint64(1, this->address); - size.add_repeated_message(1, this->services); +uint32_t BluetoothGATTGetServicesResponse::calculate_size() const { + uint32_t size = 0; + size += ProtoSize::calc_uint64(1, this->address); + if (!this->services.empty()) { + for (const auto &it : this->services) { + size += ProtoSize::calc_message_force(1, it.calculate_size()); + } + } + return size; } -void BluetoothGATTGetServicesDoneResponse::encode(ProtoWriteBuffer buffer) const { +void BluetoothGATTGetServicesDoneResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_uint64(1, this->address); } -void BluetoothGATTGetServicesDoneResponse::calculate_size(ProtoSize &size) const { size.add_uint64(1, this->address); } -bool BluetoothGATTReadRequest::decode_varint(uint32_t field_id, ProtoVarInt value) { +uint32_t BluetoothGATTGetServicesDoneResponse::calculate_size() const { + uint32_t size = 0; + size += ProtoSize::calc_uint64(1, this->address); + return size; +} +bool BluetoothGATTReadRequest::decode_varint(uint32_t field_id, proto_varint_value_t value) { switch (field_id) { case 1: - this->address = value.as_uint64(); + this->address = value; break; case 2: - this->handle = value.as_uint32(); + this->handle = value; break; default: return false; } return true; } -void BluetoothGATTReadResponse::encode(ProtoWriteBuffer buffer) const { +void BluetoothGATTReadResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_uint64(1, this->address); buffer.encode_uint32(2, this->handle); buffer.encode_bytes(3, this->data_ptr_, this->data_len_); } -void BluetoothGATTReadResponse::calculate_size(ProtoSize &size) const { - size.add_uint64(1, this->address); - size.add_uint32(1, this->handle); - size.add_length(1, this->data_len_); +uint32_t BluetoothGATTReadResponse::calculate_size() const { + uint32_t size = 0; + size += ProtoSize::calc_uint64(1, this->address); + size += ProtoSize::calc_uint32(1, this->handle); + size += ProtoSize::calc_length(1, this->data_len_); + return size; } -bool BluetoothGATTWriteRequest::decode_varint(uint32_t field_id, ProtoVarInt value) { +bool BluetoothGATTWriteRequest::decode_varint(uint32_t field_id, proto_varint_value_t value) { switch (field_id) { case 1: - this->address = value.as_uint64(); + this->address = value; break; case 2: - this->handle = value.as_uint32(); + this->handle = value; break; case 3: - this->response = value.as_bool(); + this->response = value != 0; break; default: return false; @@ -2248,26 +2470,26 @@ bool BluetoothGATTWriteRequest::decode_length(uint32_t field_id, ProtoLengthDeli } return true; } -bool BluetoothGATTReadDescriptorRequest::decode_varint(uint32_t field_id, ProtoVarInt value) { +bool BluetoothGATTReadDescriptorRequest::decode_varint(uint32_t field_id, proto_varint_value_t value) { switch (field_id) { case 1: - this->address = value.as_uint64(); + this->address = value; break; case 2: - this->handle = value.as_uint32(); + this->handle = value; break; default: return false; } return true; } -bool BluetoothGATTWriteDescriptorRequest::decode_varint(uint32_t field_id, ProtoVarInt value) { +bool BluetoothGATTWriteDescriptorRequest::decode_varint(uint32_t field_id, proto_varint_value_t value) { switch (field_id) { case 1: - this->address = value.as_uint64(); + this->address = value; break; case 2: - this->handle = value.as_uint32(); + this->handle = value; break; default: return false; @@ -2286,33 +2508,35 @@ bool BluetoothGATTWriteDescriptorRequest::decode_length(uint32_t field_id, Proto } return true; } -bool BluetoothGATTNotifyRequest::decode_varint(uint32_t field_id, ProtoVarInt value) { +bool BluetoothGATTNotifyRequest::decode_varint(uint32_t field_id, proto_varint_value_t value) { switch (field_id) { case 1: - this->address = value.as_uint64(); + this->address = value; break; case 2: - this->handle = value.as_uint32(); + this->handle = value; break; case 3: - this->enable = value.as_bool(); + this->enable = value != 0; break; default: return false; } return true; } -void BluetoothGATTNotifyDataResponse::encode(ProtoWriteBuffer buffer) const { +void BluetoothGATTNotifyDataResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_uint64(1, this->address); buffer.encode_uint32(2, this->handle); buffer.encode_bytes(3, this->data_ptr_, this->data_len_); } -void BluetoothGATTNotifyDataResponse::calculate_size(ProtoSize &size) const { - size.add_uint64(1, this->address); - size.add_uint32(1, this->handle); - size.add_length(1, this->data_len_); +uint32_t BluetoothGATTNotifyDataResponse::calculate_size() const { + uint32_t size = 0; + size += ProtoSize::calc_uint64(1, this->address); + size += ProtoSize::calc_uint32(1, this->handle); + size += ProtoSize::calc_length(1, this->data_len_); + return size; } -void BluetoothConnectionsFreeResponse::encode(ProtoWriteBuffer buffer) const { +void BluetoothConnectionsFreeResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_uint32(1, this->free); buffer.encode_uint32(2, this->limit); for (const auto &it : this->allocated) { @@ -2321,85 +2545,101 @@ void BluetoothConnectionsFreeResponse::encode(ProtoWriteBuffer buffer) const { } } } -void BluetoothConnectionsFreeResponse::calculate_size(ProtoSize &size) const { - size.add_uint32(1, this->free); - size.add_uint32(1, this->limit); +uint32_t BluetoothConnectionsFreeResponse::calculate_size() const { + uint32_t size = 0; + size += ProtoSize::calc_uint32(1, this->free); + size += ProtoSize::calc_uint32(1, this->limit); for (const auto &it : this->allocated) { if (it != 0) { - size.add_uint64_force(1, it); + size += ProtoSize::calc_uint64_force(1, it); } } + return size; } -void BluetoothGATTErrorResponse::encode(ProtoWriteBuffer buffer) const { +void BluetoothGATTErrorResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_uint64(1, this->address); buffer.encode_uint32(2, this->handle); buffer.encode_int32(3, this->error); } -void BluetoothGATTErrorResponse::calculate_size(ProtoSize &size) const { - size.add_uint64(1, this->address); - size.add_uint32(1, this->handle); - size.add_int32(1, this->error); +uint32_t BluetoothGATTErrorResponse::calculate_size() const { + uint32_t size = 0; + size += ProtoSize::calc_uint64(1, this->address); + size += ProtoSize::calc_uint32(1, this->handle); + size += ProtoSize::calc_int32(1, this->error); + return size; } -void BluetoothGATTWriteResponse::encode(ProtoWriteBuffer buffer) const { +void BluetoothGATTWriteResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_uint64(1, this->address); buffer.encode_uint32(2, this->handle); } -void BluetoothGATTWriteResponse::calculate_size(ProtoSize &size) const { - size.add_uint64(1, this->address); - size.add_uint32(1, this->handle); +uint32_t BluetoothGATTWriteResponse::calculate_size() const { + uint32_t size = 0; + size += ProtoSize::calc_uint64(1, this->address); + size += ProtoSize::calc_uint32(1, this->handle); + return size; } -void BluetoothGATTNotifyResponse::encode(ProtoWriteBuffer buffer) const { +void BluetoothGATTNotifyResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_uint64(1, this->address); buffer.encode_uint32(2, this->handle); } -void BluetoothGATTNotifyResponse::calculate_size(ProtoSize &size) const { - size.add_uint64(1, this->address); - size.add_uint32(1, this->handle); +uint32_t BluetoothGATTNotifyResponse::calculate_size() const { + uint32_t size = 0; + size += ProtoSize::calc_uint64(1, this->address); + size += ProtoSize::calc_uint32(1, this->handle); + return size; } -void BluetoothDevicePairingResponse::encode(ProtoWriteBuffer buffer) const { +void BluetoothDevicePairingResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_uint64(1, this->address); buffer.encode_bool(2, this->paired); buffer.encode_int32(3, this->error); } -void BluetoothDevicePairingResponse::calculate_size(ProtoSize &size) const { - size.add_uint64(1, this->address); - size.add_bool(1, this->paired); - size.add_int32(1, this->error); +uint32_t BluetoothDevicePairingResponse::calculate_size() const { + uint32_t size = 0; + size += ProtoSize::calc_uint64(1, this->address); + size += ProtoSize::calc_bool(1, this->paired); + size += ProtoSize::calc_int32(1, this->error); + return size; } -void BluetoothDeviceUnpairingResponse::encode(ProtoWriteBuffer buffer) const { +void BluetoothDeviceUnpairingResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_uint64(1, this->address); buffer.encode_bool(2, this->success); buffer.encode_int32(3, this->error); } -void BluetoothDeviceUnpairingResponse::calculate_size(ProtoSize &size) const { - size.add_uint64(1, this->address); - size.add_bool(1, this->success); - size.add_int32(1, this->error); +uint32_t BluetoothDeviceUnpairingResponse::calculate_size() const { + uint32_t size = 0; + size += ProtoSize::calc_uint64(1, this->address); + size += ProtoSize::calc_bool(1, this->success); + size += ProtoSize::calc_int32(1, this->error); + return size; } -void BluetoothDeviceClearCacheResponse::encode(ProtoWriteBuffer buffer) const { +void BluetoothDeviceClearCacheResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_uint64(1, this->address); buffer.encode_bool(2, this->success); buffer.encode_int32(3, this->error); } -void BluetoothDeviceClearCacheResponse::calculate_size(ProtoSize &size) const { - size.add_uint64(1, this->address); - size.add_bool(1, this->success); - size.add_int32(1, this->error); +uint32_t BluetoothDeviceClearCacheResponse::calculate_size() const { + uint32_t size = 0; + size += ProtoSize::calc_uint64(1, this->address); + size += ProtoSize::calc_bool(1, this->success); + size += ProtoSize::calc_int32(1, this->error); + return size; } -void BluetoothScannerStateResponse::encode(ProtoWriteBuffer buffer) const { +void BluetoothScannerStateResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_uint32(1, static_cast(this->state)); buffer.encode_uint32(2, static_cast(this->mode)); buffer.encode_uint32(3, static_cast(this->configured_mode)); } -void BluetoothScannerStateResponse::calculate_size(ProtoSize &size) const { - size.add_uint32(1, static_cast(this->state)); - size.add_uint32(1, static_cast(this->mode)); - size.add_uint32(1, static_cast(this->configured_mode)); +uint32_t BluetoothScannerStateResponse::calculate_size() const { + uint32_t size = 0; + size += ProtoSize::calc_uint32(1, static_cast(this->state)); + size += ProtoSize::calc_uint32(1, static_cast(this->mode)); + size += ProtoSize::calc_uint32(1, static_cast(this->configured_mode)); + return size; } -bool BluetoothScannerSetModeRequest::decode_varint(uint32_t field_id, ProtoVarInt value) { +bool BluetoothScannerSetModeRequest::decode_varint(uint32_t field_id, proto_varint_value_t value) { switch (field_id) { case 1: - this->mode = static_cast(value.as_uint32()); + this->mode = static_cast(value); break; default: return false; @@ -2408,50 +2648,54 @@ bool BluetoothScannerSetModeRequest::decode_varint(uint32_t field_id, ProtoVarIn } #endif #ifdef USE_VOICE_ASSISTANT -bool SubscribeVoiceAssistantRequest::decode_varint(uint32_t field_id, ProtoVarInt value) { +bool SubscribeVoiceAssistantRequest::decode_varint(uint32_t field_id, proto_varint_value_t value) { switch (field_id) { case 1: - this->subscribe = value.as_bool(); + this->subscribe = value != 0; break; case 2: - this->flags = value.as_uint32(); + this->flags = value; break; default: return false; } return true; } -void VoiceAssistantAudioSettings::encode(ProtoWriteBuffer buffer) const { +void VoiceAssistantAudioSettings::encode(ProtoWriteBuffer &buffer) const { buffer.encode_uint32(1, this->noise_suppression_level); buffer.encode_uint32(2, this->auto_gain); buffer.encode_float(3, this->volume_multiplier); } -void VoiceAssistantAudioSettings::calculate_size(ProtoSize &size) const { - size.add_uint32(1, this->noise_suppression_level); - size.add_uint32(1, this->auto_gain); - size.add_float(1, this->volume_multiplier); +uint32_t VoiceAssistantAudioSettings::calculate_size() const { + uint32_t size = 0; + size += ProtoSize::calc_uint32(1, this->noise_suppression_level); + size += ProtoSize::calc_uint32(1, this->auto_gain); + size += ProtoSize::calc_float(1, this->volume_multiplier); + return size; } -void VoiceAssistantRequest::encode(ProtoWriteBuffer buffer) const { +void VoiceAssistantRequest::encode(ProtoWriteBuffer &buffer) const { buffer.encode_bool(1, this->start); buffer.encode_string(2, this->conversation_id); buffer.encode_uint32(3, this->flags); - buffer.encode_message(4, this->audio_settings); + buffer.encode_optional_sub_message(4, this->audio_settings); buffer.encode_string(5, this->wake_word_phrase); } -void VoiceAssistantRequest::calculate_size(ProtoSize &size) const { - size.add_bool(1, this->start); - size.add_length(1, this->conversation_id.size()); - size.add_uint32(1, this->flags); - size.add_message_object(1, this->audio_settings); - size.add_length(1, this->wake_word_phrase.size()); +uint32_t VoiceAssistantRequest::calculate_size() const { + uint32_t size = 0; + size += ProtoSize::calc_bool(1, this->start); + size += ProtoSize::calc_length(1, this->conversation_id.size()); + size += ProtoSize::calc_uint32(1, this->flags); + size += ProtoSize::calc_message(1, this->audio_settings.calculate_size()); + size += ProtoSize::calc_length(1, this->wake_word_phrase.size()); + return size; } -bool VoiceAssistantResponse::decode_varint(uint32_t field_id, ProtoVarInt value) { +bool VoiceAssistantResponse::decode_varint(uint32_t field_id, proto_varint_value_t value) { switch (field_id) { case 1: - this->port = value.as_uint32(); + this->port = value; break; case 2: - this->error = value.as_bool(); + this->error = value != 0; break; default: return false; @@ -2473,10 +2717,10 @@ bool VoiceAssistantEventData::decode_length(uint32_t field_id, ProtoLengthDelimi } return true; } -bool VoiceAssistantEventResponse::decode_varint(uint32_t field_id, ProtoVarInt value) { +bool VoiceAssistantEventResponse::decode_varint(uint32_t field_id, proto_varint_value_t value) { switch (field_id) { case 1: - this->event_type = static_cast(value.as_uint32()); + this->event_type = static_cast(value); break; default: return false; @@ -2494,10 +2738,10 @@ bool VoiceAssistantEventResponse::decode_length(uint32_t field_id, ProtoLengthDe } return true; } -bool VoiceAssistantAudio::decode_varint(uint32_t field_id, ProtoVarInt value) { +bool VoiceAssistantAudio::decode_varint(uint32_t field_id, proto_varint_value_t value) { switch (field_id) { case 2: - this->end = value.as_bool(); + this->end = value != 0; break; default: return false; @@ -2516,27 +2760,29 @@ bool VoiceAssistantAudio::decode_length(uint32_t field_id, ProtoLengthDelimited } return true; } -void VoiceAssistantAudio::encode(ProtoWriteBuffer buffer) const { +void VoiceAssistantAudio::encode(ProtoWriteBuffer &buffer) const { buffer.encode_bytes(1, this->data, this->data_len); buffer.encode_bool(2, this->end); } -void VoiceAssistantAudio::calculate_size(ProtoSize &size) const { - size.add_length(1, this->data_len); - size.add_bool(1, this->end); +uint32_t VoiceAssistantAudio::calculate_size() const { + uint32_t size = 0; + size += ProtoSize::calc_length(1, this->data_len); + size += ProtoSize::calc_bool(1, this->end); + return size; } -bool VoiceAssistantTimerEventResponse::decode_varint(uint32_t field_id, ProtoVarInt value) { +bool VoiceAssistantTimerEventResponse::decode_varint(uint32_t field_id, proto_varint_value_t value) { switch (field_id) { case 1: - this->event_type = static_cast(value.as_uint32()); + this->event_type = static_cast(value); break; case 4: - this->total_seconds = value.as_uint32(); + this->total_seconds = value; break; case 5: - this->seconds_left = value.as_uint32(); + this->seconds_left = value; break; case 6: - this->is_active = value.as_bool(); + this->is_active = value != 0; break; default: return false; @@ -2558,10 +2804,10 @@ bool VoiceAssistantTimerEventResponse::decode_length(uint32_t field_id, ProtoLen } return true; } -bool VoiceAssistantAnnounceRequest::decode_varint(uint32_t field_id, ProtoVarInt value) { +bool VoiceAssistantAnnounceRequest::decode_varint(uint32_t field_id, proto_varint_value_t value) { switch (field_id) { case 4: - this->start_conversation = value.as_bool(); + this->start_conversation = value != 0; break; default: return false; @@ -2587,28 +2833,34 @@ bool VoiceAssistantAnnounceRequest::decode_length(uint32_t field_id, ProtoLength } return true; } -void VoiceAssistantAnnounceFinished::encode(ProtoWriteBuffer buffer) const { buffer.encode_bool(1, this->success); } -void VoiceAssistantAnnounceFinished::calculate_size(ProtoSize &size) const { size.add_bool(1, this->success); } -void VoiceAssistantWakeWord::encode(ProtoWriteBuffer buffer) const { +void VoiceAssistantAnnounceFinished::encode(ProtoWriteBuffer &buffer) const { buffer.encode_bool(1, this->success); } +uint32_t VoiceAssistantAnnounceFinished::calculate_size() const { + uint32_t size = 0; + size += ProtoSize::calc_bool(1, this->success); + return size; +} +void VoiceAssistantWakeWord::encode(ProtoWriteBuffer &buffer) const { buffer.encode_string(1, this->id); buffer.encode_string(2, this->wake_word); for (auto &it : this->trained_languages) { buffer.encode_string(3, it, true); } } -void VoiceAssistantWakeWord::calculate_size(ProtoSize &size) const { - size.add_length(1, this->id.size()); - size.add_length(1, this->wake_word.size()); +uint32_t VoiceAssistantWakeWord::calculate_size() const { + uint32_t size = 0; + size += ProtoSize::calc_length(1, this->id.size()); + size += ProtoSize::calc_length(1, this->wake_word.size()); if (!this->trained_languages.empty()) { for (const auto &it : this->trained_languages) { - size.add_length_force(1, it.size()); + size += ProtoSize::calc_length_force(1, it.size()); } } + return size; } -bool VoiceAssistantExternalWakeWord::decode_varint(uint32_t field_id, ProtoVarInt value) { +bool VoiceAssistantExternalWakeWord::decode_varint(uint32_t field_id, proto_varint_value_t value) { switch (field_id) { case 5: - this->model_size = value.as_uint32(); + this->model_size = value; break; default: return false; @@ -2656,23 +2908,29 @@ bool VoiceAssistantConfigurationRequest::decode_length(uint32_t field_id, ProtoL } return true; } -void VoiceAssistantConfigurationResponse::encode(ProtoWriteBuffer buffer) const { +void VoiceAssistantConfigurationResponse::encode(ProtoWriteBuffer &buffer) const { for (auto &it : this->available_wake_words) { - buffer.encode_message(1, it); + buffer.encode_sub_message(1, it); } for (const auto &it : *this->active_wake_words) { buffer.encode_string(2, it, true); } buffer.encode_uint32(3, this->max_active_wake_words); } -void VoiceAssistantConfigurationResponse::calculate_size(ProtoSize &size) const { - size.add_repeated_message(1, this->available_wake_words); - if (!this->active_wake_words->empty()) { - for (const auto &it : *this->active_wake_words) { - size.add_length_force(1, it.size()); +uint32_t VoiceAssistantConfigurationResponse::calculate_size() const { + uint32_t size = 0; + if (!this->available_wake_words.empty()) { + for (const auto &it : this->available_wake_words) { + size += ProtoSize::calc_message_force(1, it.calculate_size()); } } - size.add_uint32(1, this->max_active_wake_words); + if (!this->active_wake_words->empty()) { + for (const auto &it : *this->active_wake_words) { + size += ProtoSize::calc_length_force(1, it.size()); + } + } + size += ProtoSize::calc_uint32(1, this->max_active_wake_words); + return size; } bool VoiceAssistantSetConfiguration::decode_length(uint32_t field_id, ProtoLengthDelimited value) { switch (field_id) { @@ -2686,9 +2944,9 @@ bool VoiceAssistantSetConfiguration::decode_length(uint32_t field_id, ProtoLengt } #endif #ifdef USE_ALARM_CONTROL_PANEL -void ListEntitiesAlarmControlPanelResponse::encode(ProtoWriteBuffer buffer) const { +void ListEntitiesAlarmControlPanelResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_string(1, this->object_id); - buffer.encode_fixed32(2, this->key); + buffer.write_tag_and_fixed32(21, this->key); buffer.encode_string(3, this->name); #ifdef USE_ENTITY_ICON buffer.encode_string(5, this->icon); @@ -2702,44 +2960,48 @@ void ListEntitiesAlarmControlPanelResponse::encode(ProtoWriteBuffer buffer) cons buffer.encode_uint32(11, this->device_id); #endif } -void ListEntitiesAlarmControlPanelResponse::calculate_size(ProtoSize &size) const { - size.add_length(1, this->object_id.size()); - size.add_fixed32(1, this->key); - size.add_length(1, this->name.size()); +uint32_t ListEntitiesAlarmControlPanelResponse::calculate_size() const { + uint32_t size = 0; + size += ProtoSize::calc_length(1, this->object_id.size()); + size += 5; + size += ProtoSize::calc_length(1, this->name.size()); #ifdef USE_ENTITY_ICON - size.add_length(1, this->icon.size()); + size += ProtoSize::calc_length(1, this->icon.size()); #endif - size.add_bool(1, this->disabled_by_default); - size.add_uint32(1, static_cast(this->entity_category)); - size.add_uint32(1, this->supported_features); - size.add_bool(1, this->requires_code); - size.add_bool(1, this->requires_code_to_arm); + size += ProtoSize::calc_bool(1, this->disabled_by_default); + size += ProtoSize::calc_uint32(1, static_cast(this->entity_category)); + size += ProtoSize::calc_uint32(1, this->supported_features); + size += ProtoSize::calc_bool(1, this->requires_code); + size += ProtoSize::calc_bool(1, this->requires_code_to_arm); #ifdef USE_DEVICES - size.add_uint32(1, this->device_id); + size += ProtoSize::calc_uint32(1, this->device_id); #endif + return size; } -void AlarmControlPanelStateResponse::encode(ProtoWriteBuffer buffer) const { - buffer.encode_fixed32(1, this->key); +void AlarmControlPanelStateResponse::encode(ProtoWriteBuffer &buffer) const { + buffer.write_tag_and_fixed32(13, this->key); buffer.encode_uint32(2, static_cast(this->state)); #ifdef USE_DEVICES buffer.encode_uint32(3, this->device_id); #endif } -void AlarmControlPanelStateResponse::calculate_size(ProtoSize &size) const { - size.add_fixed32(1, this->key); - size.add_uint32(1, static_cast(this->state)); +uint32_t AlarmControlPanelStateResponse::calculate_size() const { + uint32_t size = 0; + size += 5; + size += ProtoSize::calc_uint32(1, static_cast(this->state)); #ifdef USE_DEVICES - size.add_uint32(1, this->device_id); + size += ProtoSize::calc_uint32(1, this->device_id); #endif + return size; } -bool AlarmControlPanelCommandRequest::decode_varint(uint32_t field_id, ProtoVarInt value) { +bool AlarmControlPanelCommandRequest::decode_varint(uint32_t field_id, proto_varint_value_t value) { switch (field_id) { case 2: - this->command = static_cast(value.as_uint32()); + this->command = static_cast(value); break; #ifdef USE_DEVICES case 4: - this->device_id = value.as_uint32(); + this->device_id = value; break; #endif default: @@ -2770,9 +3032,9 @@ bool AlarmControlPanelCommandRequest::decode_32bit(uint32_t field_id, Proto32Bit } #endif #ifdef USE_TEXT -void ListEntitiesTextResponse::encode(ProtoWriteBuffer buffer) const { +void ListEntitiesTextResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_string(1, this->object_id); - buffer.encode_fixed32(2, this->key); + buffer.write_tag_and_fixed32(21, this->key); buffer.encode_string(3, this->name); #ifdef USE_ENTITY_ICON buffer.encode_string(5, this->icon); @@ -2787,44 +3049,48 @@ void ListEntitiesTextResponse::encode(ProtoWriteBuffer buffer) const { buffer.encode_uint32(12, this->device_id); #endif } -void ListEntitiesTextResponse::calculate_size(ProtoSize &size) const { - size.add_length(1, this->object_id.size()); - size.add_fixed32(1, this->key); - size.add_length(1, this->name.size()); +uint32_t ListEntitiesTextResponse::calculate_size() const { + uint32_t size = 0; + size += ProtoSize::calc_length(1, this->object_id.size()); + size += 5; + size += ProtoSize::calc_length(1, this->name.size()); #ifdef USE_ENTITY_ICON - size.add_length(1, this->icon.size()); + size += ProtoSize::calc_length(1, this->icon.size()); #endif - size.add_bool(1, this->disabled_by_default); - size.add_uint32(1, static_cast(this->entity_category)); - size.add_uint32(1, this->min_length); - size.add_uint32(1, this->max_length); - size.add_length(1, this->pattern.size()); - size.add_uint32(1, static_cast(this->mode)); + size += ProtoSize::calc_bool(1, this->disabled_by_default); + size += ProtoSize::calc_uint32(1, static_cast(this->entity_category)); + size += ProtoSize::calc_uint32(1, this->min_length); + size += ProtoSize::calc_uint32(1, this->max_length); + size += ProtoSize::calc_length(1, this->pattern.size()); + size += ProtoSize::calc_uint32(1, static_cast(this->mode)); #ifdef USE_DEVICES - size.add_uint32(1, this->device_id); + size += ProtoSize::calc_uint32(1, this->device_id); #endif + return size; } -void TextStateResponse::encode(ProtoWriteBuffer buffer) const { - buffer.encode_fixed32(1, this->key); +void TextStateResponse::encode(ProtoWriteBuffer &buffer) const { + buffer.write_tag_and_fixed32(13, this->key); buffer.encode_string(2, this->state); buffer.encode_bool(3, this->missing_state); #ifdef USE_DEVICES buffer.encode_uint32(4, this->device_id); #endif } -void TextStateResponse::calculate_size(ProtoSize &size) const { - size.add_fixed32(1, this->key); - size.add_length(1, this->state.size()); - size.add_bool(1, this->missing_state); +uint32_t TextStateResponse::calculate_size() const { + uint32_t size = 0; + size += 5; + size += ProtoSize::calc_length(1, this->state.size()); + size += ProtoSize::calc_bool(1, this->missing_state); #ifdef USE_DEVICES - size.add_uint32(1, this->device_id); + size += ProtoSize::calc_uint32(1, this->device_id); #endif + return size; } -bool TextCommandRequest::decode_varint(uint32_t field_id, ProtoVarInt value) { +bool TextCommandRequest::decode_varint(uint32_t field_id, proto_varint_value_t value) { switch (field_id) { #ifdef USE_DEVICES case 3: - this->device_id = value.as_uint32(); + this->device_id = value; break; #endif default: @@ -2855,9 +3121,9 @@ bool TextCommandRequest::decode_32bit(uint32_t field_id, Proto32Bit value) { } #endif #ifdef USE_DATETIME_DATE -void ListEntitiesDateResponse::encode(ProtoWriteBuffer buffer) const { +void ListEntitiesDateResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_string(1, this->object_id); - buffer.encode_fixed32(2, this->key); + buffer.write_tag_and_fixed32(21, this->key); buffer.encode_string(3, this->name); #ifdef USE_ENTITY_ICON buffer.encode_string(5, this->icon); @@ -2868,21 +3134,23 @@ void ListEntitiesDateResponse::encode(ProtoWriteBuffer buffer) const { buffer.encode_uint32(8, this->device_id); #endif } -void ListEntitiesDateResponse::calculate_size(ProtoSize &size) const { - size.add_length(1, this->object_id.size()); - size.add_fixed32(1, this->key); - size.add_length(1, this->name.size()); +uint32_t ListEntitiesDateResponse::calculate_size() const { + uint32_t size = 0; + size += ProtoSize::calc_length(1, this->object_id.size()); + size += 5; + size += ProtoSize::calc_length(1, this->name.size()); #ifdef USE_ENTITY_ICON - size.add_length(1, this->icon.size()); + size += ProtoSize::calc_length(1, this->icon.size()); #endif - size.add_bool(1, this->disabled_by_default); - size.add_uint32(1, static_cast(this->entity_category)); + size += ProtoSize::calc_bool(1, this->disabled_by_default); + size += ProtoSize::calc_uint32(1, static_cast(this->entity_category)); #ifdef USE_DEVICES - size.add_uint32(1, this->device_id); + size += ProtoSize::calc_uint32(1, this->device_id); #endif + return size; } -void DateStateResponse::encode(ProtoWriteBuffer buffer) const { - buffer.encode_fixed32(1, this->key); +void DateStateResponse::encode(ProtoWriteBuffer &buffer) const { + buffer.write_tag_and_fixed32(13, this->key); buffer.encode_bool(2, this->missing_state); buffer.encode_uint32(3, this->year); buffer.encode_uint32(4, this->month); @@ -2891,30 +3159,32 @@ void DateStateResponse::encode(ProtoWriteBuffer buffer) const { buffer.encode_uint32(6, this->device_id); #endif } -void DateStateResponse::calculate_size(ProtoSize &size) const { - size.add_fixed32(1, this->key); - size.add_bool(1, this->missing_state); - size.add_uint32(1, this->year); - size.add_uint32(1, this->month); - size.add_uint32(1, this->day); +uint32_t DateStateResponse::calculate_size() const { + uint32_t size = 0; + size += 5; + size += ProtoSize::calc_bool(1, this->missing_state); + size += ProtoSize::calc_uint32(1, this->year); + size += ProtoSize::calc_uint32(1, this->month); + size += ProtoSize::calc_uint32(1, this->day); #ifdef USE_DEVICES - size.add_uint32(1, this->device_id); + size += ProtoSize::calc_uint32(1, this->device_id); #endif + return size; } -bool DateCommandRequest::decode_varint(uint32_t field_id, ProtoVarInt value) { +bool DateCommandRequest::decode_varint(uint32_t field_id, proto_varint_value_t value) { switch (field_id) { case 2: - this->year = value.as_uint32(); + this->year = value; break; case 3: - this->month = value.as_uint32(); + this->month = value; break; case 4: - this->day = value.as_uint32(); + this->day = value; break; #ifdef USE_DEVICES case 5: - this->device_id = value.as_uint32(); + this->device_id = value; break; #endif default: @@ -2934,9 +3204,9 @@ bool DateCommandRequest::decode_32bit(uint32_t field_id, Proto32Bit value) { } #endif #ifdef USE_DATETIME_TIME -void ListEntitiesTimeResponse::encode(ProtoWriteBuffer buffer) const { +void ListEntitiesTimeResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_string(1, this->object_id); - buffer.encode_fixed32(2, this->key); + buffer.write_tag_and_fixed32(21, this->key); buffer.encode_string(3, this->name); #ifdef USE_ENTITY_ICON buffer.encode_string(5, this->icon); @@ -2947,21 +3217,23 @@ void ListEntitiesTimeResponse::encode(ProtoWriteBuffer buffer) const { buffer.encode_uint32(8, this->device_id); #endif } -void ListEntitiesTimeResponse::calculate_size(ProtoSize &size) const { - size.add_length(1, this->object_id.size()); - size.add_fixed32(1, this->key); - size.add_length(1, this->name.size()); +uint32_t ListEntitiesTimeResponse::calculate_size() const { + uint32_t size = 0; + size += ProtoSize::calc_length(1, this->object_id.size()); + size += 5; + size += ProtoSize::calc_length(1, this->name.size()); #ifdef USE_ENTITY_ICON - size.add_length(1, this->icon.size()); + size += ProtoSize::calc_length(1, this->icon.size()); #endif - size.add_bool(1, this->disabled_by_default); - size.add_uint32(1, static_cast(this->entity_category)); + size += ProtoSize::calc_bool(1, this->disabled_by_default); + size += ProtoSize::calc_uint32(1, static_cast(this->entity_category)); #ifdef USE_DEVICES - size.add_uint32(1, this->device_id); + size += ProtoSize::calc_uint32(1, this->device_id); #endif + return size; } -void TimeStateResponse::encode(ProtoWriteBuffer buffer) const { - buffer.encode_fixed32(1, this->key); +void TimeStateResponse::encode(ProtoWriteBuffer &buffer) const { + buffer.write_tag_and_fixed32(13, this->key); buffer.encode_bool(2, this->missing_state); buffer.encode_uint32(3, this->hour); buffer.encode_uint32(4, this->minute); @@ -2970,30 +3242,32 @@ void TimeStateResponse::encode(ProtoWriteBuffer buffer) const { buffer.encode_uint32(6, this->device_id); #endif } -void TimeStateResponse::calculate_size(ProtoSize &size) const { - size.add_fixed32(1, this->key); - size.add_bool(1, this->missing_state); - size.add_uint32(1, this->hour); - size.add_uint32(1, this->minute); - size.add_uint32(1, this->second); +uint32_t TimeStateResponse::calculate_size() const { + uint32_t size = 0; + size += 5; + size += ProtoSize::calc_bool(1, this->missing_state); + size += ProtoSize::calc_uint32(1, this->hour); + size += ProtoSize::calc_uint32(1, this->minute); + size += ProtoSize::calc_uint32(1, this->second); #ifdef USE_DEVICES - size.add_uint32(1, this->device_id); + size += ProtoSize::calc_uint32(1, this->device_id); #endif + return size; } -bool TimeCommandRequest::decode_varint(uint32_t field_id, ProtoVarInt value) { +bool TimeCommandRequest::decode_varint(uint32_t field_id, proto_varint_value_t value) { switch (field_id) { case 2: - this->hour = value.as_uint32(); + this->hour = value; break; case 3: - this->minute = value.as_uint32(); + this->minute = value; break; case 4: - this->second = value.as_uint32(); + this->second = value; break; #ifdef USE_DEVICES case 5: - this->device_id = value.as_uint32(); + this->device_id = value; break; #endif default: @@ -3013,9 +3287,9 @@ bool TimeCommandRequest::decode_32bit(uint32_t field_id, Proto32Bit value) { } #endif #ifdef USE_EVENT -void ListEntitiesEventResponse::encode(ProtoWriteBuffer buffer) const { +void ListEntitiesEventResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_string(1, this->object_id); - buffer.encode_fixed32(2, this->key); + buffer.write_tag_and_fixed32(21, this->key); buffer.encode_string(3, this->name); #ifdef USE_ENTITY_ICON buffer.encode_string(5, this->icon); @@ -3030,44 +3304,48 @@ void ListEntitiesEventResponse::encode(ProtoWriteBuffer buffer) const { buffer.encode_uint32(10, this->device_id); #endif } -void ListEntitiesEventResponse::calculate_size(ProtoSize &size) const { - size.add_length(1, this->object_id.size()); - size.add_fixed32(1, this->key); - size.add_length(1, this->name.size()); +uint32_t ListEntitiesEventResponse::calculate_size() const { + uint32_t size = 0; + size += ProtoSize::calc_length(1, this->object_id.size()); + size += 5; + size += ProtoSize::calc_length(1, this->name.size()); #ifdef USE_ENTITY_ICON - size.add_length(1, this->icon.size()); + size += ProtoSize::calc_length(1, this->icon.size()); #endif - size.add_bool(1, this->disabled_by_default); - size.add_uint32(1, static_cast(this->entity_category)); - size.add_length(1, this->device_class.size()); + size += ProtoSize::calc_bool(1, this->disabled_by_default); + size += ProtoSize::calc_uint32(1, static_cast(this->entity_category)); + size += ProtoSize::calc_length(1, this->device_class.size()); if (!this->event_types->empty()) { for (const char *it : *this->event_types) { - size.add_length_force(1, strlen(it)); + size += ProtoSize::calc_length_force(1, strlen(it)); } } #ifdef USE_DEVICES - size.add_uint32(1, this->device_id); + size += ProtoSize::calc_uint32(1, this->device_id); #endif + return size; } -void EventResponse::encode(ProtoWriteBuffer buffer) const { - buffer.encode_fixed32(1, this->key); +void EventResponse::encode(ProtoWriteBuffer &buffer) const { + buffer.write_tag_and_fixed32(13, this->key); buffer.encode_string(2, this->event_type); #ifdef USE_DEVICES buffer.encode_uint32(3, this->device_id); #endif } -void EventResponse::calculate_size(ProtoSize &size) const { - size.add_fixed32(1, this->key); - size.add_length(1, this->event_type.size()); +uint32_t EventResponse::calculate_size() const { + uint32_t size = 0; + size += 5; + size += ProtoSize::calc_length(1, this->event_type.size()); #ifdef USE_DEVICES - size.add_uint32(1, this->device_id); + size += ProtoSize::calc_uint32(1, this->device_id); #endif + return size; } #endif #ifdef USE_VALVE -void ListEntitiesValveResponse::encode(ProtoWriteBuffer buffer) const { +void ListEntitiesValveResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_string(1, this->object_id); - buffer.encode_fixed32(2, this->key); + buffer.write_tag_and_fixed32(21, this->key); buffer.encode_string(3, this->name); #ifdef USE_ENTITY_ICON buffer.encode_string(5, this->icon); @@ -3082,50 +3360,54 @@ void ListEntitiesValveResponse::encode(ProtoWriteBuffer buffer) const { buffer.encode_uint32(12, this->device_id); #endif } -void ListEntitiesValveResponse::calculate_size(ProtoSize &size) const { - size.add_length(1, this->object_id.size()); - size.add_fixed32(1, this->key); - size.add_length(1, this->name.size()); +uint32_t ListEntitiesValveResponse::calculate_size() const { + uint32_t size = 0; + size += ProtoSize::calc_length(1, this->object_id.size()); + size += 5; + size += ProtoSize::calc_length(1, this->name.size()); #ifdef USE_ENTITY_ICON - size.add_length(1, this->icon.size()); + size += ProtoSize::calc_length(1, this->icon.size()); #endif - size.add_bool(1, this->disabled_by_default); - size.add_uint32(1, static_cast(this->entity_category)); - size.add_length(1, this->device_class.size()); - size.add_bool(1, this->assumed_state); - size.add_bool(1, this->supports_position); - size.add_bool(1, this->supports_stop); + size += ProtoSize::calc_bool(1, this->disabled_by_default); + size += ProtoSize::calc_uint32(1, static_cast(this->entity_category)); + size += ProtoSize::calc_length(1, this->device_class.size()); + size += ProtoSize::calc_bool(1, this->assumed_state); + size += ProtoSize::calc_bool(1, this->supports_position); + size += ProtoSize::calc_bool(1, this->supports_stop); #ifdef USE_DEVICES - size.add_uint32(1, this->device_id); + size += ProtoSize::calc_uint32(1, this->device_id); #endif + return size; } -void ValveStateResponse::encode(ProtoWriteBuffer buffer) const { - buffer.encode_fixed32(1, this->key); +void ValveStateResponse::encode(ProtoWriteBuffer &buffer) const { + buffer.write_tag_and_fixed32(13, this->key); buffer.encode_float(2, this->position); buffer.encode_uint32(3, static_cast(this->current_operation)); #ifdef USE_DEVICES buffer.encode_uint32(4, this->device_id); #endif } -void ValveStateResponse::calculate_size(ProtoSize &size) const { - size.add_fixed32(1, this->key); - size.add_float(1, this->position); - size.add_uint32(1, static_cast(this->current_operation)); +uint32_t ValveStateResponse::calculate_size() const { + uint32_t size = 0; + size += 5; + size += ProtoSize::calc_float(1, this->position); + size += ProtoSize::calc_uint32(1, static_cast(this->current_operation)); #ifdef USE_DEVICES - size.add_uint32(1, this->device_id); + size += ProtoSize::calc_uint32(1, this->device_id); #endif + return size; } -bool ValveCommandRequest::decode_varint(uint32_t field_id, ProtoVarInt value) { +bool ValveCommandRequest::decode_varint(uint32_t field_id, proto_varint_value_t value) { switch (field_id) { case 2: - this->has_position = value.as_bool(); + this->has_position = value != 0; break; case 4: - this->stop = value.as_bool(); + this->stop = value != 0; break; #ifdef USE_DEVICES case 5: - this->device_id = value.as_uint32(); + this->device_id = value; break; #endif default: @@ -3148,9 +3430,9 @@ bool ValveCommandRequest::decode_32bit(uint32_t field_id, Proto32Bit value) { } #endif #ifdef USE_DATETIME_DATETIME -void ListEntitiesDateTimeResponse::encode(ProtoWriteBuffer buffer) const { +void ListEntitiesDateTimeResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_string(1, this->object_id); - buffer.encode_fixed32(2, this->key); + buffer.write_tag_and_fixed32(21, this->key); buffer.encode_string(3, this->name); #ifdef USE_ENTITY_ICON buffer.encode_string(5, this->icon); @@ -3161,40 +3443,44 @@ void ListEntitiesDateTimeResponse::encode(ProtoWriteBuffer buffer) const { buffer.encode_uint32(8, this->device_id); #endif } -void ListEntitiesDateTimeResponse::calculate_size(ProtoSize &size) const { - size.add_length(1, this->object_id.size()); - size.add_fixed32(1, this->key); - size.add_length(1, this->name.size()); +uint32_t ListEntitiesDateTimeResponse::calculate_size() const { + uint32_t size = 0; + size += ProtoSize::calc_length(1, this->object_id.size()); + size += 5; + size += ProtoSize::calc_length(1, this->name.size()); #ifdef USE_ENTITY_ICON - size.add_length(1, this->icon.size()); + size += ProtoSize::calc_length(1, this->icon.size()); #endif - size.add_bool(1, this->disabled_by_default); - size.add_uint32(1, static_cast(this->entity_category)); + size += ProtoSize::calc_bool(1, this->disabled_by_default); + size += ProtoSize::calc_uint32(1, static_cast(this->entity_category)); #ifdef USE_DEVICES - size.add_uint32(1, this->device_id); + size += ProtoSize::calc_uint32(1, this->device_id); #endif + return size; } -void DateTimeStateResponse::encode(ProtoWriteBuffer buffer) const { - buffer.encode_fixed32(1, this->key); +void DateTimeStateResponse::encode(ProtoWriteBuffer &buffer) const { + buffer.write_tag_and_fixed32(13, this->key); buffer.encode_bool(2, this->missing_state); buffer.encode_fixed32(3, this->epoch_seconds); #ifdef USE_DEVICES buffer.encode_uint32(4, this->device_id); #endif } -void DateTimeStateResponse::calculate_size(ProtoSize &size) const { - size.add_fixed32(1, this->key); - size.add_bool(1, this->missing_state); - size.add_fixed32(1, this->epoch_seconds); +uint32_t DateTimeStateResponse::calculate_size() const { + uint32_t size = 0; + size += 5; + size += ProtoSize::calc_bool(1, this->missing_state); + size += ProtoSize::calc_fixed32(1, this->epoch_seconds); #ifdef USE_DEVICES - size.add_uint32(1, this->device_id); + size += ProtoSize::calc_uint32(1, this->device_id); #endif + return size; } -bool DateTimeCommandRequest::decode_varint(uint32_t field_id, ProtoVarInt value) { +bool DateTimeCommandRequest::decode_varint(uint32_t field_id, proto_varint_value_t value) { switch (field_id) { #ifdef USE_DEVICES case 3: - this->device_id = value.as_uint32(); + this->device_id = value; break; #endif default: @@ -3217,9 +3503,9 @@ bool DateTimeCommandRequest::decode_32bit(uint32_t field_id, Proto32Bit value) { } #endif #ifdef USE_UPDATE -void ListEntitiesUpdateResponse::encode(ProtoWriteBuffer buffer) const { +void ListEntitiesUpdateResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_string(1, this->object_id); - buffer.encode_fixed32(2, this->key); + buffer.write_tag_and_fixed32(21, this->key); buffer.encode_string(3, this->name); #ifdef USE_ENTITY_ICON buffer.encode_string(5, this->icon); @@ -3231,22 +3517,24 @@ void ListEntitiesUpdateResponse::encode(ProtoWriteBuffer buffer) const { buffer.encode_uint32(9, this->device_id); #endif } -void ListEntitiesUpdateResponse::calculate_size(ProtoSize &size) const { - size.add_length(1, this->object_id.size()); - size.add_fixed32(1, this->key); - size.add_length(1, this->name.size()); +uint32_t ListEntitiesUpdateResponse::calculate_size() const { + uint32_t size = 0; + size += ProtoSize::calc_length(1, this->object_id.size()); + size += 5; + size += ProtoSize::calc_length(1, this->name.size()); #ifdef USE_ENTITY_ICON - size.add_length(1, this->icon.size()); + size += ProtoSize::calc_length(1, this->icon.size()); #endif - size.add_bool(1, this->disabled_by_default); - size.add_uint32(1, static_cast(this->entity_category)); - size.add_length(1, this->device_class.size()); + size += ProtoSize::calc_bool(1, this->disabled_by_default); + size += ProtoSize::calc_uint32(1, static_cast(this->entity_category)); + size += ProtoSize::calc_length(1, this->device_class.size()); #ifdef USE_DEVICES - size.add_uint32(1, this->device_id); + size += ProtoSize::calc_uint32(1, this->device_id); #endif + return size; } -void UpdateStateResponse::encode(ProtoWriteBuffer buffer) const { - buffer.encode_fixed32(1, this->key); +void UpdateStateResponse::encode(ProtoWriteBuffer &buffer) const { + buffer.write_tag_and_fixed32(13, this->key); buffer.encode_bool(2, this->missing_state); buffer.encode_bool(3, this->in_progress); buffer.encode_bool(4, this->has_progress); @@ -3260,29 +3548,31 @@ void UpdateStateResponse::encode(ProtoWriteBuffer buffer) const { buffer.encode_uint32(11, this->device_id); #endif } -void UpdateStateResponse::calculate_size(ProtoSize &size) const { - size.add_fixed32(1, this->key); - size.add_bool(1, this->missing_state); - size.add_bool(1, this->in_progress); - size.add_bool(1, this->has_progress); - size.add_float(1, this->progress); - size.add_length(1, this->current_version.size()); - size.add_length(1, this->latest_version.size()); - size.add_length(1, this->title.size()); - size.add_length(1, this->release_summary.size()); - size.add_length(1, this->release_url.size()); +uint32_t UpdateStateResponse::calculate_size() const { + uint32_t size = 0; + size += 5; + size += ProtoSize::calc_bool(1, this->missing_state); + size += ProtoSize::calc_bool(1, this->in_progress); + size += ProtoSize::calc_bool(1, this->has_progress); + size += ProtoSize::calc_float(1, this->progress); + size += ProtoSize::calc_length(1, this->current_version.size()); + size += ProtoSize::calc_length(1, this->latest_version.size()); + size += ProtoSize::calc_length(1, this->title.size()); + size += ProtoSize::calc_length(1, this->release_summary.size()); + size += ProtoSize::calc_length(1, this->release_url.size()); #ifdef USE_DEVICES - size.add_uint32(1, this->device_id); + size += ProtoSize::calc_uint32(1, this->device_id); #endif + return size; } -bool UpdateCommandRequest::decode_varint(uint32_t field_id, ProtoVarInt value) { +bool UpdateCommandRequest::decode_varint(uint32_t field_id, proto_varint_value_t value) { switch (field_id) { case 2: - this->command = static_cast(value.as_uint32()); + this->command = static_cast(value); break; #ifdef USE_DEVICES case 3: - this->device_id = value.as_uint32(); + this->device_id = value; break; #endif default: @@ -3314,12 +3604,16 @@ bool ZWaveProxyFrame::decode_length(uint32_t field_id, ProtoLengthDelimited valu } return true; } -void ZWaveProxyFrame::encode(ProtoWriteBuffer buffer) const { buffer.encode_bytes(1, this->data, this->data_len); } -void ZWaveProxyFrame::calculate_size(ProtoSize &size) const { size.add_length(1, this->data_len); } -bool ZWaveProxyRequest::decode_varint(uint32_t field_id, ProtoVarInt value) { +void ZWaveProxyFrame::encode(ProtoWriteBuffer &buffer) const { buffer.encode_bytes(1, this->data, this->data_len); } +uint32_t ZWaveProxyFrame::calculate_size() const { + uint32_t size = 0; + size += ProtoSize::calc_length(1, this->data_len); + return size; +} +bool ZWaveProxyRequest::decode_varint(uint32_t field_id, proto_varint_value_t value) { switch (field_id) { case 1: - this->type = static_cast(value.as_uint32()); + this->type = static_cast(value); break; default: return false; @@ -3338,19 +3632,21 @@ bool ZWaveProxyRequest::decode_length(uint32_t field_id, ProtoLengthDelimited va } return true; } -void ZWaveProxyRequest::encode(ProtoWriteBuffer buffer) const { +void ZWaveProxyRequest::encode(ProtoWriteBuffer &buffer) const { buffer.encode_uint32(1, static_cast(this->type)); buffer.encode_bytes(2, this->data, this->data_len); } -void ZWaveProxyRequest::calculate_size(ProtoSize &size) const { - size.add_uint32(1, static_cast(this->type)); - size.add_length(1, this->data_len); +uint32_t ZWaveProxyRequest::calculate_size() const { + uint32_t size = 0; + size += ProtoSize::calc_uint32(1, static_cast(this->type)); + size += ProtoSize::calc_length(1, this->data_len); + return size; } #endif #ifdef USE_INFRARED -void ListEntitiesInfraredResponse::encode(ProtoWriteBuffer buffer) const { +void ListEntitiesInfraredResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_string(1, this->object_id); - buffer.encode_fixed32(2, this->key); + buffer.write_tag_and_fixed32(21, this->key); buffer.encode_string(3, this->name); #ifdef USE_ENTITY_ICON buffer.encode_string(4, this->icon); @@ -3361,35 +3657,39 @@ void ListEntitiesInfraredResponse::encode(ProtoWriteBuffer buffer) const { buffer.encode_uint32(7, this->device_id); #endif buffer.encode_uint32(8, this->capabilities); + buffer.encode_uint32(9, this->receiver_frequency); } -void ListEntitiesInfraredResponse::calculate_size(ProtoSize &size) const { - size.add_length(1, this->object_id.size()); - size.add_fixed32(1, this->key); - size.add_length(1, this->name.size()); +uint32_t ListEntitiesInfraredResponse::calculate_size() const { + uint32_t size = 0; + size += ProtoSize::calc_length(1, this->object_id.size()); + size += 5; + size += ProtoSize::calc_length(1, this->name.size()); #ifdef USE_ENTITY_ICON - size.add_length(1, this->icon.size()); + size += ProtoSize::calc_length(1, this->icon.size()); #endif - size.add_bool(1, this->disabled_by_default); - size.add_uint32(1, static_cast(this->entity_category)); + size += ProtoSize::calc_bool(1, this->disabled_by_default); + size += ProtoSize::calc_uint32(1, static_cast(this->entity_category)); #ifdef USE_DEVICES - size.add_uint32(1, this->device_id); + size += ProtoSize::calc_uint32(1, this->device_id); #endif - size.add_uint32(1, this->capabilities); + size += ProtoSize::calc_uint32(1, this->capabilities); + size += ProtoSize::calc_uint32(1, this->receiver_frequency); + return size; } #endif #ifdef USE_IR_RF -bool InfraredRFTransmitRawTimingsRequest::decode_varint(uint32_t field_id, ProtoVarInt value) { +bool InfraredRFTransmitRawTimingsRequest::decode_varint(uint32_t field_id, proto_varint_value_t value) { switch (field_id) { #ifdef USE_DEVICES case 1: - this->device_id = value.as_uint32(); + this->device_id = value; break; #endif case 3: - this->carrier_frequency = value.as_uint32(); + this->carrier_frequency = value; break; case 4: - this->repeat_count = value.as_uint32(); + this->repeat_count = value; break; default: return false; @@ -3419,25 +3719,180 @@ bool InfraredRFTransmitRawTimingsRequest::decode_32bit(uint32_t field_id, Proto3 } return true; } -void InfraredRFReceiveEvent::encode(ProtoWriteBuffer buffer) const { +void InfraredRFReceiveEvent::encode(ProtoWriteBuffer &buffer) const { #ifdef USE_DEVICES buffer.encode_uint32(1, this->device_id); #endif - buffer.encode_fixed32(2, this->key); + buffer.write_tag_and_fixed32(21, this->key); for (const auto &it : *this->timings) { buffer.encode_sint32(3, it, true); } } -void InfraredRFReceiveEvent::calculate_size(ProtoSize &size) const { +uint32_t InfraredRFReceiveEvent::calculate_size() const { + uint32_t size = 0; #ifdef USE_DEVICES - size.add_uint32(1, this->device_id); + size += ProtoSize::calc_uint32(1, this->device_id); #endif - size.add_fixed32(1, this->key); + size += 5; if (!this->timings->empty()) { for (const auto &it : *this->timings) { - size.add_sint32_force(1, it); + size += ProtoSize::calc_sint32_force(1, it); } } + return size; +} +#endif +#ifdef USE_SERIAL_PROXY +bool SerialProxyConfigureRequest::decode_varint(uint32_t field_id, proto_varint_value_t value) { + switch (field_id) { + case 1: + this->instance = value; + break; + case 2: + this->baudrate = value; + break; + case 3: + this->flow_control = value != 0; + break; + case 4: + this->parity = static_cast(value); + break; + case 5: + this->stop_bits = value; + break; + case 6: + this->data_size = value; + break; + default: + return false; + } + return true; +} +void SerialProxyDataReceived::encode(ProtoWriteBuffer &buffer) const { + buffer.encode_uint32(1, this->instance); + buffer.encode_bytes(2, this->data_ptr_, this->data_len_); +} +uint32_t SerialProxyDataReceived::calculate_size() const { + uint32_t size = 0; + size += ProtoSize::calc_uint32(1, this->instance); + size += ProtoSize::calc_length(1, this->data_len_); + return size; +} +bool SerialProxyWriteRequest::decode_varint(uint32_t field_id, proto_varint_value_t value) { + switch (field_id) { + case 1: + this->instance = value; + break; + default: + return false; + } + return true; +} +bool SerialProxyWriteRequest::decode_length(uint32_t field_id, ProtoLengthDelimited value) { + switch (field_id) { + case 2: { + this->data = value.data(); + this->data_len = value.size(); + break; + } + default: + return false; + } + return true; +} +bool SerialProxySetModemPinsRequest::decode_varint(uint32_t field_id, proto_varint_value_t value) { + switch (field_id) { + case 1: + this->instance = value; + break; + case 2: + this->line_states = value; + break; + default: + return false; + } + return true; +} +bool SerialProxyGetModemPinsRequest::decode_varint(uint32_t field_id, proto_varint_value_t value) { + switch (field_id) { + case 1: + this->instance = value; + break; + default: + return false; + } + return true; +} +void SerialProxyGetModemPinsResponse::encode(ProtoWriteBuffer &buffer) const { + buffer.encode_uint32(1, this->instance); + buffer.encode_uint32(2, this->line_states); +} +uint32_t SerialProxyGetModemPinsResponse::calculate_size() const { + uint32_t size = 0; + size += ProtoSize::calc_uint32(1, this->instance); + size += ProtoSize::calc_uint32(1, this->line_states); + return size; +} +bool SerialProxyRequest::decode_varint(uint32_t field_id, proto_varint_value_t value) { + switch (field_id) { + case 1: + this->instance = value; + break; + case 2: + this->type = static_cast(value); + break; + default: + return false; + } + return true; +} +void SerialProxyRequestResponse::encode(ProtoWriteBuffer &buffer) const { + buffer.encode_uint32(1, this->instance); + buffer.encode_uint32(2, static_cast(this->type)); + buffer.encode_uint32(3, static_cast(this->status)); + buffer.encode_string(4, this->error_message); +} +uint32_t SerialProxyRequestResponse::calculate_size() const { + uint32_t size = 0; + size += ProtoSize::calc_uint32(1, this->instance); + size += ProtoSize::calc_uint32(1, static_cast(this->type)); + size += ProtoSize::calc_uint32(1, static_cast(this->status)); + size += ProtoSize::calc_length(1, this->error_message.size()); + return size; +} +#endif +#ifdef USE_BLUETOOTH_PROXY +bool BluetoothSetConnectionParamsRequest::decode_varint(uint32_t field_id, proto_varint_value_t value) { + switch (field_id) { + case 1: + this->address = value; + break; + case 2: + this->min_interval = value; + break; + case 3: + this->max_interval = value; + break; + case 4: + this->latency = value; + break; + case 5: + this->timeout = value; + break; + default: + return false; + } + return true; +} +void BluetoothSetConnectionParamsResponse::encode(ProtoWriteBuffer &buffer) const { + buffer.encode_uint64(1, this->address); + buffer.encode_int32(2, this->error); +} +uint32_t BluetoothSetConnectionParamsResponse::calculate_size() const { + uint32_t size = 0; + size += ProtoSize::calc_uint64(1, this->address); + size += ProtoSize::calc_int32(1, this->error); + return size; } #endif diff --git a/esphome/components/api/api_pb2.h b/esphome/components/api/api_pb2.h index cf6c65f285..14f6c704ae 100644 --- a/esphome/components/api/api_pb2.h +++ b/esphome/components/api/api_pb2.h @@ -2,7 +2,6 @@ // See script/api_protobuf/api_protobuf.py #pragma once -#include "esphome/core/defines.h" #include "esphome/core/string_ref.h" #include "proto.h" @@ -12,6 +11,11 @@ namespace esphome::api { namespace enums { +enum SerialProxyPortType : uint32_t { + SERIAL_PROXY_PORT_TYPE_TTL = 0, + SERIAL_PROXY_PORT_TYPE_RS232 = 1, + SERIAL_PROXY_PORT_TYPE_RS485 = 2, +}; enum EntityCategory : uint32_t { ENTITY_CATEGORY_NONE = 0, ENTITY_CATEGORY_CONFIG = 1, @@ -64,6 +68,12 @@ enum LogLevel : uint32_t { LOG_LEVEL_VERBOSE = 6, LOG_LEVEL_VERY_VERBOSE = 7, }; +enum DSTRuleType : uint32_t { + DST_RULE_TYPE_NONE = 0, + DST_RULE_TYPE_MONTH_WEEK_DAY = 1, + DST_RULE_TYPE_JULIAN_NO_LEAP = 2, + DST_RULE_TYPE_DAY_OF_YEAR = 3, +}; #ifdef USE_API_USER_DEFINED_ACTIONS enum ServiceArgType : uint32_t { SERVICE_ARG_TYPE_BOOL = 0, @@ -117,6 +127,7 @@ enum ClimateAction : uint32_t { CLIMATE_ACTION_IDLE = 4, CLIMATE_ACTION_DRYING = 5, CLIMATE_ACTION_FAN = 6, + CLIMATE_ACTION_DEFROSTING = 7, }; enum ClimatePreset : uint32_t { CLIMATE_PRESET_NONE = 0, @@ -147,6 +158,8 @@ enum WaterHeaterCommandHasField : uint32_t { WATER_HEATER_COMMAND_HAS_STATE = 4, WATER_HEATER_COMMAND_HAS_TARGET_TEMPERATURE_LOW = 8, WATER_HEATER_COMMAND_HAS_TARGET_TEMPERATURE_HIGH = 16, + WATER_HEATER_COMMAND_HAS_ON_STATE = 32, + WATER_HEATER_COMMAND_HAS_AWAY_STATE = 64, }; #ifdef USE_NUMBER enum NumberMode : uint32_t { @@ -309,12 +322,30 @@ enum ZWaveProxyRequestType : uint32_t { ZWAVE_PROXY_REQUEST_TYPE_HOME_ID_CHANGE = 2, }; #endif +#ifdef USE_SERIAL_PROXY +enum SerialProxyParity : uint32_t { + SERIAL_PROXY_PARITY_NONE = 0, + SERIAL_PROXY_PARITY_EVEN = 1, + SERIAL_PROXY_PARITY_ODD = 2, +}; +enum SerialProxyRequestType : uint32_t { + SERIAL_PROXY_REQUEST_TYPE_SUBSCRIBE = 0, + SERIAL_PROXY_REQUEST_TYPE_UNSUBSCRIBE = 1, + SERIAL_PROXY_REQUEST_TYPE_FLUSH = 2, +}; +enum SerialProxyStatus : uint32_t { + SERIAL_PROXY_STATUS_OK = 0, + SERIAL_PROXY_STATUS_ASSUMED_SUCCESS = 1, + SERIAL_PROXY_STATUS_ERROR = 2, + SERIAL_PROXY_STATUS_TIMEOUT = 3, + SERIAL_PROXY_STATUS_NOT_SUPPORTED = 4, +}; +#endif } // namespace enums class InfoResponseProtoMessage : public ProtoMessage { public: - ~InfoResponseProtoMessage() override = default; StringRef object_id{}; uint32_t key{0}; StringRef name{}; @@ -328,35 +359,36 @@ class InfoResponseProtoMessage : public ProtoMessage { #endif protected: + ~InfoResponseProtoMessage() = default; }; class StateResponseProtoMessage : public ProtoMessage { public: - ~StateResponseProtoMessage() override = default; uint32_t key{0}; #ifdef USE_DEVICES uint32_t device_id{0}; #endif protected: + ~StateResponseProtoMessage() = default; }; class CommandProtoMessage : public ProtoDecodableMessage { public: - ~CommandProtoMessage() override = default; uint32_t key{0}; #ifdef USE_DEVICES uint32_t device_id{0}; #endif protected: + ~CommandProtoMessage() = default; }; class HelloRequest final : public ProtoDecodableMessage { public: static constexpr uint8_t MESSAGE_TYPE = 1; static constexpr uint8_t ESTIMATED_SIZE = 17; #ifdef HAS_PROTO_MESSAGE_DUMP - const char *message_name() const override { return "hello_request"; } + const LogString *message_name() const override { return LOG_STR("hello_request"); } #endif StringRef client_info{}; uint32_t api_version_major{0}; @@ -367,21 +399,21 @@ class HelloRequest final : public ProtoDecodableMessage { protected: bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override; - bool decode_varint(uint32_t field_id, ProtoVarInt value) override; + bool decode_varint(uint32_t field_id, proto_varint_value_t value) override; }; class HelloResponse final : public ProtoMessage { public: static constexpr uint8_t MESSAGE_TYPE = 2; static constexpr uint8_t ESTIMATED_SIZE = 26; #ifdef HAS_PROTO_MESSAGE_DUMP - const char *message_name() const override { return "hello_response"; } + const LogString *message_name() const override { return LOG_STR("hello_response"); } #endif uint32_t api_version_major{0}; uint32_t api_version_minor{0}; StringRef server_info{}; StringRef name{}; - void encode(ProtoWriteBuffer buffer) const override; - void calculate_size(ProtoSize &size) const override; + void encode(ProtoWriteBuffer &buffer) const; + uint32_t calculate_size() const; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; #endif @@ -393,7 +425,7 @@ class DisconnectRequest final : public ProtoMessage { static constexpr uint8_t MESSAGE_TYPE = 5; static constexpr uint8_t ESTIMATED_SIZE = 0; #ifdef HAS_PROTO_MESSAGE_DUMP - const char *message_name() const override { return "disconnect_request"; } + const LogString *message_name() const override { return LOG_STR("disconnect_request"); } #endif #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; @@ -406,7 +438,7 @@ class DisconnectResponse final : public ProtoMessage { static constexpr uint8_t MESSAGE_TYPE = 6; static constexpr uint8_t ESTIMATED_SIZE = 0; #ifdef HAS_PROTO_MESSAGE_DUMP - const char *message_name() const override { return "disconnect_response"; } + const LogString *message_name() const override { return LOG_STR("disconnect_response"); } #endif #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; @@ -419,7 +451,7 @@ class PingRequest final : public ProtoMessage { static constexpr uint8_t MESSAGE_TYPE = 7; static constexpr uint8_t ESTIMATED_SIZE = 0; #ifdef HAS_PROTO_MESSAGE_DUMP - const char *message_name() const override { return "ping_request"; } + const LogString *message_name() const override { return LOG_STR("ping_request"); } #endif #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; @@ -432,20 +464,7 @@ class PingResponse final : public ProtoMessage { static constexpr uint8_t MESSAGE_TYPE = 8; static constexpr uint8_t ESTIMATED_SIZE = 0; #ifdef HAS_PROTO_MESSAGE_DUMP - const char *message_name() const override { return "ping_response"; } -#endif -#ifdef HAS_PROTO_MESSAGE_DUMP - const char *dump_to(DumpBuffer &out) const override; -#endif - - protected: -}; -class DeviceInfoRequest final : public ProtoMessage { - public: - static constexpr uint8_t MESSAGE_TYPE = 9; - static constexpr uint8_t ESTIMATED_SIZE = 0; -#ifdef HAS_PROTO_MESSAGE_DUMP - const char *message_name() const override { return "device_info_request"; } + const LogString *message_name() const override { return LOG_STR("ping_response"); } #endif #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; @@ -458,8 +477,8 @@ class AreaInfo final : public ProtoMessage { public: uint32_t area_id{0}; StringRef name{}; - void encode(ProtoWriteBuffer buffer) const override; - void calculate_size(ProtoSize &size) const override; + void encode(ProtoWriteBuffer &buffer) const; + uint32_t calculate_size() const; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; #endif @@ -473,8 +492,22 @@ class DeviceInfo final : public ProtoMessage { uint32_t device_id{0}; StringRef name{}; uint32_t area_id{0}; - void encode(ProtoWriteBuffer buffer) const override; - void calculate_size(ProtoSize &size) const override; + void encode(ProtoWriteBuffer &buffer) const; + uint32_t calculate_size() const; +#ifdef HAS_PROTO_MESSAGE_DUMP + const char *dump_to(DumpBuffer &out) const override; +#endif + + protected: +}; +#endif +#ifdef USE_SERIAL_PROXY +class SerialProxyInfo final : public ProtoMessage { + public: + StringRef name{}; + enums::SerialProxyPortType port_type{}; + void encode(ProtoWriteBuffer &buffer) const; + uint32_t calculate_size() const; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; #endif @@ -485,9 +518,9 @@ class DeviceInfo final : public ProtoMessage { class DeviceInfoResponse final : public ProtoMessage { public: static constexpr uint8_t MESSAGE_TYPE = 10; - static constexpr uint8_t ESTIMATED_SIZE = 255; + static constexpr uint16_t ESTIMATED_SIZE = 309; #ifdef HAS_PROTO_MESSAGE_DUMP - const char *message_name() const override { return "device_info_response"; } + const LogString *message_name() const override { return LOG_STR("device_info_response"); } #endif StringRef name{}; StringRef mac_address{}; @@ -538,21 +571,11 @@ class DeviceInfoResponse final : public ProtoMessage { #ifdef USE_ZWAVE_PROXY uint32_t zwave_home_id{0}; #endif - void encode(ProtoWriteBuffer buffer) const override; - void calculate_size(ProtoSize &size) const override; -#ifdef HAS_PROTO_MESSAGE_DUMP - const char *dump_to(DumpBuffer &out) const override; -#endif - - protected: -}; -class ListEntitiesRequest final : public ProtoMessage { - public: - static constexpr uint8_t MESSAGE_TYPE = 11; - static constexpr uint8_t ESTIMATED_SIZE = 0; -#ifdef HAS_PROTO_MESSAGE_DUMP - const char *message_name() const override { return "list_entities_request"; } +#ifdef USE_SERIAL_PROXY + std::array serial_proxies{}; #endif + void encode(ProtoWriteBuffer &buffer) const; + uint32_t calculate_size() const; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; #endif @@ -564,20 +587,7 @@ class ListEntitiesDoneResponse final : public ProtoMessage { static constexpr uint8_t MESSAGE_TYPE = 19; static constexpr uint8_t ESTIMATED_SIZE = 0; #ifdef HAS_PROTO_MESSAGE_DUMP - const char *message_name() const override { return "list_entities_done_response"; } -#endif -#ifdef HAS_PROTO_MESSAGE_DUMP - const char *dump_to(DumpBuffer &out) const override; -#endif - - protected: -}; -class SubscribeStatesRequest final : public ProtoMessage { - public: - static constexpr uint8_t MESSAGE_TYPE = 20; - static constexpr uint8_t ESTIMATED_SIZE = 0; -#ifdef HAS_PROTO_MESSAGE_DUMP - const char *message_name() const override { return "subscribe_states_request"; } + const LogString *message_name() const override { return LOG_STR("list_entities_done_response"); } #endif #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; @@ -591,12 +601,12 @@ class ListEntitiesBinarySensorResponse final : public InfoResponseProtoMessage { static constexpr uint8_t MESSAGE_TYPE = 12; static constexpr uint8_t ESTIMATED_SIZE = 51; #ifdef HAS_PROTO_MESSAGE_DUMP - const char *message_name() const override { return "list_entities_binary_sensor_response"; } + const LogString *message_name() const override { return LOG_STR("list_entities_binary_sensor_response"); } #endif StringRef device_class{}; bool is_status_binary_sensor{false}; - void encode(ProtoWriteBuffer buffer) const override; - void calculate_size(ProtoSize &size) const override; + void encode(ProtoWriteBuffer &buffer) const; + uint32_t calculate_size() const; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; #endif @@ -608,12 +618,12 @@ class BinarySensorStateResponse final : public StateResponseProtoMessage { static constexpr uint8_t MESSAGE_TYPE = 21; static constexpr uint8_t ESTIMATED_SIZE = 13; #ifdef HAS_PROTO_MESSAGE_DUMP - const char *message_name() const override { return "binary_sensor_state_response"; } + const LogString *message_name() const override { return LOG_STR("binary_sensor_state_response"); } #endif bool state{false}; bool missing_state{false}; - void encode(ProtoWriteBuffer buffer) const override; - void calculate_size(ProtoSize &size) const override; + void encode(ProtoWriteBuffer &buffer) const; + uint32_t calculate_size() const; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; #endif @@ -627,15 +637,15 @@ class ListEntitiesCoverResponse final : public InfoResponseProtoMessage { static constexpr uint8_t MESSAGE_TYPE = 13; static constexpr uint8_t ESTIMATED_SIZE = 57; #ifdef HAS_PROTO_MESSAGE_DUMP - const char *message_name() const override { return "list_entities_cover_response"; } + const LogString *message_name() const override { return LOG_STR("list_entities_cover_response"); } #endif bool assumed_state{false}; bool supports_position{false}; bool supports_tilt{false}; StringRef device_class{}; bool supports_stop{false}; - void encode(ProtoWriteBuffer buffer) const override; - void calculate_size(ProtoSize &size) const override; + void encode(ProtoWriteBuffer &buffer) const; + uint32_t calculate_size() const; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; #endif @@ -647,13 +657,13 @@ class CoverStateResponse final : public StateResponseProtoMessage { static constexpr uint8_t MESSAGE_TYPE = 22; static constexpr uint8_t ESTIMATED_SIZE = 21; #ifdef HAS_PROTO_MESSAGE_DUMP - const char *message_name() const override { return "cover_state_response"; } + const LogString *message_name() const override { return LOG_STR("cover_state_response"); } #endif float position{0.0f}; float tilt{0.0f}; enums::CoverOperation current_operation{}; - void encode(ProtoWriteBuffer buffer) const override; - void calculate_size(ProtoSize &size) const override; + void encode(ProtoWriteBuffer &buffer) const; + uint32_t calculate_size() const; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; #endif @@ -665,7 +675,7 @@ class CoverCommandRequest final : public CommandProtoMessage { static constexpr uint8_t MESSAGE_TYPE = 30; static constexpr uint8_t ESTIMATED_SIZE = 25; #ifdef HAS_PROTO_MESSAGE_DUMP - const char *message_name() const override { return "cover_command_request"; } + const LogString *message_name() const override { return LOG_STR("cover_command_request"); } #endif bool has_position{false}; float position{0.0f}; @@ -678,7 +688,7 @@ class CoverCommandRequest final : public CommandProtoMessage { protected: bool decode_32bit(uint32_t field_id, Proto32Bit value) override; - bool decode_varint(uint32_t field_id, ProtoVarInt value) override; + bool decode_varint(uint32_t field_id, proto_varint_value_t value) override; }; #endif #ifdef USE_FAN @@ -687,15 +697,15 @@ class ListEntitiesFanResponse final : public InfoResponseProtoMessage { static constexpr uint8_t MESSAGE_TYPE = 14; static constexpr uint8_t ESTIMATED_SIZE = 68; #ifdef HAS_PROTO_MESSAGE_DUMP - const char *message_name() const override { return "list_entities_fan_response"; } + const LogString *message_name() const override { return LOG_STR("list_entities_fan_response"); } #endif bool supports_oscillation{false}; bool supports_speed{false}; bool supports_direction{false}; int32_t supported_speed_count{0}; const std::vector *supported_preset_modes{}; - void encode(ProtoWriteBuffer buffer) const override; - void calculate_size(ProtoSize &size) const override; + void encode(ProtoWriteBuffer &buffer) const; + uint32_t calculate_size() const; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; #endif @@ -707,15 +717,15 @@ class FanStateResponse final : public StateResponseProtoMessage { static constexpr uint8_t MESSAGE_TYPE = 23; static constexpr uint8_t ESTIMATED_SIZE = 28; #ifdef HAS_PROTO_MESSAGE_DUMP - const char *message_name() const override { return "fan_state_response"; } + const LogString *message_name() const override { return LOG_STR("fan_state_response"); } #endif bool state{false}; bool oscillating{false}; enums::FanDirection direction{}; int32_t speed_level{0}; StringRef preset_mode{}; - void encode(ProtoWriteBuffer buffer) const override; - void calculate_size(ProtoSize &size) const override; + void encode(ProtoWriteBuffer &buffer) const; + uint32_t calculate_size() const; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; #endif @@ -727,7 +737,7 @@ class FanCommandRequest final : public CommandProtoMessage { static constexpr uint8_t MESSAGE_TYPE = 31; static constexpr uint8_t ESTIMATED_SIZE = 38; #ifdef HAS_PROTO_MESSAGE_DUMP - const char *message_name() const override { return "fan_command_request"; } + const LogString *message_name() const override { return LOG_STR("fan_command_request"); } #endif bool has_state{false}; bool state{false}; @@ -746,7 +756,7 @@ class FanCommandRequest final : public CommandProtoMessage { protected: bool decode_32bit(uint32_t field_id, Proto32Bit value) override; bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override; - bool decode_varint(uint32_t field_id, ProtoVarInt value) override; + bool decode_varint(uint32_t field_id, proto_varint_value_t value) override; }; #endif #ifdef USE_LIGHT @@ -755,14 +765,14 @@ class ListEntitiesLightResponse final : public InfoResponseProtoMessage { static constexpr uint8_t MESSAGE_TYPE = 15; static constexpr uint8_t ESTIMATED_SIZE = 73; #ifdef HAS_PROTO_MESSAGE_DUMP - const char *message_name() const override { return "list_entities_light_response"; } + const LogString *message_name() const override { return LOG_STR("list_entities_light_response"); } #endif const light::ColorModeMask *supported_color_modes{}; float min_mireds{0.0f}; float max_mireds{0.0f}; const FixedVector *effects{}; - void encode(ProtoWriteBuffer buffer) const override; - void calculate_size(ProtoSize &size) const override; + void encode(ProtoWriteBuffer &buffer) const; + uint32_t calculate_size() const; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; #endif @@ -774,7 +784,7 @@ class LightStateResponse final : public StateResponseProtoMessage { static constexpr uint8_t MESSAGE_TYPE = 24; static constexpr uint8_t ESTIMATED_SIZE = 67; #ifdef HAS_PROTO_MESSAGE_DUMP - const char *message_name() const override { return "light_state_response"; } + const LogString *message_name() const override { return LOG_STR("light_state_response"); } #endif bool state{false}; float brightness{0.0f}; @@ -788,8 +798,8 @@ class LightStateResponse final : public StateResponseProtoMessage { float cold_white{0.0f}; float warm_white{0.0f}; StringRef effect{}; - void encode(ProtoWriteBuffer buffer) const override; - void calculate_size(ProtoSize &size) const override; + void encode(ProtoWriteBuffer &buffer) const; + uint32_t calculate_size() const; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; #endif @@ -801,7 +811,7 @@ class LightCommandRequest final : public CommandProtoMessage { static constexpr uint8_t MESSAGE_TYPE = 32; static constexpr uint8_t ESTIMATED_SIZE = 112; #ifdef HAS_PROTO_MESSAGE_DUMP - const char *message_name() const override { return "light_command_request"; } + const LogString *message_name() const override { return LOG_STR("light_command_request"); } #endif bool has_state{false}; bool state{false}; @@ -836,7 +846,7 @@ class LightCommandRequest final : public CommandProtoMessage { protected: bool decode_32bit(uint32_t field_id, Proto32Bit value) override; bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override; - bool decode_varint(uint32_t field_id, ProtoVarInt value) override; + bool decode_varint(uint32_t field_id, proto_varint_value_t value) override; }; #endif #ifdef USE_SENSOR @@ -845,15 +855,15 @@ class ListEntitiesSensorResponse final : public InfoResponseProtoMessage { static constexpr uint8_t MESSAGE_TYPE = 16; static constexpr uint8_t ESTIMATED_SIZE = 66; #ifdef HAS_PROTO_MESSAGE_DUMP - const char *message_name() const override { return "list_entities_sensor_response"; } + const LogString *message_name() const override { return LOG_STR("list_entities_sensor_response"); } #endif StringRef unit_of_measurement{}; int32_t accuracy_decimals{0}; bool force_update{false}; StringRef device_class{}; enums::SensorStateClass state_class{}; - void encode(ProtoWriteBuffer buffer) const override; - void calculate_size(ProtoSize &size) const override; + void encode(ProtoWriteBuffer &buffer) const; + uint32_t calculate_size() const; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; #endif @@ -865,12 +875,12 @@ class SensorStateResponse final : public StateResponseProtoMessage { static constexpr uint8_t MESSAGE_TYPE = 25; static constexpr uint8_t ESTIMATED_SIZE = 16; #ifdef HAS_PROTO_MESSAGE_DUMP - const char *message_name() const override { return "sensor_state_response"; } + const LogString *message_name() const override { return LOG_STR("sensor_state_response"); } #endif float state{0.0f}; bool missing_state{false}; - void encode(ProtoWriteBuffer buffer) const override; - void calculate_size(ProtoSize &size) const override; + void encode(ProtoWriteBuffer &buffer) const; + uint32_t calculate_size() const; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; #endif @@ -884,12 +894,12 @@ class ListEntitiesSwitchResponse final : public InfoResponseProtoMessage { static constexpr uint8_t MESSAGE_TYPE = 17; static constexpr uint8_t ESTIMATED_SIZE = 51; #ifdef HAS_PROTO_MESSAGE_DUMP - const char *message_name() const override { return "list_entities_switch_response"; } + const LogString *message_name() const override { return LOG_STR("list_entities_switch_response"); } #endif bool assumed_state{false}; StringRef device_class{}; - void encode(ProtoWriteBuffer buffer) const override; - void calculate_size(ProtoSize &size) const override; + void encode(ProtoWriteBuffer &buffer) const; + uint32_t calculate_size() const; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; #endif @@ -901,11 +911,11 @@ class SwitchStateResponse final : public StateResponseProtoMessage { static constexpr uint8_t MESSAGE_TYPE = 26; static constexpr uint8_t ESTIMATED_SIZE = 11; #ifdef HAS_PROTO_MESSAGE_DUMP - const char *message_name() const override { return "switch_state_response"; } + const LogString *message_name() const override { return LOG_STR("switch_state_response"); } #endif bool state{false}; - void encode(ProtoWriteBuffer buffer) const override; - void calculate_size(ProtoSize &size) const override; + void encode(ProtoWriteBuffer &buffer) const; + uint32_t calculate_size() const; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; #endif @@ -917,7 +927,7 @@ class SwitchCommandRequest final : public CommandProtoMessage { static constexpr uint8_t MESSAGE_TYPE = 33; static constexpr uint8_t ESTIMATED_SIZE = 11; #ifdef HAS_PROTO_MESSAGE_DUMP - const char *message_name() const override { return "switch_command_request"; } + const LogString *message_name() const override { return LOG_STR("switch_command_request"); } #endif bool state{false}; #ifdef HAS_PROTO_MESSAGE_DUMP @@ -926,7 +936,7 @@ class SwitchCommandRequest final : public CommandProtoMessage { protected: bool decode_32bit(uint32_t field_id, Proto32Bit value) override; - bool decode_varint(uint32_t field_id, ProtoVarInt value) override; + bool decode_varint(uint32_t field_id, proto_varint_value_t value) override; }; #endif #ifdef USE_TEXT_SENSOR @@ -935,11 +945,11 @@ class ListEntitiesTextSensorResponse final : public InfoResponseProtoMessage { static constexpr uint8_t MESSAGE_TYPE = 18; static constexpr uint8_t ESTIMATED_SIZE = 49; #ifdef HAS_PROTO_MESSAGE_DUMP - const char *message_name() const override { return "list_entities_text_sensor_response"; } + const LogString *message_name() const override { return LOG_STR("list_entities_text_sensor_response"); } #endif StringRef device_class{}; - void encode(ProtoWriteBuffer buffer) const override; - void calculate_size(ProtoSize &size) const override; + void encode(ProtoWriteBuffer &buffer) const; + uint32_t calculate_size() const; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; #endif @@ -951,12 +961,12 @@ class TextSensorStateResponse final : public StateResponseProtoMessage { static constexpr uint8_t MESSAGE_TYPE = 27; static constexpr uint8_t ESTIMATED_SIZE = 20; #ifdef HAS_PROTO_MESSAGE_DUMP - const char *message_name() const override { return "text_sensor_state_response"; } + const LogString *message_name() const override { return LOG_STR("text_sensor_state_response"); } #endif StringRef state{}; bool missing_state{false}; - void encode(ProtoWriteBuffer buffer) const override; - void calculate_size(ProtoSize &size) const override; + void encode(ProtoWriteBuffer &buffer) const; + uint32_t calculate_size() const; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; #endif @@ -969,7 +979,7 @@ class SubscribeLogsRequest final : public ProtoDecodableMessage { static constexpr uint8_t MESSAGE_TYPE = 28; static constexpr uint8_t ESTIMATED_SIZE = 4; #ifdef HAS_PROTO_MESSAGE_DUMP - const char *message_name() const override { return "subscribe_logs_request"; } + const LogString *message_name() const override { return LOG_STR("subscribe_logs_request"); } #endif enums::LogLevel level{}; bool dump_config{false}; @@ -978,14 +988,14 @@ class SubscribeLogsRequest final : public ProtoDecodableMessage { #endif protected: - bool decode_varint(uint32_t field_id, ProtoVarInt value) override; + bool decode_varint(uint32_t field_id, proto_varint_value_t value) override; }; class SubscribeLogsResponse final : public ProtoMessage { public: static constexpr uint8_t MESSAGE_TYPE = 29; static constexpr uint8_t ESTIMATED_SIZE = 21; #ifdef HAS_PROTO_MESSAGE_DUMP - const char *message_name() const override { return "subscribe_logs_response"; } + const LogString *message_name() const override { return LOG_STR("subscribe_logs_response"); } #endif enums::LogLevel level{}; const uint8_t *message_ptr_{nullptr}; @@ -994,8 +1004,8 @@ class SubscribeLogsResponse final : public ProtoMessage { this->message_ptr_ = data; this->message_len_ = len; } - void encode(ProtoWriteBuffer buffer) const override; - void calculate_size(ProtoSize &size) const override; + void encode(ProtoWriteBuffer &buffer) const; + uint32_t calculate_size() const; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; #endif @@ -1008,7 +1018,7 @@ class NoiseEncryptionSetKeyRequest final : public ProtoDecodableMessage { static constexpr uint8_t MESSAGE_TYPE = 124; static constexpr uint8_t ESTIMATED_SIZE = 19; #ifdef HAS_PROTO_MESSAGE_DUMP - const char *message_name() const override { return "noise_encryption_set_key_request"; } + const LogString *message_name() const override { return LOG_STR("noise_encryption_set_key_request"); } #endif const uint8_t *key{nullptr}; uint16_t key_len{0}; @@ -1024,11 +1034,11 @@ class NoiseEncryptionSetKeyResponse final : public ProtoMessage { static constexpr uint8_t MESSAGE_TYPE = 125; static constexpr uint8_t ESTIMATED_SIZE = 2; #ifdef HAS_PROTO_MESSAGE_DUMP - const char *message_name() const override { return "noise_encryption_set_key_response"; } + const LogString *message_name() const override { return LOG_STR("noise_encryption_set_key_response"); } #endif bool success{false}; - void encode(ProtoWriteBuffer buffer) const override; - void calculate_size(ProtoSize &size) const override; + void encode(ProtoWriteBuffer &buffer) const; + uint32_t calculate_size() const; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; #endif @@ -1037,25 +1047,12 @@ class NoiseEncryptionSetKeyResponse final : public ProtoMessage { }; #endif #ifdef USE_API_HOMEASSISTANT_SERVICES -class SubscribeHomeassistantServicesRequest final : public ProtoMessage { - public: - static constexpr uint8_t MESSAGE_TYPE = 34; - static constexpr uint8_t ESTIMATED_SIZE = 0; -#ifdef HAS_PROTO_MESSAGE_DUMP - const char *message_name() const override { return "subscribe_homeassistant_services_request"; } -#endif -#ifdef HAS_PROTO_MESSAGE_DUMP - const char *dump_to(DumpBuffer &out) const override; -#endif - - protected: -}; class HomeassistantServiceMap final : public ProtoMessage { public: StringRef key{}; StringRef value{}; - void encode(ProtoWriteBuffer buffer) const override; - void calculate_size(ProtoSize &size) const override; + void encode(ProtoWriteBuffer &buffer) const; + uint32_t calculate_size() const; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; #endif @@ -1067,7 +1064,7 @@ class HomeassistantActionRequest final : public ProtoMessage { static constexpr uint8_t MESSAGE_TYPE = 35; static constexpr uint8_t ESTIMATED_SIZE = 128; #ifdef HAS_PROTO_MESSAGE_DUMP - const char *message_name() const override { return "homeassistant_action_request"; } + const LogString *message_name() const override { return LOG_STR("homeassistant_action_request"); } #endif StringRef service{}; FixedVector data{}; @@ -1083,8 +1080,8 @@ class HomeassistantActionRequest final : public ProtoMessage { #ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES_JSON StringRef response_template{}; #endif - void encode(ProtoWriteBuffer buffer) const override; - void calculate_size(ProtoSize &size) const override; + void encode(ProtoWriteBuffer &buffer) const; + uint32_t calculate_size() const; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; #endif @@ -1098,7 +1095,7 @@ class HomeassistantActionResponse final : public ProtoDecodableMessage { static constexpr uint8_t MESSAGE_TYPE = 130; static constexpr uint8_t ESTIMATED_SIZE = 34; #ifdef HAS_PROTO_MESSAGE_DUMP - const char *message_name() const override { return "homeassistant_action_response"; } + const LogString *message_name() const override { return LOG_STR("homeassistant_action_response"); } #endif uint32_t call_id{0}; bool success{false}; @@ -1113,35 +1110,22 @@ class HomeassistantActionResponse final : public ProtoDecodableMessage { protected: bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override; - bool decode_varint(uint32_t field_id, ProtoVarInt value) override; + bool decode_varint(uint32_t field_id, proto_varint_value_t value) override; }; #endif #ifdef USE_API_HOMEASSISTANT_STATES -class SubscribeHomeAssistantStatesRequest final : public ProtoMessage { - public: - static constexpr uint8_t MESSAGE_TYPE = 38; - static constexpr uint8_t ESTIMATED_SIZE = 0; -#ifdef HAS_PROTO_MESSAGE_DUMP - const char *message_name() const override { return "subscribe_home_assistant_states_request"; } -#endif -#ifdef HAS_PROTO_MESSAGE_DUMP - const char *dump_to(DumpBuffer &out) const override; -#endif - - protected: -}; class SubscribeHomeAssistantStateResponse final : public ProtoMessage { public: static constexpr uint8_t MESSAGE_TYPE = 39; static constexpr uint8_t ESTIMATED_SIZE = 20; #ifdef HAS_PROTO_MESSAGE_DUMP - const char *message_name() const override { return "subscribe_home_assistant_state_response"; } + const LogString *message_name() const override { return LOG_STR("subscribe_home_assistant_state_response"); } #endif StringRef entity_id{}; StringRef attribute{}; bool once{false}; - void encode(ProtoWriteBuffer buffer) const override; - void calculate_size(ProtoSize &size) const override; + void encode(ProtoWriteBuffer &buffer) const; + uint32_t calculate_size() const; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; #endif @@ -1153,7 +1137,7 @@ class HomeAssistantStateResponse final : public ProtoDecodableMessage { static constexpr uint8_t MESSAGE_TYPE = 40; static constexpr uint8_t ESTIMATED_SIZE = 27; #ifdef HAS_PROTO_MESSAGE_DUMP - const char *message_name() const override { return "home_assistant_state_response"; } + const LogString *message_name() const override { return LOG_STR("home_assistant_state_response"); } #endif StringRef entity_id{}; StringRef state{}; @@ -1171,7 +1155,7 @@ class GetTimeRequest final : public ProtoMessage { static constexpr uint8_t MESSAGE_TYPE = 36; static constexpr uint8_t ESTIMATED_SIZE = 0; #ifdef HAS_PROTO_MESSAGE_DUMP - const char *message_name() const override { return "get_time_request"; } + const LogString *message_name() const override { return LOG_STR("get_time_request"); } #endif #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; @@ -1179,15 +1163,45 @@ class GetTimeRequest final : public ProtoMessage { protected: }; +class DSTRule final : public ProtoDecodableMessage { + public: + int32_t time_seconds{0}; + uint32_t day{0}; + enums::DSTRuleType type{}; + uint32_t month{0}; + uint32_t week{0}; + uint32_t day_of_week{0}; +#ifdef HAS_PROTO_MESSAGE_DUMP + const char *dump_to(DumpBuffer &out) const override; +#endif + + protected: + bool decode_varint(uint32_t field_id, proto_varint_value_t value) override; +}; +class ParsedTimezone final : public ProtoDecodableMessage { + public: + int32_t std_offset_seconds{0}; + int32_t dst_offset_seconds{0}; + DSTRule dst_start{}; + DSTRule dst_end{}; +#ifdef HAS_PROTO_MESSAGE_DUMP + const char *dump_to(DumpBuffer &out) const override; +#endif + + protected: + bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override; + bool decode_varint(uint32_t field_id, proto_varint_value_t value) override; +}; class GetTimeResponse final : public ProtoDecodableMessage { public: static constexpr uint8_t MESSAGE_TYPE = 37; - static constexpr uint8_t ESTIMATED_SIZE = 14; + static constexpr uint8_t ESTIMATED_SIZE = 31; #ifdef HAS_PROTO_MESSAGE_DUMP - const char *message_name() const override { return "get_time_response"; } + const LogString *message_name() const override { return LOG_STR("get_time_response"); } #endif uint32_t epoch_seconds{0}; StringRef timezone{}; + ParsedTimezone parsed_timezone{}; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; #endif @@ -1201,8 +1215,8 @@ class ListEntitiesServicesArgument final : public ProtoMessage { public: StringRef name{}; enums::ServiceArgType type{}; - void encode(ProtoWriteBuffer buffer) const override; - void calculate_size(ProtoSize &size) const override; + void encode(ProtoWriteBuffer &buffer) const; + uint32_t calculate_size() const; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; #endif @@ -1214,14 +1228,14 @@ class ListEntitiesServicesResponse final : public ProtoMessage { static constexpr uint8_t MESSAGE_TYPE = 41; static constexpr uint8_t ESTIMATED_SIZE = 50; #ifdef HAS_PROTO_MESSAGE_DUMP - const char *message_name() const override { return "list_entities_services_response"; } + const LogString *message_name() const override { return LOG_STR("list_entities_services_response"); } #endif StringRef name{}; uint32_t key{0}; FixedVector args{}; enums::SupportsResponseType supports_response{}; - void encode(ProtoWriteBuffer buffer) const override; - void calculate_size(ProtoSize &size) const override; + void encode(ProtoWriteBuffer &buffer) const; + uint32_t calculate_size() const; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; #endif @@ -1239,7 +1253,7 @@ class ExecuteServiceArgument final : public ProtoDecodableMessage { FixedVector int_array{}; FixedVector float_array{}; FixedVector string_array{}; - void decode(const uint8_t *buffer, size_t length) override; + void decode(const uint8_t *buffer, size_t length); #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; #endif @@ -1247,14 +1261,14 @@ class ExecuteServiceArgument final : public ProtoDecodableMessage { protected: bool decode_32bit(uint32_t field_id, Proto32Bit value) override; bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override; - bool decode_varint(uint32_t field_id, ProtoVarInt value) override; + bool decode_varint(uint32_t field_id, proto_varint_value_t value) override; }; class ExecuteServiceRequest final : public ProtoDecodableMessage { public: static constexpr uint8_t MESSAGE_TYPE = 42; static constexpr uint8_t ESTIMATED_SIZE = 45; #ifdef HAS_PROTO_MESSAGE_DUMP - const char *message_name() const override { return "execute_service_request"; } + const LogString *message_name() const override { return LOG_STR("execute_service_request"); } #endif uint32_t key{0}; FixedVector args{}; @@ -1264,7 +1278,7 @@ class ExecuteServiceRequest final : public ProtoDecodableMessage { #ifdef USE_API_USER_DEFINED_ACTION_RESPONSES bool return_response{false}; #endif - void decode(const uint8_t *buffer, size_t length) override; + void decode(const uint8_t *buffer, size_t length); #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; #endif @@ -1272,7 +1286,7 @@ class ExecuteServiceRequest final : public ProtoDecodableMessage { protected: bool decode_32bit(uint32_t field_id, Proto32Bit value) override; bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override; - bool decode_varint(uint32_t field_id, ProtoVarInt value) override; + bool decode_varint(uint32_t field_id, proto_varint_value_t value) override; }; #endif #ifdef USE_API_USER_DEFINED_ACTION_RESPONSES @@ -1281,7 +1295,7 @@ class ExecuteServiceResponse final : public ProtoMessage { static constexpr uint8_t MESSAGE_TYPE = 131; static constexpr uint8_t ESTIMATED_SIZE = 34; #ifdef HAS_PROTO_MESSAGE_DUMP - const char *message_name() const override { return "execute_service_response"; } + const LogString *message_name() const override { return LOG_STR("execute_service_response"); } #endif uint32_t call_id{0}; bool success{false}; @@ -1290,8 +1304,8 @@ class ExecuteServiceResponse final : public ProtoMessage { const uint8_t *response_data{nullptr}; uint16_t response_data_len{0}; #endif - void encode(ProtoWriteBuffer buffer) const override; - void calculate_size(ProtoSize &size) const override; + void encode(ProtoWriteBuffer &buffer) const; + uint32_t calculate_size() const; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; #endif @@ -1305,10 +1319,10 @@ class ListEntitiesCameraResponse final : public InfoResponseProtoMessage { static constexpr uint8_t MESSAGE_TYPE = 43; static constexpr uint8_t ESTIMATED_SIZE = 40; #ifdef HAS_PROTO_MESSAGE_DUMP - const char *message_name() const override { return "list_entities_camera_response"; } + const LogString *message_name() const override { return LOG_STR("list_entities_camera_response"); } #endif - void encode(ProtoWriteBuffer buffer) const override; - void calculate_size(ProtoSize &size) const override; + void encode(ProtoWriteBuffer &buffer) const; + uint32_t calculate_size() const; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; #endif @@ -1320,7 +1334,7 @@ class CameraImageResponse final : public StateResponseProtoMessage { static constexpr uint8_t MESSAGE_TYPE = 44; static constexpr uint8_t ESTIMATED_SIZE = 30; #ifdef HAS_PROTO_MESSAGE_DUMP - const char *message_name() const override { return "camera_image_response"; } + const LogString *message_name() const override { return LOG_STR("camera_image_response"); } #endif const uint8_t *data_ptr_{nullptr}; size_t data_len_{0}; @@ -1329,8 +1343,8 @@ class CameraImageResponse final : public StateResponseProtoMessage { this->data_len_ = len; } bool done{false}; - void encode(ProtoWriteBuffer buffer) const override; - void calculate_size(ProtoSize &size) const override; + void encode(ProtoWriteBuffer &buffer) const; + uint32_t calculate_size() const; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; #endif @@ -1342,7 +1356,7 @@ class CameraImageRequest final : public ProtoDecodableMessage { static constexpr uint8_t MESSAGE_TYPE = 45; static constexpr uint8_t ESTIMATED_SIZE = 4; #ifdef HAS_PROTO_MESSAGE_DUMP - const char *message_name() const override { return "camera_image_request"; } + const LogString *message_name() const override { return LOG_STR("camera_image_request"); } #endif bool single{false}; bool stream{false}; @@ -1351,7 +1365,7 @@ class CameraImageRequest final : public ProtoDecodableMessage { #endif protected: - bool decode_varint(uint32_t field_id, ProtoVarInt value) override; + bool decode_varint(uint32_t field_id, proto_varint_value_t value) override; }; #endif #ifdef USE_CLIMATE @@ -1360,7 +1374,7 @@ class ListEntitiesClimateResponse final : public InfoResponseProtoMessage { static constexpr uint8_t MESSAGE_TYPE = 46; static constexpr uint8_t ESTIMATED_SIZE = 150; #ifdef HAS_PROTO_MESSAGE_DUMP - const char *message_name() const override { return "list_entities_climate_response"; } + const LogString *message_name() const override { return LOG_STR("list_entities_climate_response"); } #endif bool supports_current_temperature{false}; bool supports_two_point_target_temperature{false}; @@ -1380,8 +1394,8 @@ class ListEntitiesClimateResponse final : public InfoResponseProtoMessage { float visual_min_humidity{0.0f}; float visual_max_humidity{0.0f}; uint32_t feature_flags{0}; - void encode(ProtoWriteBuffer buffer) const override; - void calculate_size(ProtoSize &size) const override; + void encode(ProtoWriteBuffer &buffer) const; + uint32_t calculate_size() const; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; #endif @@ -1393,7 +1407,7 @@ class ClimateStateResponse final : public StateResponseProtoMessage { static constexpr uint8_t MESSAGE_TYPE = 47; static constexpr uint8_t ESTIMATED_SIZE = 68; #ifdef HAS_PROTO_MESSAGE_DUMP - const char *message_name() const override { return "climate_state_response"; } + const LogString *message_name() const override { return LOG_STR("climate_state_response"); } #endif enums::ClimateMode mode{}; float current_temperature{0.0f}; @@ -1408,8 +1422,8 @@ class ClimateStateResponse final : public StateResponseProtoMessage { StringRef custom_preset{}; float current_humidity{0.0f}; float target_humidity{0.0f}; - void encode(ProtoWriteBuffer buffer) const override; - void calculate_size(ProtoSize &size) const override; + void encode(ProtoWriteBuffer &buffer) const; + uint32_t calculate_size() const; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; #endif @@ -1421,7 +1435,7 @@ class ClimateCommandRequest final : public CommandProtoMessage { static constexpr uint8_t MESSAGE_TYPE = 48; static constexpr uint8_t ESTIMATED_SIZE = 84; #ifdef HAS_PROTO_MESSAGE_DUMP - const char *message_name() const override { return "climate_command_request"; } + const LogString *message_name() const override { return LOG_STR("climate_command_request"); } #endif bool has_mode{false}; enums::ClimateMode mode{}; @@ -1450,7 +1464,7 @@ class ClimateCommandRequest final : public CommandProtoMessage { protected: bool decode_32bit(uint32_t field_id, Proto32Bit value) override; bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override; - bool decode_varint(uint32_t field_id, ProtoVarInt value) override; + bool decode_varint(uint32_t field_id, proto_varint_value_t value) override; }; #endif #ifdef USE_WATER_HEATER @@ -1459,15 +1473,15 @@ class ListEntitiesWaterHeaterResponse final : public InfoResponseProtoMessage { static constexpr uint8_t MESSAGE_TYPE = 132; static constexpr uint8_t ESTIMATED_SIZE = 63; #ifdef HAS_PROTO_MESSAGE_DUMP - const char *message_name() const override { return "list_entities_water_heater_response"; } + const LogString *message_name() const override { return LOG_STR("list_entities_water_heater_response"); } #endif float min_temperature{0.0f}; float max_temperature{0.0f}; float target_temperature_step{0.0f}; const water_heater::WaterHeaterModeMask *supported_modes{}; uint32_t supported_features{0}; - void encode(ProtoWriteBuffer buffer) const override; - void calculate_size(ProtoSize &size) const override; + void encode(ProtoWriteBuffer &buffer) const; + uint32_t calculate_size() const; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; #endif @@ -1479,7 +1493,7 @@ class WaterHeaterStateResponse final : public StateResponseProtoMessage { static constexpr uint8_t MESSAGE_TYPE = 133; static constexpr uint8_t ESTIMATED_SIZE = 35; #ifdef HAS_PROTO_MESSAGE_DUMP - const char *message_name() const override { return "water_heater_state_response"; } + const LogString *message_name() const override { return LOG_STR("water_heater_state_response"); } #endif float current_temperature{0.0f}; float target_temperature{0.0f}; @@ -1487,8 +1501,8 @@ class WaterHeaterStateResponse final : public StateResponseProtoMessage { uint32_t state{0}; float target_temperature_low{0.0f}; float target_temperature_high{0.0f}; - void encode(ProtoWriteBuffer buffer) const override; - void calculate_size(ProtoSize &size) const override; + void encode(ProtoWriteBuffer &buffer) const; + uint32_t calculate_size() const; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; #endif @@ -1500,7 +1514,7 @@ class WaterHeaterCommandRequest final : public CommandProtoMessage { static constexpr uint8_t MESSAGE_TYPE = 134; static constexpr uint8_t ESTIMATED_SIZE = 34; #ifdef HAS_PROTO_MESSAGE_DUMP - const char *message_name() const override { return "water_heater_command_request"; } + const LogString *message_name() const override { return LOG_STR("water_heater_command_request"); } #endif uint32_t has_fields{0}; enums::WaterHeaterMode mode{}; @@ -1514,7 +1528,7 @@ class WaterHeaterCommandRequest final : public CommandProtoMessage { protected: bool decode_32bit(uint32_t field_id, Proto32Bit value) override; - bool decode_varint(uint32_t field_id, ProtoVarInt value) override; + bool decode_varint(uint32_t field_id, proto_varint_value_t value) override; }; #endif #ifdef USE_NUMBER @@ -1523,7 +1537,7 @@ class ListEntitiesNumberResponse final : public InfoResponseProtoMessage { static constexpr uint8_t MESSAGE_TYPE = 49; static constexpr uint8_t ESTIMATED_SIZE = 75; #ifdef HAS_PROTO_MESSAGE_DUMP - const char *message_name() const override { return "list_entities_number_response"; } + const LogString *message_name() const override { return LOG_STR("list_entities_number_response"); } #endif float min_value{0.0f}; float max_value{0.0f}; @@ -1531,8 +1545,8 @@ class ListEntitiesNumberResponse final : public InfoResponseProtoMessage { StringRef unit_of_measurement{}; enums::NumberMode mode{}; StringRef device_class{}; - void encode(ProtoWriteBuffer buffer) const override; - void calculate_size(ProtoSize &size) const override; + void encode(ProtoWriteBuffer &buffer) const; + uint32_t calculate_size() const; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; #endif @@ -1544,12 +1558,12 @@ class NumberStateResponse final : public StateResponseProtoMessage { static constexpr uint8_t MESSAGE_TYPE = 50; static constexpr uint8_t ESTIMATED_SIZE = 16; #ifdef HAS_PROTO_MESSAGE_DUMP - const char *message_name() const override { return "number_state_response"; } + const LogString *message_name() const override { return LOG_STR("number_state_response"); } #endif float state{0.0f}; bool missing_state{false}; - void encode(ProtoWriteBuffer buffer) const override; - void calculate_size(ProtoSize &size) const override; + void encode(ProtoWriteBuffer &buffer) const; + uint32_t calculate_size() const; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; #endif @@ -1561,7 +1575,7 @@ class NumberCommandRequest final : public CommandProtoMessage { static constexpr uint8_t MESSAGE_TYPE = 51; static constexpr uint8_t ESTIMATED_SIZE = 14; #ifdef HAS_PROTO_MESSAGE_DUMP - const char *message_name() const override { return "number_command_request"; } + const LogString *message_name() const override { return LOG_STR("number_command_request"); } #endif float state{0.0f}; #ifdef HAS_PROTO_MESSAGE_DUMP @@ -1570,7 +1584,7 @@ class NumberCommandRequest final : public CommandProtoMessage { protected: bool decode_32bit(uint32_t field_id, Proto32Bit value) override; - bool decode_varint(uint32_t field_id, ProtoVarInt value) override; + bool decode_varint(uint32_t field_id, proto_varint_value_t value) override; }; #endif #ifdef USE_SELECT @@ -1579,11 +1593,11 @@ class ListEntitiesSelectResponse final : public InfoResponseProtoMessage { static constexpr uint8_t MESSAGE_TYPE = 52; static constexpr uint8_t ESTIMATED_SIZE = 58; #ifdef HAS_PROTO_MESSAGE_DUMP - const char *message_name() const override { return "list_entities_select_response"; } + const LogString *message_name() const override { return LOG_STR("list_entities_select_response"); } #endif const FixedVector *options{}; - void encode(ProtoWriteBuffer buffer) const override; - void calculate_size(ProtoSize &size) const override; + void encode(ProtoWriteBuffer &buffer) const; + uint32_t calculate_size() const; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; #endif @@ -1595,12 +1609,12 @@ class SelectStateResponse final : public StateResponseProtoMessage { static constexpr uint8_t MESSAGE_TYPE = 53; static constexpr uint8_t ESTIMATED_SIZE = 20; #ifdef HAS_PROTO_MESSAGE_DUMP - const char *message_name() const override { return "select_state_response"; } + const LogString *message_name() const override { return LOG_STR("select_state_response"); } #endif StringRef state{}; bool missing_state{false}; - void encode(ProtoWriteBuffer buffer) const override; - void calculate_size(ProtoSize &size) const override; + void encode(ProtoWriteBuffer &buffer) const; + uint32_t calculate_size() const; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; #endif @@ -1612,7 +1626,7 @@ class SelectCommandRequest final : public CommandProtoMessage { static constexpr uint8_t MESSAGE_TYPE = 54; static constexpr uint8_t ESTIMATED_SIZE = 18; #ifdef HAS_PROTO_MESSAGE_DUMP - const char *message_name() const override { return "select_command_request"; } + const LogString *message_name() const override { return LOG_STR("select_command_request"); } #endif StringRef state{}; #ifdef HAS_PROTO_MESSAGE_DUMP @@ -1622,7 +1636,7 @@ class SelectCommandRequest final : public CommandProtoMessage { protected: bool decode_32bit(uint32_t field_id, Proto32Bit value) override; bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override; - bool decode_varint(uint32_t field_id, ProtoVarInt value) override; + bool decode_varint(uint32_t field_id, proto_varint_value_t value) override; }; #endif #ifdef USE_SIREN @@ -1631,13 +1645,13 @@ class ListEntitiesSirenResponse final : public InfoResponseProtoMessage { static constexpr uint8_t MESSAGE_TYPE = 55; static constexpr uint8_t ESTIMATED_SIZE = 62; #ifdef HAS_PROTO_MESSAGE_DUMP - const char *message_name() const override { return "list_entities_siren_response"; } + const LogString *message_name() const override { return LOG_STR("list_entities_siren_response"); } #endif const FixedVector *tones{}; bool supports_duration{false}; bool supports_volume{false}; - void encode(ProtoWriteBuffer buffer) const override; - void calculate_size(ProtoSize &size) const override; + void encode(ProtoWriteBuffer &buffer) const; + uint32_t calculate_size() const; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; #endif @@ -1649,11 +1663,11 @@ class SirenStateResponse final : public StateResponseProtoMessage { static constexpr uint8_t MESSAGE_TYPE = 56; static constexpr uint8_t ESTIMATED_SIZE = 11; #ifdef HAS_PROTO_MESSAGE_DUMP - const char *message_name() const override { return "siren_state_response"; } + const LogString *message_name() const override { return LOG_STR("siren_state_response"); } #endif bool state{false}; - void encode(ProtoWriteBuffer buffer) const override; - void calculate_size(ProtoSize &size) const override; + void encode(ProtoWriteBuffer &buffer) const; + uint32_t calculate_size() const; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; #endif @@ -1665,7 +1679,7 @@ class SirenCommandRequest final : public CommandProtoMessage { static constexpr uint8_t MESSAGE_TYPE = 57; static constexpr uint8_t ESTIMATED_SIZE = 37; #ifdef HAS_PROTO_MESSAGE_DUMP - const char *message_name() const override { return "siren_command_request"; } + const LogString *message_name() const override { return LOG_STR("siren_command_request"); } #endif bool has_state{false}; bool state{false}; @@ -1682,7 +1696,7 @@ class SirenCommandRequest final : public CommandProtoMessage { protected: bool decode_32bit(uint32_t field_id, Proto32Bit value) override; bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override; - bool decode_varint(uint32_t field_id, ProtoVarInt value) override; + bool decode_varint(uint32_t field_id, proto_varint_value_t value) override; }; #endif #ifdef USE_LOCK @@ -1691,14 +1705,14 @@ class ListEntitiesLockResponse final : public InfoResponseProtoMessage { static constexpr uint8_t MESSAGE_TYPE = 58; static constexpr uint8_t ESTIMATED_SIZE = 55; #ifdef HAS_PROTO_MESSAGE_DUMP - const char *message_name() const override { return "list_entities_lock_response"; } + const LogString *message_name() const override { return LOG_STR("list_entities_lock_response"); } #endif bool assumed_state{false}; bool supports_open{false}; bool requires_code{false}; StringRef code_format{}; - void encode(ProtoWriteBuffer buffer) const override; - void calculate_size(ProtoSize &size) const override; + void encode(ProtoWriteBuffer &buffer) const; + uint32_t calculate_size() const; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; #endif @@ -1710,11 +1724,11 @@ class LockStateResponse final : public StateResponseProtoMessage { static constexpr uint8_t MESSAGE_TYPE = 59; static constexpr uint8_t ESTIMATED_SIZE = 11; #ifdef HAS_PROTO_MESSAGE_DUMP - const char *message_name() const override { return "lock_state_response"; } + const LogString *message_name() const override { return LOG_STR("lock_state_response"); } #endif enums::LockState state{}; - void encode(ProtoWriteBuffer buffer) const override; - void calculate_size(ProtoSize &size) const override; + void encode(ProtoWriteBuffer &buffer) const; + uint32_t calculate_size() const; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; #endif @@ -1726,7 +1740,7 @@ class LockCommandRequest final : public CommandProtoMessage { static constexpr uint8_t MESSAGE_TYPE = 60; static constexpr uint8_t ESTIMATED_SIZE = 22; #ifdef HAS_PROTO_MESSAGE_DUMP - const char *message_name() const override { return "lock_command_request"; } + const LogString *message_name() const override { return LOG_STR("lock_command_request"); } #endif enums::LockCommand command{}; bool has_code{false}; @@ -1738,7 +1752,7 @@ class LockCommandRequest final : public CommandProtoMessage { protected: bool decode_32bit(uint32_t field_id, Proto32Bit value) override; bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override; - bool decode_varint(uint32_t field_id, ProtoVarInt value) override; + bool decode_varint(uint32_t field_id, proto_varint_value_t value) override; }; #endif #ifdef USE_BUTTON @@ -1747,11 +1761,11 @@ class ListEntitiesButtonResponse final : public InfoResponseProtoMessage { static constexpr uint8_t MESSAGE_TYPE = 61; static constexpr uint8_t ESTIMATED_SIZE = 49; #ifdef HAS_PROTO_MESSAGE_DUMP - const char *message_name() const override { return "list_entities_button_response"; } + const LogString *message_name() const override { return LOG_STR("list_entities_button_response"); } #endif StringRef device_class{}; - void encode(ProtoWriteBuffer buffer) const override; - void calculate_size(ProtoSize &size) const override; + void encode(ProtoWriteBuffer &buffer) const; + uint32_t calculate_size() const; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; #endif @@ -1763,7 +1777,7 @@ class ButtonCommandRequest final : public CommandProtoMessage { static constexpr uint8_t MESSAGE_TYPE = 62; static constexpr uint8_t ESTIMATED_SIZE = 9; #ifdef HAS_PROTO_MESSAGE_DUMP - const char *message_name() const override { return "button_command_request"; } + const LogString *message_name() const override { return LOG_STR("button_command_request"); } #endif #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; @@ -1771,7 +1785,7 @@ class ButtonCommandRequest final : public CommandProtoMessage { protected: bool decode_32bit(uint32_t field_id, Proto32Bit value) override; - bool decode_varint(uint32_t field_id, ProtoVarInt value) override; + bool decode_varint(uint32_t field_id, proto_varint_value_t value) override; }; #endif #ifdef USE_MEDIA_PLAYER @@ -1782,8 +1796,8 @@ class MediaPlayerSupportedFormat final : public ProtoMessage { uint32_t num_channels{0}; enums::MediaPlayerFormatPurpose purpose{}; uint32_t sample_bytes{0}; - void encode(ProtoWriteBuffer buffer) const override; - void calculate_size(ProtoSize &size) const override; + void encode(ProtoWriteBuffer &buffer) const; + uint32_t calculate_size() const; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; #endif @@ -1795,13 +1809,13 @@ class ListEntitiesMediaPlayerResponse final : public InfoResponseProtoMessage { static constexpr uint8_t MESSAGE_TYPE = 63; static constexpr uint8_t ESTIMATED_SIZE = 80; #ifdef HAS_PROTO_MESSAGE_DUMP - const char *message_name() const override { return "list_entities_media_player_response"; } + const LogString *message_name() const override { return LOG_STR("list_entities_media_player_response"); } #endif bool supports_pause{false}; std::vector supported_formats{}; uint32_t feature_flags{0}; - void encode(ProtoWriteBuffer buffer) const override; - void calculate_size(ProtoSize &size) const override; + void encode(ProtoWriteBuffer &buffer) const; + uint32_t calculate_size() const; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; #endif @@ -1813,13 +1827,13 @@ class MediaPlayerStateResponse final : public StateResponseProtoMessage { static constexpr uint8_t MESSAGE_TYPE = 64; static constexpr uint8_t ESTIMATED_SIZE = 18; #ifdef HAS_PROTO_MESSAGE_DUMP - const char *message_name() const override { return "media_player_state_response"; } + const LogString *message_name() const override { return LOG_STR("media_player_state_response"); } #endif enums::MediaPlayerState state{}; float volume{0.0f}; bool muted{false}; - void encode(ProtoWriteBuffer buffer) const override; - void calculate_size(ProtoSize &size) const override; + void encode(ProtoWriteBuffer &buffer) const; + uint32_t calculate_size() const; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; #endif @@ -1831,7 +1845,7 @@ class MediaPlayerCommandRequest final : public CommandProtoMessage { static constexpr uint8_t MESSAGE_TYPE = 65; static constexpr uint8_t ESTIMATED_SIZE = 35; #ifdef HAS_PROTO_MESSAGE_DUMP - const char *message_name() const override { return "media_player_command_request"; } + const LogString *message_name() const override { return LOG_STR("media_player_command_request"); } #endif bool has_command{false}; enums::MediaPlayerCommand command{}; @@ -1848,7 +1862,7 @@ class MediaPlayerCommandRequest final : public CommandProtoMessage { protected: bool decode_32bit(uint32_t field_id, Proto32Bit value) override; bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override; - bool decode_varint(uint32_t field_id, ProtoVarInt value) override; + bool decode_varint(uint32_t field_id, proto_varint_value_t value) override; }; #endif #ifdef USE_BLUETOOTH_PROXY @@ -1857,7 +1871,7 @@ class SubscribeBluetoothLEAdvertisementsRequest final : public ProtoDecodableMes static constexpr uint8_t MESSAGE_TYPE = 66; static constexpr uint8_t ESTIMATED_SIZE = 4; #ifdef HAS_PROTO_MESSAGE_DUMP - const char *message_name() const override { return "subscribe_bluetooth_le_advertisements_request"; } + const LogString *message_name() const override { return LOG_STR("subscribe_bluetooth_le_advertisements_request"); } #endif uint32_t flags{0}; #ifdef HAS_PROTO_MESSAGE_DUMP @@ -1865,7 +1879,7 @@ class SubscribeBluetoothLEAdvertisementsRequest final : public ProtoDecodableMes #endif protected: - bool decode_varint(uint32_t field_id, ProtoVarInt value) override; + bool decode_varint(uint32_t field_id, proto_varint_value_t value) override; }; class BluetoothLERawAdvertisement final : public ProtoMessage { public: @@ -1874,8 +1888,8 @@ class BluetoothLERawAdvertisement final : public ProtoMessage { uint32_t address_type{0}; uint8_t data[62]{}; uint8_t data_len{0}; - void encode(ProtoWriteBuffer buffer) const override; - void calculate_size(ProtoSize &size) const override; + void encode(ProtoWriteBuffer &buffer) const; + uint32_t calculate_size() const; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; #endif @@ -1887,12 +1901,12 @@ class BluetoothLERawAdvertisementsResponse final : public ProtoMessage { static constexpr uint8_t MESSAGE_TYPE = 93; static constexpr uint8_t ESTIMATED_SIZE = 136; #ifdef HAS_PROTO_MESSAGE_DUMP - const char *message_name() const override { return "bluetooth_le_raw_advertisements_response"; } + const LogString *message_name() const override { return LOG_STR("bluetooth_le_raw_advertisements_response"); } #endif std::array advertisements{}; uint16_t advertisements_len{0}; - void encode(ProtoWriteBuffer buffer) const override; - void calculate_size(ProtoSize &size) const override; + void encode(ProtoWriteBuffer &buffer) const; + uint32_t calculate_size() const; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; #endif @@ -1904,7 +1918,7 @@ class BluetoothDeviceRequest final : public ProtoDecodableMessage { static constexpr uint8_t MESSAGE_TYPE = 68; static constexpr uint8_t ESTIMATED_SIZE = 12; #ifdef HAS_PROTO_MESSAGE_DUMP - const char *message_name() const override { return "bluetooth_device_request"; } + const LogString *message_name() const override { return LOG_STR("bluetooth_device_request"); } #endif uint64_t address{0}; enums::BluetoothDeviceRequestType request_type{}; @@ -1915,21 +1929,21 @@ class BluetoothDeviceRequest final : public ProtoDecodableMessage { #endif protected: - bool decode_varint(uint32_t field_id, ProtoVarInt value) override; + bool decode_varint(uint32_t field_id, proto_varint_value_t value) override; }; class BluetoothDeviceConnectionResponse final : public ProtoMessage { public: static constexpr uint8_t MESSAGE_TYPE = 69; static constexpr uint8_t ESTIMATED_SIZE = 14; #ifdef HAS_PROTO_MESSAGE_DUMP - const char *message_name() const override { return "bluetooth_device_connection_response"; } + const LogString *message_name() const override { return LOG_STR("bluetooth_device_connection_response"); } #endif uint64_t address{0}; bool connected{false}; uint32_t mtu{0}; int32_t error{0}; - void encode(ProtoWriteBuffer buffer) const override; - void calculate_size(ProtoSize &size) const override; + void encode(ProtoWriteBuffer &buffer) const; + uint32_t calculate_size() const; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; #endif @@ -1941,7 +1955,7 @@ class BluetoothGATTGetServicesRequest final : public ProtoDecodableMessage { static constexpr uint8_t MESSAGE_TYPE = 70; static constexpr uint8_t ESTIMATED_SIZE = 4; #ifdef HAS_PROTO_MESSAGE_DUMP - const char *message_name() const override { return "bluetooth_gatt_get_services_request"; } + const LogString *message_name() const override { return LOG_STR("bluetooth_gatt_get_services_request"); } #endif uint64_t address{0}; #ifdef HAS_PROTO_MESSAGE_DUMP @@ -1949,15 +1963,15 @@ class BluetoothGATTGetServicesRequest final : public ProtoDecodableMessage { #endif protected: - bool decode_varint(uint32_t field_id, ProtoVarInt value) override; + bool decode_varint(uint32_t field_id, proto_varint_value_t value) override; }; class BluetoothGATTDescriptor final : public ProtoMessage { public: std::array uuid{}; uint32_t handle{0}; uint32_t short_uuid{0}; - void encode(ProtoWriteBuffer buffer) const override; - void calculate_size(ProtoSize &size) const override; + void encode(ProtoWriteBuffer &buffer) const; + uint32_t calculate_size() const; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; #endif @@ -1971,8 +1985,8 @@ class BluetoothGATTCharacteristic final : public ProtoMessage { uint32_t properties{0}; FixedVector descriptors{}; uint32_t short_uuid{0}; - void encode(ProtoWriteBuffer buffer) const override; - void calculate_size(ProtoSize &size) const override; + void encode(ProtoWriteBuffer &buffer) const; + uint32_t calculate_size() const; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; #endif @@ -1985,8 +1999,8 @@ class BluetoothGATTService final : public ProtoMessage { uint32_t handle{0}; FixedVector characteristics{}; uint32_t short_uuid{0}; - void encode(ProtoWriteBuffer buffer) const override; - void calculate_size(ProtoSize &size) const override; + void encode(ProtoWriteBuffer &buffer) const; + uint32_t calculate_size() const; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; #endif @@ -1998,12 +2012,12 @@ class BluetoothGATTGetServicesResponse final : public ProtoMessage { static constexpr uint8_t MESSAGE_TYPE = 71; static constexpr uint8_t ESTIMATED_SIZE = 38; #ifdef HAS_PROTO_MESSAGE_DUMP - const char *message_name() const override { return "bluetooth_gatt_get_services_response"; } + const LogString *message_name() const override { return LOG_STR("bluetooth_gatt_get_services_response"); } #endif uint64_t address{0}; std::vector services{}; - void encode(ProtoWriteBuffer buffer) const override; - void calculate_size(ProtoSize &size) const override; + void encode(ProtoWriteBuffer &buffer) const; + uint32_t calculate_size() const; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; #endif @@ -2015,11 +2029,11 @@ class BluetoothGATTGetServicesDoneResponse final : public ProtoMessage { static constexpr uint8_t MESSAGE_TYPE = 72; static constexpr uint8_t ESTIMATED_SIZE = 4; #ifdef HAS_PROTO_MESSAGE_DUMP - const char *message_name() const override { return "bluetooth_gatt_get_services_done_response"; } + const LogString *message_name() const override { return LOG_STR("bluetooth_gatt_get_services_done_response"); } #endif uint64_t address{0}; - void encode(ProtoWriteBuffer buffer) const override; - void calculate_size(ProtoSize &size) const override; + void encode(ProtoWriteBuffer &buffer) const; + uint32_t calculate_size() const; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; #endif @@ -2031,7 +2045,7 @@ class BluetoothGATTReadRequest final : public ProtoDecodableMessage { static constexpr uint8_t MESSAGE_TYPE = 73; static constexpr uint8_t ESTIMATED_SIZE = 8; #ifdef HAS_PROTO_MESSAGE_DUMP - const char *message_name() const override { return "bluetooth_gatt_read_request"; } + const LogString *message_name() const override { return LOG_STR("bluetooth_gatt_read_request"); } #endif uint64_t address{0}; uint32_t handle{0}; @@ -2040,14 +2054,14 @@ class BluetoothGATTReadRequest final : public ProtoDecodableMessage { #endif protected: - bool decode_varint(uint32_t field_id, ProtoVarInt value) override; + bool decode_varint(uint32_t field_id, proto_varint_value_t value) override; }; class BluetoothGATTReadResponse final : public ProtoMessage { public: static constexpr uint8_t MESSAGE_TYPE = 74; static constexpr uint8_t ESTIMATED_SIZE = 27; #ifdef HAS_PROTO_MESSAGE_DUMP - const char *message_name() const override { return "bluetooth_gatt_read_response"; } + const LogString *message_name() const override { return LOG_STR("bluetooth_gatt_read_response"); } #endif uint64_t address{0}; uint32_t handle{0}; @@ -2057,8 +2071,8 @@ class BluetoothGATTReadResponse final : public ProtoMessage { this->data_ptr_ = data; this->data_len_ = len; } - void encode(ProtoWriteBuffer buffer) const override; - void calculate_size(ProtoSize &size) const override; + void encode(ProtoWriteBuffer &buffer) const; + uint32_t calculate_size() const; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; #endif @@ -2070,7 +2084,7 @@ class BluetoothGATTWriteRequest final : public ProtoDecodableMessage { static constexpr uint8_t MESSAGE_TYPE = 75; static constexpr uint8_t ESTIMATED_SIZE = 29; #ifdef HAS_PROTO_MESSAGE_DUMP - const char *message_name() const override { return "bluetooth_gatt_write_request"; } + const LogString *message_name() const override { return LOG_STR("bluetooth_gatt_write_request"); } #endif uint64_t address{0}; uint32_t handle{0}; @@ -2083,14 +2097,14 @@ class BluetoothGATTWriteRequest final : public ProtoDecodableMessage { protected: bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override; - bool decode_varint(uint32_t field_id, ProtoVarInt value) override; + bool decode_varint(uint32_t field_id, proto_varint_value_t value) override; }; class BluetoothGATTReadDescriptorRequest final : public ProtoDecodableMessage { public: static constexpr uint8_t MESSAGE_TYPE = 76; static constexpr uint8_t ESTIMATED_SIZE = 8; #ifdef HAS_PROTO_MESSAGE_DUMP - const char *message_name() const override { return "bluetooth_gatt_read_descriptor_request"; } + const LogString *message_name() const override { return LOG_STR("bluetooth_gatt_read_descriptor_request"); } #endif uint64_t address{0}; uint32_t handle{0}; @@ -2099,14 +2113,14 @@ class BluetoothGATTReadDescriptorRequest final : public ProtoDecodableMessage { #endif protected: - bool decode_varint(uint32_t field_id, ProtoVarInt value) override; + bool decode_varint(uint32_t field_id, proto_varint_value_t value) override; }; class BluetoothGATTWriteDescriptorRequest final : public ProtoDecodableMessage { public: static constexpr uint8_t MESSAGE_TYPE = 77; static constexpr uint8_t ESTIMATED_SIZE = 27; #ifdef HAS_PROTO_MESSAGE_DUMP - const char *message_name() const override { return "bluetooth_gatt_write_descriptor_request"; } + const LogString *message_name() const override { return LOG_STR("bluetooth_gatt_write_descriptor_request"); } #endif uint64_t address{0}; uint32_t handle{0}; @@ -2118,14 +2132,14 @@ class BluetoothGATTWriteDescriptorRequest final : public ProtoDecodableMessage { protected: bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override; - bool decode_varint(uint32_t field_id, ProtoVarInt value) override; + bool decode_varint(uint32_t field_id, proto_varint_value_t value) override; }; class BluetoothGATTNotifyRequest final : public ProtoDecodableMessage { public: static constexpr uint8_t MESSAGE_TYPE = 78; static constexpr uint8_t ESTIMATED_SIZE = 10; #ifdef HAS_PROTO_MESSAGE_DUMP - const char *message_name() const override { return "bluetooth_gatt_notify_request"; } + const LogString *message_name() const override { return LOG_STR("bluetooth_gatt_notify_request"); } #endif uint64_t address{0}; uint32_t handle{0}; @@ -2135,14 +2149,14 @@ class BluetoothGATTNotifyRequest final : public ProtoDecodableMessage { #endif protected: - bool decode_varint(uint32_t field_id, ProtoVarInt value) override; + bool decode_varint(uint32_t field_id, proto_varint_value_t value) override; }; class BluetoothGATTNotifyDataResponse final : public ProtoMessage { public: static constexpr uint8_t MESSAGE_TYPE = 79; static constexpr uint8_t ESTIMATED_SIZE = 27; #ifdef HAS_PROTO_MESSAGE_DUMP - const char *message_name() const override { return "bluetooth_gatt_notify_data_response"; } + const LogString *message_name() const override { return LOG_STR("bluetooth_gatt_notify_data_response"); } #endif uint64_t address{0}; uint32_t handle{0}; @@ -2152,21 +2166,8 @@ class BluetoothGATTNotifyDataResponse final : public ProtoMessage { this->data_ptr_ = data; this->data_len_ = len; } - void encode(ProtoWriteBuffer buffer) const override; - void calculate_size(ProtoSize &size) const override; -#ifdef HAS_PROTO_MESSAGE_DUMP - const char *dump_to(DumpBuffer &out) const override; -#endif - - protected: -}; -class SubscribeBluetoothConnectionsFreeRequest final : public ProtoMessage { - public: - static constexpr uint8_t MESSAGE_TYPE = 80; - static constexpr uint8_t ESTIMATED_SIZE = 0; -#ifdef HAS_PROTO_MESSAGE_DUMP - const char *message_name() const override { return "subscribe_bluetooth_connections_free_request"; } -#endif + void encode(ProtoWriteBuffer &buffer) const; + uint32_t calculate_size() const; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; #endif @@ -2178,13 +2179,13 @@ class BluetoothConnectionsFreeResponse final : public ProtoMessage { static constexpr uint8_t MESSAGE_TYPE = 81; static constexpr uint8_t ESTIMATED_SIZE = 20; #ifdef HAS_PROTO_MESSAGE_DUMP - const char *message_name() const override { return "bluetooth_connections_free_response"; } + const LogString *message_name() const override { return LOG_STR("bluetooth_connections_free_response"); } #endif uint32_t free{0}; uint32_t limit{0}; std::array allocated{}; - void encode(ProtoWriteBuffer buffer) const override; - void calculate_size(ProtoSize &size) const override; + void encode(ProtoWriteBuffer &buffer) const; + uint32_t calculate_size() const; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; #endif @@ -2196,13 +2197,13 @@ class BluetoothGATTErrorResponse final : public ProtoMessage { static constexpr uint8_t MESSAGE_TYPE = 82; static constexpr uint8_t ESTIMATED_SIZE = 12; #ifdef HAS_PROTO_MESSAGE_DUMP - const char *message_name() const override { return "bluetooth_gatt_error_response"; } + const LogString *message_name() const override { return LOG_STR("bluetooth_gatt_error_response"); } #endif uint64_t address{0}; uint32_t handle{0}; int32_t error{0}; - void encode(ProtoWriteBuffer buffer) const override; - void calculate_size(ProtoSize &size) const override; + void encode(ProtoWriteBuffer &buffer) const; + uint32_t calculate_size() const; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; #endif @@ -2214,12 +2215,12 @@ class BluetoothGATTWriteResponse final : public ProtoMessage { static constexpr uint8_t MESSAGE_TYPE = 83; static constexpr uint8_t ESTIMATED_SIZE = 8; #ifdef HAS_PROTO_MESSAGE_DUMP - const char *message_name() const override { return "bluetooth_gatt_write_response"; } + const LogString *message_name() const override { return LOG_STR("bluetooth_gatt_write_response"); } #endif uint64_t address{0}; uint32_t handle{0}; - void encode(ProtoWriteBuffer buffer) const override; - void calculate_size(ProtoSize &size) const override; + void encode(ProtoWriteBuffer &buffer) const; + uint32_t calculate_size() const; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; #endif @@ -2231,12 +2232,12 @@ class BluetoothGATTNotifyResponse final : public ProtoMessage { static constexpr uint8_t MESSAGE_TYPE = 84; static constexpr uint8_t ESTIMATED_SIZE = 8; #ifdef HAS_PROTO_MESSAGE_DUMP - const char *message_name() const override { return "bluetooth_gatt_notify_response"; } + const LogString *message_name() const override { return LOG_STR("bluetooth_gatt_notify_response"); } #endif uint64_t address{0}; uint32_t handle{0}; - void encode(ProtoWriteBuffer buffer) const override; - void calculate_size(ProtoSize &size) const override; + void encode(ProtoWriteBuffer &buffer) const; + uint32_t calculate_size() const; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; #endif @@ -2248,13 +2249,13 @@ class BluetoothDevicePairingResponse final : public ProtoMessage { static constexpr uint8_t MESSAGE_TYPE = 85; static constexpr uint8_t ESTIMATED_SIZE = 10; #ifdef HAS_PROTO_MESSAGE_DUMP - const char *message_name() const override { return "bluetooth_device_pairing_response"; } + const LogString *message_name() const override { return LOG_STR("bluetooth_device_pairing_response"); } #endif uint64_t address{0}; bool paired{false}; int32_t error{0}; - void encode(ProtoWriteBuffer buffer) const override; - void calculate_size(ProtoSize &size) const override; + void encode(ProtoWriteBuffer &buffer) const; + uint32_t calculate_size() const; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; #endif @@ -2266,26 +2267,13 @@ class BluetoothDeviceUnpairingResponse final : public ProtoMessage { static constexpr uint8_t MESSAGE_TYPE = 86; static constexpr uint8_t ESTIMATED_SIZE = 10; #ifdef HAS_PROTO_MESSAGE_DUMP - const char *message_name() const override { return "bluetooth_device_unpairing_response"; } + const LogString *message_name() const override { return LOG_STR("bluetooth_device_unpairing_response"); } #endif uint64_t address{0}; bool success{false}; int32_t error{0}; - void encode(ProtoWriteBuffer buffer) const override; - void calculate_size(ProtoSize &size) const override; -#ifdef HAS_PROTO_MESSAGE_DUMP - const char *dump_to(DumpBuffer &out) const override; -#endif - - protected: -}; -class UnsubscribeBluetoothLEAdvertisementsRequest final : public ProtoMessage { - public: - static constexpr uint8_t MESSAGE_TYPE = 87; - static constexpr uint8_t ESTIMATED_SIZE = 0; -#ifdef HAS_PROTO_MESSAGE_DUMP - const char *message_name() const override { return "unsubscribe_bluetooth_le_advertisements_request"; } -#endif + void encode(ProtoWriteBuffer &buffer) const; + uint32_t calculate_size() const; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; #endif @@ -2297,13 +2285,13 @@ class BluetoothDeviceClearCacheResponse final : public ProtoMessage { static constexpr uint8_t MESSAGE_TYPE = 88; static constexpr uint8_t ESTIMATED_SIZE = 10; #ifdef HAS_PROTO_MESSAGE_DUMP - const char *message_name() const override { return "bluetooth_device_clear_cache_response"; } + const LogString *message_name() const override { return LOG_STR("bluetooth_device_clear_cache_response"); } #endif uint64_t address{0}; bool success{false}; int32_t error{0}; - void encode(ProtoWriteBuffer buffer) const override; - void calculate_size(ProtoSize &size) const override; + void encode(ProtoWriteBuffer &buffer) const; + uint32_t calculate_size() const; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; #endif @@ -2315,13 +2303,13 @@ class BluetoothScannerStateResponse final : public ProtoMessage { static constexpr uint8_t MESSAGE_TYPE = 126; static constexpr uint8_t ESTIMATED_SIZE = 6; #ifdef HAS_PROTO_MESSAGE_DUMP - const char *message_name() const override { return "bluetooth_scanner_state_response"; } + const LogString *message_name() const override { return LOG_STR("bluetooth_scanner_state_response"); } #endif enums::BluetoothScannerState state{}; enums::BluetoothScannerMode mode{}; enums::BluetoothScannerMode configured_mode{}; - void encode(ProtoWriteBuffer buffer) const override; - void calculate_size(ProtoSize &size) const override; + void encode(ProtoWriteBuffer &buffer) const; + uint32_t calculate_size() const; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; #endif @@ -2333,7 +2321,7 @@ class BluetoothScannerSetModeRequest final : public ProtoDecodableMessage { static constexpr uint8_t MESSAGE_TYPE = 127; static constexpr uint8_t ESTIMATED_SIZE = 2; #ifdef HAS_PROTO_MESSAGE_DUMP - const char *message_name() const override { return "bluetooth_scanner_set_mode_request"; } + const LogString *message_name() const override { return LOG_STR("bluetooth_scanner_set_mode_request"); } #endif enums::BluetoothScannerMode mode{}; #ifdef HAS_PROTO_MESSAGE_DUMP @@ -2341,7 +2329,7 @@ class BluetoothScannerSetModeRequest final : public ProtoDecodableMessage { #endif protected: - bool decode_varint(uint32_t field_id, ProtoVarInt value) override; + bool decode_varint(uint32_t field_id, proto_varint_value_t value) override; }; #endif #ifdef USE_VOICE_ASSISTANT @@ -2350,7 +2338,7 @@ class SubscribeVoiceAssistantRequest final : public ProtoDecodableMessage { static constexpr uint8_t MESSAGE_TYPE = 89; static constexpr uint8_t ESTIMATED_SIZE = 6; #ifdef HAS_PROTO_MESSAGE_DUMP - const char *message_name() const override { return "subscribe_voice_assistant_request"; } + const LogString *message_name() const override { return LOG_STR("subscribe_voice_assistant_request"); } #endif bool subscribe{false}; uint32_t flags{0}; @@ -2359,15 +2347,15 @@ class SubscribeVoiceAssistantRequest final : public ProtoDecodableMessage { #endif protected: - bool decode_varint(uint32_t field_id, ProtoVarInt value) override; + bool decode_varint(uint32_t field_id, proto_varint_value_t value) override; }; class VoiceAssistantAudioSettings final : public ProtoMessage { public: uint32_t noise_suppression_level{0}; uint32_t auto_gain{0}; float volume_multiplier{0.0f}; - void encode(ProtoWriteBuffer buffer) const override; - void calculate_size(ProtoSize &size) const override; + void encode(ProtoWriteBuffer &buffer) const; + uint32_t calculate_size() const; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; #endif @@ -2379,15 +2367,15 @@ class VoiceAssistantRequest final : public ProtoMessage { static constexpr uint8_t MESSAGE_TYPE = 90; static constexpr uint8_t ESTIMATED_SIZE = 41; #ifdef HAS_PROTO_MESSAGE_DUMP - const char *message_name() const override { return "voice_assistant_request"; } + const LogString *message_name() const override { return LOG_STR("voice_assistant_request"); } #endif bool start{false}; StringRef conversation_id{}; uint32_t flags{0}; VoiceAssistantAudioSettings audio_settings{}; StringRef wake_word_phrase{}; - void encode(ProtoWriteBuffer buffer) const override; - void calculate_size(ProtoSize &size) const override; + void encode(ProtoWriteBuffer &buffer) const; + uint32_t calculate_size() const; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; #endif @@ -2399,7 +2387,7 @@ class VoiceAssistantResponse final : public ProtoDecodableMessage { static constexpr uint8_t MESSAGE_TYPE = 91; static constexpr uint8_t ESTIMATED_SIZE = 6; #ifdef HAS_PROTO_MESSAGE_DUMP - const char *message_name() const override { return "voice_assistant_response"; } + const LogString *message_name() const override { return LOG_STR("voice_assistant_response"); } #endif uint32_t port{0}; bool error{false}; @@ -2408,7 +2396,7 @@ class VoiceAssistantResponse final : public ProtoDecodableMessage { #endif protected: - bool decode_varint(uint32_t field_id, ProtoVarInt value) override; + bool decode_varint(uint32_t field_id, proto_varint_value_t value) override; }; class VoiceAssistantEventData final : public ProtoDecodableMessage { public: @@ -2426,7 +2414,7 @@ class VoiceAssistantEventResponse final : public ProtoDecodableMessage { static constexpr uint8_t MESSAGE_TYPE = 92; static constexpr uint8_t ESTIMATED_SIZE = 36; #ifdef HAS_PROTO_MESSAGE_DUMP - const char *message_name() const override { return "voice_assistant_event_response"; } + const LogString *message_name() const override { return LOG_STR("voice_assistant_event_response"); } #endif enums::VoiceAssistantEvent event_type{}; std::vector data{}; @@ -2436,34 +2424,34 @@ class VoiceAssistantEventResponse final : public ProtoDecodableMessage { protected: bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override; - bool decode_varint(uint32_t field_id, ProtoVarInt value) override; + bool decode_varint(uint32_t field_id, proto_varint_value_t value) override; }; class VoiceAssistantAudio final : public ProtoDecodableMessage { public: static constexpr uint8_t MESSAGE_TYPE = 106; static constexpr uint8_t ESTIMATED_SIZE = 21; #ifdef HAS_PROTO_MESSAGE_DUMP - const char *message_name() const override { return "voice_assistant_audio"; } + const LogString *message_name() const override { return LOG_STR("voice_assistant_audio"); } #endif const uint8_t *data{nullptr}; uint16_t data_len{0}; bool end{false}; - void encode(ProtoWriteBuffer buffer) const override; - void calculate_size(ProtoSize &size) const override; + void encode(ProtoWriteBuffer &buffer) const; + uint32_t calculate_size() const; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; #endif protected: bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override; - bool decode_varint(uint32_t field_id, ProtoVarInt value) override; + bool decode_varint(uint32_t field_id, proto_varint_value_t value) override; }; class VoiceAssistantTimerEventResponse final : public ProtoDecodableMessage { public: static constexpr uint8_t MESSAGE_TYPE = 115; static constexpr uint8_t ESTIMATED_SIZE = 30; #ifdef HAS_PROTO_MESSAGE_DUMP - const char *message_name() const override { return "voice_assistant_timer_event_response"; } + const LogString *message_name() const override { return LOG_STR("voice_assistant_timer_event_response"); } #endif enums::VoiceAssistantTimerEvent event_type{}; StringRef timer_id{}; @@ -2477,14 +2465,14 @@ class VoiceAssistantTimerEventResponse final : public ProtoDecodableMessage { protected: bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override; - bool decode_varint(uint32_t field_id, ProtoVarInt value) override; + bool decode_varint(uint32_t field_id, proto_varint_value_t value) override; }; class VoiceAssistantAnnounceRequest final : public ProtoDecodableMessage { public: static constexpr uint8_t MESSAGE_TYPE = 119; static constexpr uint8_t ESTIMATED_SIZE = 29; #ifdef HAS_PROTO_MESSAGE_DUMP - const char *message_name() const override { return "voice_assistant_announce_request"; } + const LogString *message_name() const override { return LOG_STR("voice_assistant_announce_request"); } #endif StringRef media_id{}; StringRef text{}; @@ -2496,18 +2484,18 @@ class VoiceAssistantAnnounceRequest final : public ProtoDecodableMessage { protected: bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override; - bool decode_varint(uint32_t field_id, ProtoVarInt value) override; + bool decode_varint(uint32_t field_id, proto_varint_value_t value) override; }; class VoiceAssistantAnnounceFinished final : public ProtoMessage { public: static constexpr uint8_t MESSAGE_TYPE = 120; static constexpr uint8_t ESTIMATED_SIZE = 2; #ifdef HAS_PROTO_MESSAGE_DUMP - const char *message_name() const override { return "voice_assistant_announce_finished"; } + const LogString *message_name() const override { return LOG_STR("voice_assistant_announce_finished"); } #endif bool success{false}; - void encode(ProtoWriteBuffer buffer) const override; - void calculate_size(ProtoSize &size) const override; + void encode(ProtoWriteBuffer &buffer) const; + uint32_t calculate_size() const; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; #endif @@ -2519,8 +2507,8 @@ class VoiceAssistantWakeWord final : public ProtoMessage { StringRef id{}; StringRef wake_word{}; std::vector trained_languages{}; - void encode(ProtoWriteBuffer buffer) const override; - void calculate_size(ProtoSize &size) const override; + void encode(ProtoWriteBuffer &buffer) const; + uint32_t calculate_size() const; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; #endif @@ -2542,14 +2530,14 @@ class VoiceAssistantExternalWakeWord final : public ProtoDecodableMessage { protected: bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override; - bool decode_varint(uint32_t field_id, ProtoVarInt value) override; + bool decode_varint(uint32_t field_id, proto_varint_value_t value) override; }; class VoiceAssistantConfigurationRequest final : public ProtoDecodableMessage { public: static constexpr uint8_t MESSAGE_TYPE = 121; static constexpr uint8_t ESTIMATED_SIZE = 34; #ifdef HAS_PROTO_MESSAGE_DUMP - const char *message_name() const override { return "voice_assistant_configuration_request"; } + const LogString *message_name() const override { return LOG_STR("voice_assistant_configuration_request"); } #endif std::vector external_wake_words{}; #ifdef HAS_PROTO_MESSAGE_DUMP @@ -2564,13 +2552,13 @@ class VoiceAssistantConfigurationResponse final : public ProtoMessage { static constexpr uint8_t MESSAGE_TYPE = 122; static constexpr uint8_t ESTIMATED_SIZE = 56; #ifdef HAS_PROTO_MESSAGE_DUMP - const char *message_name() const override { return "voice_assistant_configuration_response"; } + const LogString *message_name() const override { return LOG_STR("voice_assistant_configuration_response"); } #endif std::vector available_wake_words{}; const std::vector *active_wake_words{}; uint32_t max_active_wake_words{0}; - void encode(ProtoWriteBuffer buffer) const override; - void calculate_size(ProtoSize &size) const override; + void encode(ProtoWriteBuffer &buffer) const; + uint32_t calculate_size() const; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; #endif @@ -2582,7 +2570,7 @@ class VoiceAssistantSetConfiguration final : public ProtoDecodableMessage { static constexpr uint8_t MESSAGE_TYPE = 123; static constexpr uint8_t ESTIMATED_SIZE = 18; #ifdef HAS_PROTO_MESSAGE_DUMP - const char *message_name() const override { return "voice_assistant_set_configuration"; } + const LogString *message_name() const override { return LOG_STR("voice_assistant_set_configuration"); } #endif std::vector active_wake_words{}; #ifdef HAS_PROTO_MESSAGE_DUMP @@ -2599,13 +2587,13 @@ class ListEntitiesAlarmControlPanelResponse final : public InfoResponseProtoMess static constexpr uint8_t MESSAGE_TYPE = 94; static constexpr uint8_t ESTIMATED_SIZE = 48; #ifdef HAS_PROTO_MESSAGE_DUMP - const char *message_name() const override { return "list_entities_alarm_control_panel_response"; } + const LogString *message_name() const override { return LOG_STR("list_entities_alarm_control_panel_response"); } #endif uint32_t supported_features{0}; bool requires_code{false}; bool requires_code_to_arm{false}; - void encode(ProtoWriteBuffer buffer) const override; - void calculate_size(ProtoSize &size) const override; + void encode(ProtoWriteBuffer &buffer) const; + uint32_t calculate_size() const; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; #endif @@ -2617,11 +2605,11 @@ class AlarmControlPanelStateResponse final : public StateResponseProtoMessage { static constexpr uint8_t MESSAGE_TYPE = 95; static constexpr uint8_t ESTIMATED_SIZE = 11; #ifdef HAS_PROTO_MESSAGE_DUMP - const char *message_name() const override { return "alarm_control_panel_state_response"; } + const LogString *message_name() const override { return LOG_STR("alarm_control_panel_state_response"); } #endif enums::AlarmControlPanelState state{}; - void encode(ProtoWriteBuffer buffer) const override; - void calculate_size(ProtoSize &size) const override; + void encode(ProtoWriteBuffer &buffer) const; + uint32_t calculate_size() const; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; #endif @@ -2633,7 +2621,7 @@ class AlarmControlPanelCommandRequest final : public CommandProtoMessage { static constexpr uint8_t MESSAGE_TYPE = 96; static constexpr uint8_t ESTIMATED_SIZE = 20; #ifdef HAS_PROTO_MESSAGE_DUMP - const char *message_name() const override { return "alarm_control_panel_command_request"; } + const LogString *message_name() const override { return LOG_STR("alarm_control_panel_command_request"); } #endif enums::AlarmControlPanelStateCommand command{}; StringRef code{}; @@ -2644,7 +2632,7 @@ class AlarmControlPanelCommandRequest final : public CommandProtoMessage { protected: bool decode_32bit(uint32_t field_id, Proto32Bit value) override; bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override; - bool decode_varint(uint32_t field_id, ProtoVarInt value) override; + bool decode_varint(uint32_t field_id, proto_varint_value_t value) override; }; #endif #ifdef USE_TEXT @@ -2653,14 +2641,14 @@ class ListEntitiesTextResponse final : public InfoResponseProtoMessage { static constexpr uint8_t MESSAGE_TYPE = 97; static constexpr uint8_t ESTIMATED_SIZE = 59; #ifdef HAS_PROTO_MESSAGE_DUMP - const char *message_name() const override { return "list_entities_text_response"; } + const LogString *message_name() const override { return LOG_STR("list_entities_text_response"); } #endif uint32_t min_length{0}; uint32_t max_length{0}; StringRef pattern{}; enums::TextMode mode{}; - void encode(ProtoWriteBuffer buffer) const override; - void calculate_size(ProtoSize &size) const override; + void encode(ProtoWriteBuffer &buffer) const; + uint32_t calculate_size() const; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; #endif @@ -2672,12 +2660,12 @@ class TextStateResponse final : public StateResponseProtoMessage { static constexpr uint8_t MESSAGE_TYPE = 98; static constexpr uint8_t ESTIMATED_SIZE = 20; #ifdef HAS_PROTO_MESSAGE_DUMP - const char *message_name() const override { return "text_state_response"; } + const LogString *message_name() const override { return LOG_STR("text_state_response"); } #endif StringRef state{}; bool missing_state{false}; - void encode(ProtoWriteBuffer buffer) const override; - void calculate_size(ProtoSize &size) const override; + void encode(ProtoWriteBuffer &buffer) const; + uint32_t calculate_size() const; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; #endif @@ -2689,7 +2677,7 @@ class TextCommandRequest final : public CommandProtoMessage { static constexpr uint8_t MESSAGE_TYPE = 99; static constexpr uint8_t ESTIMATED_SIZE = 18; #ifdef HAS_PROTO_MESSAGE_DUMP - const char *message_name() const override { return "text_command_request"; } + const LogString *message_name() const override { return LOG_STR("text_command_request"); } #endif StringRef state{}; #ifdef HAS_PROTO_MESSAGE_DUMP @@ -2699,7 +2687,7 @@ class TextCommandRequest final : public CommandProtoMessage { protected: bool decode_32bit(uint32_t field_id, Proto32Bit value) override; bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override; - bool decode_varint(uint32_t field_id, ProtoVarInt value) override; + bool decode_varint(uint32_t field_id, proto_varint_value_t value) override; }; #endif #ifdef USE_DATETIME_DATE @@ -2708,10 +2696,10 @@ class ListEntitiesDateResponse final : public InfoResponseProtoMessage { static constexpr uint8_t MESSAGE_TYPE = 100; static constexpr uint8_t ESTIMATED_SIZE = 40; #ifdef HAS_PROTO_MESSAGE_DUMP - const char *message_name() const override { return "list_entities_date_response"; } + const LogString *message_name() const override { return LOG_STR("list_entities_date_response"); } #endif - void encode(ProtoWriteBuffer buffer) const override; - void calculate_size(ProtoSize &size) const override; + void encode(ProtoWriteBuffer &buffer) const; + uint32_t calculate_size() const; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; #endif @@ -2723,14 +2711,14 @@ class DateStateResponse final : public StateResponseProtoMessage { static constexpr uint8_t MESSAGE_TYPE = 101; static constexpr uint8_t ESTIMATED_SIZE = 23; #ifdef HAS_PROTO_MESSAGE_DUMP - const char *message_name() const override { return "date_state_response"; } + const LogString *message_name() const override { return LOG_STR("date_state_response"); } #endif bool missing_state{false}; uint32_t year{0}; uint32_t month{0}; uint32_t day{0}; - void encode(ProtoWriteBuffer buffer) const override; - void calculate_size(ProtoSize &size) const override; + void encode(ProtoWriteBuffer &buffer) const; + uint32_t calculate_size() const; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; #endif @@ -2742,7 +2730,7 @@ class DateCommandRequest final : public CommandProtoMessage { static constexpr uint8_t MESSAGE_TYPE = 102; static constexpr uint8_t ESTIMATED_SIZE = 21; #ifdef HAS_PROTO_MESSAGE_DUMP - const char *message_name() const override { return "date_command_request"; } + const LogString *message_name() const override { return LOG_STR("date_command_request"); } #endif uint32_t year{0}; uint32_t month{0}; @@ -2753,7 +2741,7 @@ class DateCommandRequest final : public CommandProtoMessage { protected: bool decode_32bit(uint32_t field_id, Proto32Bit value) override; - bool decode_varint(uint32_t field_id, ProtoVarInt value) override; + bool decode_varint(uint32_t field_id, proto_varint_value_t value) override; }; #endif #ifdef USE_DATETIME_TIME @@ -2762,10 +2750,10 @@ class ListEntitiesTimeResponse final : public InfoResponseProtoMessage { static constexpr uint8_t MESSAGE_TYPE = 103; static constexpr uint8_t ESTIMATED_SIZE = 40; #ifdef HAS_PROTO_MESSAGE_DUMP - const char *message_name() const override { return "list_entities_time_response"; } + const LogString *message_name() const override { return LOG_STR("list_entities_time_response"); } #endif - void encode(ProtoWriteBuffer buffer) const override; - void calculate_size(ProtoSize &size) const override; + void encode(ProtoWriteBuffer &buffer) const; + uint32_t calculate_size() const; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; #endif @@ -2777,14 +2765,14 @@ class TimeStateResponse final : public StateResponseProtoMessage { static constexpr uint8_t MESSAGE_TYPE = 104; static constexpr uint8_t ESTIMATED_SIZE = 23; #ifdef HAS_PROTO_MESSAGE_DUMP - const char *message_name() const override { return "time_state_response"; } + const LogString *message_name() const override { return LOG_STR("time_state_response"); } #endif bool missing_state{false}; uint32_t hour{0}; uint32_t minute{0}; uint32_t second{0}; - void encode(ProtoWriteBuffer buffer) const override; - void calculate_size(ProtoSize &size) const override; + void encode(ProtoWriteBuffer &buffer) const; + uint32_t calculate_size() const; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; #endif @@ -2796,7 +2784,7 @@ class TimeCommandRequest final : public CommandProtoMessage { static constexpr uint8_t MESSAGE_TYPE = 105; static constexpr uint8_t ESTIMATED_SIZE = 21; #ifdef HAS_PROTO_MESSAGE_DUMP - const char *message_name() const override { return "time_command_request"; } + const LogString *message_name() const override { return LOG_STR("time_command_request"); } #endif uint32_t hour{0}; uint32_t minute{0}; @@ -2807,7 +2795,7 @@ class TimeCommandRequest final : public CommandProtoMessage { protected: bool decode_32bit(uint32_t field_id, Proto32Bit value) override; - bool decode_varint(uint32_t field_id, ProtoVarInt value) override; + bool decode_varint(uint32_t field_id, proto_varint_value_t value) override; }; #endif #ifdef USE_EVENT @@ -2816,12 +2804,12 @@ class ListEntitiesEventResponse final : public InfoResponseProtoMessage { static constexpr uint8_t MESSAGE_TYPE = 107; static constexpr uint8_t ESTIMATED_SIZE = 67; #ifdef HAS_PROTO_MESSAGE_DUMP - const char *message_name() const override { return "list_entities_event_response"; } + const LogString *message_name() const override { return LOG_STR("list_entities_event_response"); } #endif StringRef device_class{}; const FixedVector *event_types{}; - void encode(ProtoWriteBuffer buffer) const override; - void calculate_size(ProtoSize &size) const override; + void encode(ProtoWriteBuffer &buffer) const; + uint32_t calculate_size() const; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; #endif @@ -2833,11 +2821,11 @@ class EventResponse final : public StateResponseProtoMessage { static constexpr uint8_t MESSAGE_TYPE = 108; static constexpr uint8_t ESTIMATED_SIZE = 18; #ifdef HAS_PROTO_MESSAGE_DUMP - const char *message_name() const override { return "event_response"; } + const LogString *message_name() const override { return LOG_STR("event_response"); } #endif StringRef event_type{}; - void encode(ProtoWriteBuffer buffer) const override; - void calculate_size(ProtoSize &size) const override; + void encode(ProtoWriteBuffer &buffer) const; + uint32_t calculate_size() const; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; #endif @@ -2851,14 +2839,14 @@ class ListEntitiesValveResponse final : public InfoResponseProtoMessage { static constexpr uint8_t MESSAGE_TYPE = 109; static constexpr uint8_t ESTIMATED_SIZE = 55; #ifdef HAS_PROTO_MESSAGE_DUMP - const char *message_name() const override { return "list_entities_valve_response"; } + const LogString *message_name() const override { return LOG_STR("list_entities_valve_response"); } #endif StringRef device_class{}; bool assumed_state{false}; bool supports_position{false}; bool supports_stop{false}; - void encode(ProtoWriteBuffer buffer) const override; - void calculate_size(ProtoSize &size) const override; + void encode(ProtoWriteBuffer &buffer) const; + uint32_t calculate_size() const; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; #endif @@ -2870,12 +2858,12 @@ class ValveStateResponse final : public StateResponseProtoMessage { static constexpr uint8_t MESSAGE_TYPE = 110; static constexpr uint8_t ESTIMATED_SIZE = 16; #ifdef HAS_PROTO_MESSAGE_DUMP - const char *message_name() const override { return "valve_state_response"; } + const LogString *message_name() const override { return LOG_STR("valve_state_response"); } #endif float position{0.0f}; enums::ValveOperation current_operation{}; - void encode(ProtoWriteBuffer buffer) const override; - void calculate_size(ProtoSize &size) const override; + void encode(ProtoWriteBuffer &buffer) const; + uint32_t calculate_size() const; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; #endif @@ -2887,7 +2875,7 @@ class ValveCommandRequest final : public CommandProtoMessage { static constexpr uint8_t MESSAGE_TYPE = 111; static constexpr uint8_t ESTIMATED_SIZE = 18; #ifdef HAS_PROTO_MESSAGE_DUMP - const char *message_name() const override { return "valve_command_request"; } + const LogString *message_name() const override { return LOG_STR("valve_command_request"); } #endif bool has_position{false}; float position{0.0f}; @@ -2898,7 +2886,7 @@ class ValveCommandRequest final : public CommandProtoMessage { protected: bool decode_32bit(uint32_t field_id, Proto32Bit value) override; - bool decode_varint(uint32_t field_id, ProtoVarInt value) override; + bool decode_varint(uint32_t field_id, proto_varint_value_t value) override; }; #endif #ifdef USE_DATETIME_DATETIME @@ -2907,10 +2895,10 @@ class ListEntitiesDateTimeResponse final : public InfoResponseProtoMessage { static constexpr uint8_t MESSAGE_TYPE = 112; static constexpr uint8_t ESTIMATED_SIZE = 40; #ifdef HAS_PROTO_MESSAGE_DUMP - const char *message_name() const override { return "list_entities_date_time_response"; } + const LogString *message_name() const override { return LOG_STR("list_entities_date_time_response"); } #endif - void encode(ProtoWriteBuffer buffer) const override; - void calculate_size(ProtoSize &size) const override; + void encode(ProtoWriteBuffer &buffer) const; + uint32_t calculate_size() const; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; #endif @@ -2922,12 +2910,12 @@ class DateTimeStateResponse final : public StateResponseProtoMessage { static constexpr uint8_t MESSAGE_TYPE = 113; static constexpr uint8_t ESTIMATED_SIZE = 16; #ifdef HAS_PROTO_MESSAGE_DUMP - const char *message_name() const override { return "date_time_state_response"; } + const LogString *message_name() const override { return LOG_STR("date_time_state_response"); } #endif bool missing_state{false}; uint32_t epoch_seconds{0}; - void encode(ProtoWriteBuffer buffer) const override; - void calculate_size(ProtoSize &size) const override; + void encode(ProtoWriteBuffer &buffer) const; + uint32_t calculate_size() const; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; #endif @@ -2939,7 +2927,7 @@ class DateTimeCommandRequest final : public CommandProtoMessage { static constexpr uint8_t MESSAGE_TYPE = 114; static constexpr uint8_t ESTIMATED_SIZE = 14; #ifdef HAS_PROTO_MESSAGE_DUMP - const char *message_name() const override { return "date_time_command_request"; } + const LogString *message_name() const override { return LOG_STR("date_time_command_request"); } #endif uint32_t epoch_seconds{0}; #ifdef HAS_PROTO_MESSAGE_DUMP @@ -2948,7 +2936,7 @@ class DateTimeCommandRequest final : public CommandProtoMessage { protected: bool decode_32bit(uint32_t field_id, Proto32Bit value) override; - bool decode_varint(uint32_t field_id, ProtoVarInt value) override; + bool decode_varint(uint32_t field_id, proto_varint_value_t value) override; }; #endif #ifdef USE_UPDATE @@ -2957,11 +2945,11 @@ class ListEntitiesUpdateResponse final : public InfoResponseProtoMessage { static constexpr uint8_t MESSAGE_TYPE = 116; static constexpr uint8_t ESTIMATED_SIZE = 49; #ifdef HAS_PROTO_MESSAGE_DUMP - const char *message_name() const override { return "list_entities_update_response"; } + const LogString *message_name() const override { return LOG_STR("list_entities_update_response"); } #endif StringRef device_class{}; - void encode(ProtoWriteBuffer buffer) const override; - void calculate_size(ProtoSize &size) const override; + void encode(ProtoWriteBuffer &buffer) const; + uint32_t calculate_size() const; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; #endif @@ -2973,7 +2961,7 @@ class UpdateStateResponse final : public StateResponseProtoMessage { static constexpr uint8_t MESSAGE_TYPE = 117; static constexpr uint8_t ESTIMATED_SIZE = 65; #ifdef HAS_PROTO_MESSAGE_DUMP - const char *message_name() const override { return "update_state_response"; } + const LogString *message_name() const override { return LOG_STR("update_state_response"); } #endif bool missing_state{false}; bool in_progress{false}; @@ -2984,8 +2972,8 @@ class UpdateStateResponse final : public StateResponseProtoMessage { StringRef title{}; StringRef release_summary{}; StringRef release_url{}; - void encode(ProtoWriteBuffer buffer) const override; - void calculate_size(ProtoSize &size) const override; + void encode(ProtoWriteBuffer &buffer) const; + uint32_t calculate_size() const; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; #endif @@ -2997,7 +2985,7 @@ class UpdateCommandRequest final : public CommandProtoMessage { static constexpr uint8_t MESSAGE_TYPE = 118; static constexpr uint8_t ESTIMATED_SIZE = 11; #ifdef HAS_PROTO_MESSAGE_DUMP - const char *message_name() const override { return "update_command_request"; } + const LogString *message_name() const override { return LOG_STR("update_command_request"); } #endif enums::UpdateCommand command{}; #ifdef HAS_PROTO_MESSAGE_DUMP @@ -3006,7 +2994,7 @@ class UpdateCommandRequest final : public CommandProtoMessage { protected: bool decode_32bit(uint32_t field_id, Proto32Bit value) override; - bool decode_varint(uint32_t field_id, ProtoVarInt value) override; + bool decode_varint(uint32_t field_id, proto_varint_value_t value) override; }; #endif #ifdef USE_ZWAVE_PROXY @@ -3015,12 +3003,12 @@ class ZWaveProxyFrame final : public ProtoDecodableMessage { static constexpr uint8_t MESSAGE_TYPE = 128; static constexpr uint8_t ESTIMATED_SIZE = 19; #ifdef HAS_PROTO_MESSAGE_DUMP - const char *message_name() const override { return "z_wave_proxy_frame"; } + const LogString *message_name() const override { return LOG_STR("z_wave_proxy_frame"); } #endif const uint8_t *data{nullptr}; uint16_t data_len{0}; - void encode(ProtoWriteBuffer buffer) const override; - void calculate_size(ProtoSize &size) const override; + void encode(ProtoWriteBuffer &buffer) const; + uint32_t calculate_size() const; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; #endif @@ -3033,33 +3021,34 @@ class ZWaveProxyRequest final : public ProtoDecodableMessage { static constexpr uint8_t MESSAGE_TYPE = 129; static constexpr uint8_t ESTIMATED_SIZE = 21; #ifdef HAS_PROTO_MESSAGE_DUMP - const char *message_name() const override { return "z_wave_proxy_request"; } + const LogString *message_name() const override { return LOG_STR("z_wave_proxy_request"); } #endif enums::ZWaveProxyRequestType type{}; const uint8_t *data{nullptr}; uint16_t data_len{0}; - void encode(ProtoWriteBuffer buffer) const override; - void calculate_size(ProtoSize &size) const override; + void encode(ProtoWriteBuffer &buffer) const; + uint32_t calculate_size() const; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; #endif protected: bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override; - bool decode_varint(uint32_t field_id, ProtoVarInt value) override; + bool decode_varint(uint32_t field_id, proto_varint_value_t value) override; }; #endif #ifdef USE_INFRARED class ListEntitiesInfraredResponse final : public InfoResponseProtoMessage { public: static constexpr uint8_t MESSAGE_TYPE = 135; - static constexpr uint8_t ESTIMATED_SIZE = 44; + static constexpr uint8_t ESTIMATED_SIZE = 48; #ifdef HAS_PROTO_MESSAGE_DUMP - const char *message_name() const override { return "list_entities_infrared_response"; } + const LogString *message_name() const override { return LOG_STR("list_entities_infrared_response"); } #endif uint32_t capabilities{0}; - void encode(ProtoWriteBuffer buffer) const override; - void calculate_size(ProtoSize &size) const override; + uint32_t receiver_frequency{0}; + void encode(ProtoWriteBuffer &buffer) const; + uint32_t calculate_size() const; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; #endif @@ -3073,7 +3062,7 @@ class InfraredRFTransmitRawTimingsRequest final : public ProtoDecodableMessage { static constexpr uint8_t MESSAGE_TYPE = 136; static constexpr uint8_t ESTIMATED_SIZE = 220; #ifdef HAS_PROTO_MESSAGE_DUMP - const char *message_name() const override { return "infrared_rf_transmit_raw_timings_request"; } + const LogString *message_name() const override { return LOG_STR("infrared_rf_transmit_raw_timings_request"); } #endif #ifdef USE_DEVICES uint32_t device_id{0}; @@ -3091,22 +3080,205 @@ class InfraredRFTransmitRawTimingsRequest final : public ProtoDecodableMessage { protected: bool decode_32bit(uint32_t field_id, Proto32Bit value) override; bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override; - bool decode_varint(uint32_t field_id, ProtoVarInt value) override; + bool decode_varint(uint32_t field_id, proto_varint_value_t value) override; }; class InfraredRFReceiveEvent final : public ProtoMessage { public: static constexpr uint8_t MESSAGE_TYPE = 137; static constexpr uint8_t ESTIMATED_SIZE = 17; #ifdef HAS_PROTO_MESSAGE_DUMP - const char *message_name() const override { return "infrared_rf_receive_event"; } + const LogString *message_name() const override { return LOG_STR("infrared_rf_receive_event"); } #endif #ifdef USE_DEVICES uint32_t device_id{0}; #endif uint32_t key{0}; const std::vector *timings{}; - void encode(ProtoWriteBuffer buffer) const override; - void calculate_size(ProtoSize &size) const override; + void encode(ProtoWriteBuffer &buffer) const; + uint32_t calculate_size() const; +#ifdef HAS_PROTO_MESSAGE_DUMP + const char *dump_to(DumpBuffer &out) const override; +#endif + + protected: +}; +#endif +#ifdef USE_SERIAL_PROXY +class SerialProxyConfigureRequest final : public ProtoDecodableMessage { + public: + static constexpr uint8_t MESSAGE_TYPE = 138; + static constexpr uint8_t ESTIMATED_SIZE = 20; +#ifdef HAS_PROTO_MESSAGE_DUMP + const LogString *message_name() const override { return LOG_STR("serial_proxy_configure_request"); } +#endif + uint32_t instance{0}; + uint32_t baudrate{0}; + bool flow_control{false}; + enums::SerialProxyParity parity{}; + uint32_t stop_bits{0}; + uint32_t data_size{0}; +#ifdef HAS_PROTO_MESSAGE_DUMP + const char *dump_to(DumpBuffer &out) const override; +#endif + + protected: + bool decode_varint(uint32_t field_id, proto_varint_value_t value) override; +}; +class SerialProxyDataReceived final : public ProtoMessage { + public: + static constexpr uint8_t MESSAGE_TYPE = 139; + static constexpr uint8_t ESTIMATED_SIZE = 23; +#ifdef HAS_PROTO_MESSAGE_DUMP + const LogString *message_name() const override { return LOG_STR("serial_proxy_data_received"); } +#endif + uint32_t instance{0}; + const uint8_t *data_ptr_{nullptr}; + size_t data_len_{0}; + void set_data(const uint8_t *data, size_t len) { + this->data_ptr_ = data; + this->data_len_ = len; + } + void encode(ProtoWriteBuffer &buffer) const; + uint32_t calculate_size() const; +#ifdef HAS_PROTO_MESSAGE_DUMP + const char *dump_to(DumpBuffer &out) const override; +#endif + + protected: +}; +class SerialProxyWriteRequest final : public ProtoDecodableMessage { + public: + static constexpr uint8_t MESSAGE_TYPE = 140; + static constexpr uint8_t ESTIMATED_SIZE = 23; +#ifdef HAS_PROTO_MESSAGE_DUMP + const LogString *message_name() const override { return LOG_STR("serial_proxy_write_request"); } +#endif + uint32_t instance{0}; + const uint8_t *data{nullptr}; + uint16_t data_len{0}; +#ifdef HAS_PROTO_MESSAGE_DUMP + const char *dump_to(DumpBuffer &out) const override; +#endif + + protected: + bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override; + bool decode_varint(uint32_t field_id, proto_varint_value_t value) override; +}; +class SerialProxySetModemPinsRequest final : public ProtoDecodableMessage { + public: + static constexpr uint8_t MESSAGE_TYPE = 141; + static constexpr uint8_t ESTIMATED_SIZE = 8; +#ifdef HAS_PROTO_MESSAGE_DUMP + const LogString *message_name() const override { return LOG_STR("serial_proxy_set_modem_pins_request"); } +#endif + uint32_t instance{0}; + uint32_t line_states{0}; +#ifdef HAS_PROTO_MESSAGE_DUMP + const char *dump_to(DumpBuffer &out) const override; +#endif + + protected: + bool decode_varint(uint32_t field_id, proto_varint_value_t value) override; +}; +class SerialProxyGetModemPinsRequest final : public ProtoDecodableMessage { + public: + static constexpr uint8_t MESSAGE_TYPE = 142; + static constexpr uint8_t ESTIMATED_SIZE = 4; +#ifdef HAS_PROTO_MESSAGE_DUMP + const LogString *message_name() const override { return LOG_STR("serial_proxy_get_modem_pins_request"); } +#endif + uint32_t instance{0}; +#ifdef HAS_PROTO_MESSAGE_DUMP + const char *dump_to(DumpBuffer &out) const override; +#endif + + protected: + bool decode_varint(uint32_t field_id, proto_varint_value_t value) override; +}; +class SerialProxyGetModemPinsResponse final : public ProtoMessage { + public: + static constexpr uint8_t MESSAGE_TYPE = 143; + static constexpr uint8_t ESTIMATED_SIZE = 8; +#ifdef HAS_PROTO_MESSAGE_DUMP + const LogString *message_name() const override { return LOG_STR("serial_proxy_get_modem_pins_response"); } +#endif + uint32_t instance{0}; + uint32_t line_states{0}; + void encode(ProtoWriteBuffer &buffer) const; + uint32_t calculate_size() const; +#ifdef HAS_PROTO_MESSAGE_DUMP + const char *dump_to(DumpBuffer &out) const override; +#endif + + protected: +}; +class SerialProxyRequest final : public ProtoDecodableMessage { + public: + static constexpr uint8_t MESSAGE_TYPE = 144; + static constexpr uint8_t ESTIMATED_SIZE = 6; +#ifdef HAS_PROTO_MESSAGE_DUMP + const LogString *message_name() const override { return LOG_STR("serial_proxy_request"); } +#endif + uint32_t instance{0}; + enums::SerialProxyRequestType type{}; +#ifdef HAS_PROTO_MESSAGE_DUMP + const char *dump_to(DumpBuffer &out) const override; +#endif + + protected: + bool decode_varint(uint32_t field_id, proto_varint_value_t value) override; +}; +class SerialProxyRequestResponse final : public ProtoMessage { + public: + static constexpr uint8_t MESSAGE_TYPE = 147; + static constexpr uint8_t ESTIMATED_SIZE = 17; +#ifdef HAS_PROTO_MESSAGE_DUMP + const LogString *message_name() const override { return LOG_STR("serial_proxy_request_response"); } +#endif + uint32_t instance{0}; + enums::SerialProxyRequestType type{}; + enums::SerialProxyStatus status{}; + StringRef error_message{}; + void encode(ProtoWriteBuffer &buffer) const; + uint32_t calculate_size() const; +#ifdef HAS_PROTO_MESSAGE_DUMP + const char *dump_to(DumpBuffer &out) const override; +#endif + + protected: +}; +#endif +#ifdef USE_BLUETOOTH_PROXY +class BluetoothSetConnectionParamsRequest final : public ProtoDecodableMessage { + public: + static constexpr uint8_t MESSAGE_TYPE = 145; + static constexpr uint8_t ESTIMATED_SIZE = 20; +#ifdef HAS_PROTO_MESSAGE_DUMP + const LogString *message_name() const override { return LOG_STR("bluetooth_set_connection_params_request"); } +#endif + uint64_t address{0}; + uint32_t min_interval{0}; + uint32_t max_interval{0}; + uint32_t latency{0}; + uint32_t timeout{0}; +#ifdef HAS_PROTO_MESSAGE_DUMP + const char *dump_to(DumpBuffer &out) const override; +#endif + + protected: + bool decode_varint(uint32_t field_id, proto_varint_value_t value) override; +}; +class BluetoothSetConnectionParamsResponse final : public ProtoMessage { + public: + static constexpr uint8_t MESSAGE_TYPE = 146; + static constexpr uint8_t ESTIMATED_SIZE = 8; +#ifdef HAS_PROTO_MESSAGE_DUMP + const LogString *message_name() const override { return LOG_STR("bluetooth_set_connection_params_response"); } +#endif + uint64_t address{0}; + int32_t error{0}; + void encode(ProtoWriteBuffer &buffer) const; + uint32_t calculate_size() const; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; #endif diff --git a/esphome/components/api/api_pb2_defines.h b/esphome/components/api/api_pb2_defines.h new file mode 100644 index 0000000000..8ebd60fb5d --- /dev/null +++ b/esphome/components/api/api_pb2_defines.h @@ -0,0 +1,12 @@ +// This file was automatically generated with a tool. +// See script/api_protobuf/api_protobuf.py +#pragma once + +#include "esphome/core/defines.h" +#ifdef USE_BLUETOOTH_PROXY +#ifndef USE_API_VARINT64 +#define USE_API_VARINT64 +#endif +#endif + +namespace esphome::api {} // namespace esphome::api diff --git a/esphome/components/api/api_pb2_dump.cpp b/esphome/components/api/api_pb2_dump.cpp index 29121f05e0..640c347371 100644 --- a/esphome/components/api/api_pb2_dump.cpp +++ b/esphome/components/api/api_pb2_dump.cpp @@ -2,6 +2,7 @@ // See script/api_protobuf/api_protobuf.py #include "api_pb2.h" #include "esphome/core/helpers.h" +#include "esphome/core/progmem.h" #include @@ -9,36 +10,46 @@ namespace esphome::api { +#ifdef USE_ESP8266 +// Out-of-line to avoid inlining strlen_P/memcpy_P at every call site +void DumpBuffer::append_p_esp8266(const char *str) { + size_t len = strlen_P(str); + size_t space = CAPACITY - 1 - pos_; + if (len > space) + len = space; + if (len > 0) { + memcpy_P(buf_ + pos_, str, len); + pos_ += len; + buf_[pos_] = '\0'; + } +} +#endif + // Helper function to append a quoted string, handling empty StringRef static inline void append_quoted_string(DumpBuffer &out, const StringRef &ref) { out.append("'"); if (!ref.empty()) { - out.append(ref.c_str()); + out.append(ref.c_str(), ref.size()); } out.append("'"); } // Common helpers for dump_field functions +// field_name is a PROGMEM pointer (flash on ESP8266, regular pointer on other platforms) static inline void append_field_prefix(DumpBuffer &out, const char *field_name, int indent) { - out.append(indent, ' ').append(field_name).append(": "); -} - -static inline void append_with_newline(DumpBuffer &out, const char *str) { - out.append(str); - out.append("\n"); + out.append(indent, ' ').append_p(field_name).append(": "); } static inline void append_uint(DumpBuffer &out, uint32_t value) { - char buf[16]; - snprintf(buf, sizeof(buf), "%" PRIu32, value); - out.append(buf); + out.set_pos(buf_append_printf(out.data(), DumpBuffer::CAPACITY, out.pos(), "%" PRIu32, value)); } // RAII helper for message dump formatting +// message_name is a PROGMEM pointer (flash on ESP8266, regular pointer on other platforms) class MessageDumpHelper { public: MessageDumpHelper(DumpBuffer &out, const char *message_name) : out_(out) { - out_.append(message_name); + out_.append_p(message_name); out_.append(" {\n"); } ~MessageDumpHelper() { out_.append(" }"); } @@ -48,32 +59,28 @@ class MessageDumpHelper { }; // Helper functions to reduce code duplication in dump methods +// field_name parameters are PROGMEM pointers (flash on ESP8266, regular pointers on other platforms) +// Not all overloads are used in every build (depends on enabled components) +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wunused-function" static void dump_field(DumpBuffer &out, const char *field_name, int32_t value, int indent = 2) { - char buffer[64]; append_field_prefix(out, field_name, indent); - snprintf(buffer, 64, "%" PRId32, value); - append_with_newline(out, buffer); + out.set_pos(buf_append_printf(out.data(), DumpBuffer::CAPACITY, out.pos(), "%" PRId32 "\n", value)); } static void dump_field(DumpBuffer &out, const char *field_name, uint32_t value, int indent = 2) { - char buffer[64]; append_field_prefix(out, field_name, indent); - snprintf(buffer, 64, "%" PRIu32, value); - append_with_newline(out, buffer); + out.set_pos(buf_append_printf(out.data(), DumpBuffer::CAPACITY, out.pos(), "%" PRIu32 "\n", value)); } static void dump_field(DumpBuffer &out, const char *field_name, float value, int indent = 2) { - char buffer[64]; append_field_prefix(out, field_name, indent); - snprintf(buffer, 64, "%g", value); - append_with_newline(out, buffer); + out.set_pos(buf_append_printf(out.data(), DumpBuffer::CAPACITY, out.pos(), "%g\n", value)); } static void dump_field(DumpBuffer &out, const char *field_name, uint64_t value, int indent = 2) { - char buffer[64]; append_field_prefix(out, field_name, indent); - snprintf(buffer, 64, "%" PRIu64, value); - append_with_newline(out, buffer); + out.set_pos(buf_append_printf(out.data(), DumpBuffer::CAPACITY, out.pos(), "%" PRIu64 "\n", value)); } static void dump_field(DumpBuffer &out, const char *field_name, bool value, int indent = 2) { @@ -100,44 +107,59 @@ static void dump_field(DumpBuffer &out, const char *field_name, const char *valu out.append("\n"); } +// proto_enum_to_string returns PROGMEM pointers, so use append_p template static void dump_field(DumpBuffer &out, const char *field_name, T value, int indent = 2) { append_field_prefix(out, field_name, indent); - out.append(proto_enum_to_string(value)); + out.append_p(proto_enum_to_string(value)); out.append("\n"); } // Helper for bytes fields - uses stack buffer to avoid heap allocation // Buffer sized for 160 bytes of data (480 chars with separators) to fit typical log buffer +// field_name is a PROGMEM pointer (flash on ESP8266, regular pointer on other platforms) static void dump_bytes_field(DumpBuffer &out, const char *field_name, const uint8_t *data, size_t len, int indent = 2) { char hex_buf[format_hex_pretty_size(160)]; append_field_prefix(out, field_name, indent); format_hex_pretty_to(hex_buf, data, len); - append_with_newline(out, hex_buf); + out.append(hex_buf).append("\n"); } +#pragma GCC diagnostic pop +template<> const char *proto_enum_to_string(enums::SerialProxyPortType value) { + switch (value) { + case enums::SERIAL_PROXY_PORT_TYPE_TTL: + return ESPHOME_PSTR("SERIAL_PROXY_PORT_TYPE_TTL"); + case enums::SERIAL_PROXY_PORT_TYPE_RS232: + return ESPHOME_PSTR("SERIAL_PROXY_PORT_TYPE_RS232"); + case enums::SERIAL_PROXY_PORT_TYPE_RS485: + return ESPHOME_PSTR("SERIAL_PROXY_PORT_TYPE_RS485"); + default: + return ESPHOME_PSTR("UNKNOWN"); + } +} template<> const char *proto_enum_to_string(enums::EntityCategory value) { switch (value) { case enums::ENTITY_CATEGORY_NONE: - return "ENTITY_CATEGORY_NONE"; + return ESPHOME_PSTR("ENTITY_CATEGORY_NONE"); case enums::ENTITY_CATEGORY_CONFIG: - return "ENTITY_CATEGORY_CONFIG"; + return ESPHOME_PSTR("ENTITY_CATEGORY_CONFIG"); case enums::ENTITY_CATEGORY_DIAGNOSTIC: - return "ENTITY_CATEGORY_DIAGNOSTIC"; + return ESPHOME_PSTR("ENTITY_CATEGORY_DIAGNOSTIC"); default: - return "UNKNOWN"; + return ESPHOME_PSTR("UNKNOWN"); } } #ifdef USE_COVER template<> const char *proto_enum_to_string(enums::CoverOperation value) { switch (value) { case enums::COVER_OPERATION_IDLE: - return "COVER_OPERATION_IDLE"; + return ESPHOME_PSTR("COVER_OPERATION_IDLE"); case enums::COVER_OPERATION_IS_OPENING: - return "COVER_OPERATION_IS_OPENING"; + return ESPHOME_PSTR("COVER_OPERATION_IS_OPENING"); case enums::COVER_OPERATION_IS_CLOSING: - return "COVER_OPERATION_IS_CLOSING"; + return ESPHOME_PSTR("COVER_OPERATION_IS_CLOSING"); default: - return "UNKNOWN"; + return ESPHOME_PSTR("UNKNOWN"); } } #endif @@ -145,11 +167,11 @@ template<> const char *proto_enum_to_string(enums::CoverO template<> const char *proto_enum_to_string(enums::FanDirection value) { switch (value) { case enums::FAN_DIRECTION_FORWARD: - return "FAN_DIRECTION_FORWARD"; + return ESPHOME_PSTR("FAN_DIRECTION_FORWARD"); case enums::FAN_DIRECTION_REVERSE: - return "FAN_DIRECTION_REVERSE"; + return ESPHOME_PSTR("FAN_DIRECTION_REVERSE"); default: - return "UNKNOWN"; + return ESPHOME_PSTR("UNKNOWN"); } } #endif @@ -157,29 +179,29 @@ template<> const char *proto_enum_to_string(enums::FanDirec template<> const char *proto_enum_to_string(enums::ColorMode value) { switch (value) { case enums::COLOR_MODE_UNKNOWN: - return "COLOR_MODE_UNKNOWN"; + return ESPHOME_PSTR("COLOR_MODE_UNKNOWN"); case enums::COLOR_MODE_ON_OFF: - return "COLOR_MODE_ON_OFF"; + return ESPHOME_PSTR("COLOR_MODE_ON_OFF"); case enums::COLOR_MODE_LEGACY_BRIGHTNESS: - return "COLOR_MODE_LEGACY_BRIGHTNESS"; + return ESPHOME_PSTR("COLOR_MODE_LEGACY_BRIGHTNESS"); case enums::COLOR_MODE_BRIGHTNESS: - return "COLOR_MODE_BRIGHTNESS"; + return ESPHOME_PSTR("COLOR_MODE_BRIGHTNESS"); case enums::COLOR_MODE_WHITE: - return "COLOR_MODE_WHITE"; + return ESPHOME_PSTR("COLOR_MODE_WHITE"); case enums::COLOR_MODE_COLOR_TEMPERATURE: - return "COLOR_MODE_COLOR_TEMPERATURE"; + return ESPHOME_PSTR("COLOR_MODE_COLOR_TEMPERATURE"); case enums::COLOR_MODE_COLD_WARM_WHITE: - return "COLOR_MODE_COLD_WARM_WHITE"; + return ESPHOME_PSTR("COLOR_MODE_COLD_WARM_WHITE"); case enums::COLOR_MODE_RGB: - return "COLOR_MODE_RGB"; + return ESPHOME_PSTR("COLOR_MODE_RGB"); case enums::COLOR_MODE_RGB_WHITE: - return "COLOR_MODE_RGB_WHITE"; + return ESPHOME_PSTR("COLOR_MODE_RGB_WHITE"); case enums::COLOR_MODE_RGB_COLOR_TEMPERATURE: - return "COLOR_MODE_RGB_COLOR_TEMPERATURE"; + return ESPHOME_PSTR("COLOR_MODE_RGB_COLOR_TEMPERATURE"); case enums::COLOR_MODE_RGB_COLD_WARM_WHITE: - return "COLOR_MODE_RGB_COLD_WARM_WHITE"; + return ESPHOME_PSTR("COLOR_MODE_RGB_COLD_WARM_WHITE"); default: - return "UNKNOWN"; + return ESPHOME_PSTR("UNKNOWN"); } } #endif @@ -187,77 +209,91 @@ template<> const char *proto_enum_to_string(enums::ColorMode v template<> const char *proto_enum_to_string(enums::SensorStateClass value) { switch (value) { case enums::STATE_CLASS_NONE: - return "STATE_CLASS_NONE"; + return ESPHOME_PSTR("STATE_CLASS_NONE"); case enums::STATE_CLASS_MEASUREMENT: - return "STATE_CLASS_MEASUREMENT"; + return ESPHOME_PSTR("STATE_CLASS_MEASUREMENT"); case enums::STATE_CLASS_TOTAL_INCREASING: - return "STATE_CLASS_TOTAL_INCREASING"; + return ESPHOME_PSTR("STATE_CLASS_TOTAL_INCREASING"); case enums::STATE_CLASS_TOTAL: - return "STATE_CLASS_TOTAL"; + return ESPHOME_PSTR("STATE_CLASS_TOTAL"); case enums::STATE_CLASS_MEASUREMENT_ANGLE: - return "STATE_CLASS_MEASUREMENT_ANGLE"; + return ESPHOME_PSTR("STATE_CLASS_MEASUREMENT_ANGLE"); default: - return "UNKNOWN"; + return ESPHOME_PSTR("UNKNOWN"); } } #endif template<> const char *proto_enum_to_string(enums::LogLevel value) { switch (value) { case enums::LOG_LEVEL_NONE: - return "LOG_LEVEL_NONE"; + return ESPHOME_PSTR("LOG_LEVEL_NONE"); case enums::LOG_LEVEL_ERROR: - return "LOG_LEVEL_ERROR"; + return ESPHOME_PSTR("LOG_LEVEL_ERROR"); case enums::LOG_LEVEL_WARN: - return "LOG_LEVEL_WARN"; + return ESPHOME_PSTR("LOG_LEVEL_WARN"); case enums::LOG_LEVEL_INFO: - return "LOG_LEVEL_INFO"; + return ESPHOME_PSTR("LOG_LEVEL_INFO"); case enums::LOG_LEVEL_CONFIG: - return "LOG_LEVEL_CONFIG"; + return ESPHOME_PSTR("LOG_LEVEL_CONFIG"); case enums::LOG_LEVEL_DEBUG: - return "LOG_LEVEL_DEBUG"; + return ESPHOME_PSTR("LOG_LEVEL_DEBUG"); case enums::LOG_LEVEL_VERBOSE: - return "LOG_LEVEL_VERBOSE"; + return ESPHOME_PSTR("LOG_LEVEL_VERBOSE"); case enums::LOG_LEVEL_VERY_VERBOSE: - return "LOG_LEVEL_VERY_VERBOSE"; + return ESPHOME_PSTR("LOG_LEVEL_VERY_VERBOSE"); default: - return "UNKNOWN"; + return ESPHOME_PSTR("UNKNOWN"); + } +} +template<> const char *proto_enum_to_string(enums::DSTRuleType value) { + switch (value) { + case enums::DST_RULE_TYPE_NONE: + return ESPHOME_PSTR("DST_RULE_TYPE_NONE"); + case enums::DST_RULE_TYPE_MONTH_WEEK_DAY: + return ESPHOME_PSTR("DST_RULE_TYPE_MONTH_WEEK_DAY"); + case enums::DST_RULE_TYPE_JULIAN_NO_LEAP: + return ESPHOME_PSTR("DST_RULE_TYPE_JULIAN_NO_LEAP"); + case enums::DST_RULE_TYPE_DAY_OF_YEAR: + return ESPHOME_PSTR("DST_RULE_TYPE_DAY_OF_YEAR"); + default: + return ESPHOME_PSTR("UNKNOWN"); } } #ifdef USE_API_USER_DEFINED_ACTIONS template<> const char *proto_enum_to_string(enums::ServiceArgType value) { switch (value) { case enums::SERVICE_ARG_TYPE_BOOL: - return "SERVICE_ARG_TYPE_BOOL"; + return ESPHOME_PSTR("SERVICE_ARG_TYPE_BOOL"); case enums::SERVICE_ARG_TYPE_INT: - return "SERVICE_ARG_TYPE_INT"; + return ESPHOME_PSTR("SERVICE_ARG_TYPE_INT"); case enums::SERVICE_ARG_TYPE_FLOAT: - return "SERVICE_ARG_TYPE_FLOAT"; + return ESPHOME_PSTR("SERVICE_ARG_TYPE_FLOAT"); case enums::SERVICE_ARG_TYPE_STRING: - return "SERVICE_ARG_TYPE_STRING"; + return ESPHOME_PSTR("SERVICE_ARG_TYPE_STRING"); case enums::SERVICE_ARG_TYPE_BOOL_ARRAY: - return "SERVICE_ARG_TYPE_BOOL_ARRAY"; + return ESPHOME_PSTR("SERVICE_ARG_TYPE_BOOL_ARRAY"); case enums::SERVICE_ARG_TYPE_INT_ARRAY: - return "SERVICE_ARG_TYPE_INT_ARRAY"; + return ESPHOME_PSTR("SERVICE_ARG_TYPE_INT_ARRAY"); case enums::SERVICE_ARG_TYPE_FLOAT_ARRAY: - return "SERVICE_ARG_TYPE_FLOAT_ARRAY"; + return ESPHOME_PSTR("SERVICE_ARG_TYPE_FLOAT_ARRAY"); case enums::SERVICE_ARG_TYPE_STRING_ARRAY: - return "SERVICE_ARG_TYPE_STRING_ARRAY"; + return ESPHOME_PSTR("SERVICE_ARG_TYPE_STRING_ARRAY"); default: - return "UNKNOWN"; + return ESPHOME_PSTR("UNKNOWN"); } } template<> const char *proto_enum_to_string(enums::SupportsResponseType value) { switch (value) { case enums::SUPPORTS_RESPONSE_NONE: - return "SUPPORTS_RESPONSE_NONE"; + return ESPHOME_PSTR("SUPPORTS_RESPONSE_NONE"); case enums::SUPPORTS_RESPONSE_OPTIONAL: - return "SUPPORTS_RESPONSE_OPTIONAL"; + return ESPHOME_PSTR("SUPPORTS_RESPONSE_OPTIONAL"); case enums::SUPPORTS_RESPONSE_ONLY: - return "SUPPORTS_RESPONSE_ONLY"; + return ESPHOME_PSTR("SUPPORTS_RESPONSE_ONLY"); case enums::SUPPORTS_RESPONSE_STATUS: - return "SUPPORTS_RESPONSE_STATUS"; + return ESPHOME_PSTR("SUPPORTS_RESPONSE_STATUS"); default: - return "UNKNOWN"; + return ESPHOME_PSTR("UNKNOWN"); } } #endif @@ -265,101 +301,103 @@ template<> const char *proto_enum_to_string(enums:: template<> const char *proto_enum_to_string(enums::ClimateMode value) { switch (value) { case enums::CLIMATE_MODE_OFF: - return "CLIMATE_MODE_OFF"; + return ESPHOME_PSTR("CLIMATE_MODE_OFF"); case enums::CLIMATE_MODE_HEAT_COOL: - return "CLIMATE_MODE_HEAT_COOL"; + return ESPHOME_PSTR("CLIMATE_MODE_HEAT_COOL"); case enums::CLIMATE_MODE_COOL: - return "CLIMATE_MODE_COOL"; + return ESPHOME_PSTR("CLIMATE_MODE_COOL"); case enums::CLIMATE_MODE_HEAT: - return "CLIMATE_MODE_HEAT"; + return ESPHOME_PSTR("CLIMATE_MODE_HEAT"); case enums::CLIMATE_MODE_FAN_ONLY: - return "CLIMATE_MODE_FAN_ONLY"; + return ESPHOME_PSTR("CLIMATE_MODE_FAN_ONLY"); case enums::CLIMATE_MODE_DRY: - return "CLIMATE_MODE_DRY"; + return ESPHOME_PSTR("CLIMATE_MODE_DRY"); case enums::CLIMATE_MODE_AUTO: - return "CLIMATE_MODE_AUTO"; + return ESPHOME_PSTR("CLIMATE_MODE_AUTO"); default: - return "UNKNOWN"; + return ESPHOME_PSTR("UNKNOWN"); } } template<> const char *proto_enum_to_string(enums::ClimateFanMode value) { switch (value) { case enums::CLIMATE_FAN_ON: - return "CLIMATE_FAN_ON"; + return ESPHOME_PSTR("CLIMATE_FAN_ON"); case enums::CLIMATE_FAN_OFF: - return "CLIMATE_FAN_OFF"; + return ESPHOME_PSTR("CLIMATE_FAN_OFF"); case enums::CLIMATE_FAN_AUTO: - return "CLIMATE_FAN_AUTO"; + return ESPHOME_PSTR("CLIMATE_FAN_AUTO"); case enums::CLIMATE_FAN_LOW: - return "CLIMATE_FAN_LOW"; + return ESPHOME_PSTR("CLIMATE_FAN_LOW"); case enums::CLIMATE_FAN_MEDIUM: - return "CLIMATE_FAN_MEDIUM"; + return ESPHOME_PSTR("CLIMATE_FAN_MEDIUM"); case enums::CLIMATE_FAN_HIGH: - return "CLIMATE_FAN_HIGH"; + return ESPHOME_PSTR("CLIMATE_FAN_HIGH"); case enums::CLIMATE_FAN_MIDDLE: - return "CLIMATE_FAN_MIDDLE"; + return ESPHOME_PSTR("CLIMATE_FAN_MIDDLE"); case enums::CLIMATE_FAN_FOCUS: - return "CLIMATE_FAN_FOCUS"; + return ESPHOME_PSTR("CLIMATE_FAN_FOCUS"); case enums::CLIMATE_FAN_DIFFUSE: - return "CLIMATE_FAN_DIFFUSE"; + return ESPHOME_PSTR("CLIMATE_FAN_DIFFUSE"); case enums::CLIMATE_FAN_QUIET: - return "CLIMATE_FAN_QUIET"; + return ESPHOME_PSTR("CLIMATE_FAN_QUIET"); default: - return "UNKNOWN"; + return ESPHOME_PSTR("UNKNOWN"); } } template<> const char *proto_enum_to_string(enums::ClimateSwingMode value) { switch (value) { case enums::CLIMATE_SWING_OFF: - return "CLIMATE_SWING_OFF"; + return ESPHOME_PSTR("CLIMATE_SWING_OFF"); case enums::CLIMATE_SWING_BOTH: - return "CLIMATE_SWING_BOTH"; + return ESPHOME_PSTR("CLIMATE_SWING_BOTH"); case enums::CLIMATE_SWING_VERTICAL: - return "CLIMATE_SWING_VERTICAL"; + return ESPHOME_PSTR("CLIMATE_SWING_VERTICAL"); case enums::CLIMATE_SWING_HORIZONTAL: - return "CLIMATE_SWING_HORIZONTAL"; + return ESPHOME_PSTR("CLIMATE_SWING_HORIZONTAL"); default: - return "UNKNOWN"; + return ESPHOME_PSTR("UNKNOWN"); } } template<> const char *proto_enum_to_string(enums::ClimateAction value) { switch (value) { case enums::CLIMATE_ACTION_OFF: - return "CLIMATE_ACTION_OFF"; + return ESPHOME_PSTR("CLIMATE_ACTION_OFF"); case enums::CLIMATE_ACTION_COOLING: - return "CLIMATE_ACTION_COOLING"; + return ESPHOME_PSTR("CLIMATE_ACTION_COOLING"); case enums::CLIMATE_ACTION_HEATING: - return "CLIMATE_ACTION_HEATING"; + return ESPHOME_PSTR("CLIMATE_ACTION_HEATING"); case enums::CLIMATE_ACTION_IDLE: - return "CLIMATE_ACTION_IDLE"; + return ESPHOME_PSTR("CLIMATE_ACTION_IDLE"); case enums::CLIMATE_ACTION_DRYING: - return "CLIMATE_ACTION_DRYING"; + return ESPHOME_PSTR("CLIMATE_ACTION_DRYING"); case enums::CLIMATE_ACTION_FAN: - return "CLIMATE_ACTION_FAN"; + return ESPHOME_PSTR("CLIMATE_ACTION_FAN"); + case enums::CLIMATE_ACTION_DEFROSTING: + return ESPHOME_PSTR("CLIMATE_ACTION_DEFROSTING"); default: - return "UNKNOWN"; + return ESPHOME_PSTR("UNKNOWN"); } } template<> const char *proto_enum_to_string(enums::ClimatePreset value) { switch (value) { case enums::CLIMATE_PRESET_NONE: - return "CLIMATE_PRESET_NONE"; + return ESPHOME_PSTR("CLIMATE_PRESET_NONE"); case enums::CLIMATE_PRESET_HOME: - return "CLIMATE_PRESET_HOME"; + return ESPHOME_PSTR("CLIMATE_PRESET_HOME"); case enums::CLIMATE_PRESET_AWAY: - return "CLIMATE_PRESET_AWAY"; + return ESPHOME_PSTR("CLIMATE_PRESET_AWAY"); case enums::CLIMATE_PRESET_BOOST: - return "CLIMATE_PRESET_BOOST"; + return ESPHOME_PSTR("CLIMATE_PRESET_BOOST"); case enums::CLIMATE_PRESET_COMFORT: - return "CLIMATE_PRESET_COMFORT"; + return ESPHOME_PSTR("CLIMATE_PRESET_COMFORT"); case enums::CLIMATE_PRESET_ECO: - return "CLIMATE_PRESET_ECO"; + return ESPHOME_PSTR("CLIMATE_PRESET_ECO"); case enums::CLIMATE_PRESET_SLEEP: - return "CLIMATE_PRESET_SLEEP"; + return ESPHOME_PSTR("CLIMATE_PRESET_SLEEP"); case enums::CLIMATE_PRESET_ACTIVITY: - return "CLIMATE_PRESET_ACTIVITY"; + return ESPHOME_PSTR("CLIMATE_PRESET_ACTIVITY"); default: - return "UNKNOWN"; + return ESPHOME_PSTR("UNKNOWN"); } } #endif @@ -367,21 +405,21 @@ template<> const char *proto_enum_to_string(enums::Climate template<> const char *proto_enum_to_string(enums::WaterHeaterMode value) { switch (value) { case enums::WATER_HEATER_MODE_OFF: - return "WATER_HEATER_MODE_OFF"; + return ESPHOME_PSTR("WATER_HEATER_MODE_OFF"); case enums::WATER_HEATER_MODE_ECO: - return "WATER_HEATER_MODE_ECO"; + return ESPHOME_PSTR("WATER_HEATER_MODE_ECO"); case enums::WATER_HEATER_MODE_ELECTRIC: - return "WATER_HEATER_MODE_ELECTRIC"; + return ESPHOME_PSTR("WATER_HEATER_MODE_ELECTRIC"); case enums::WATER_HEATER_MODE_PERFORMANCE: - return "WATER_HEATER_MODE_PERFORMANCE"; + return ESPHOME_PSTR("WATER_HEATER_MODE_PERFORMANCE"); case enums::WATER_HEATER_MODE_HIGH_DEMAND: - return "WATER_HEATER_MODE_HIGH_DEMAND"; + return ESPHOME_PSTR("WATER_HEATER_MODE_HIGH_DEMAND"); case enums::WATER_HEATER_MODE_HEAT_PUMP: - return "WATER_HEATER_MODE_HEAT_PUMP"; + return ESPHOME_PSTR("WATER_HEATER_MODE_HEAT_PUMP"); case enums::WATER_HEATER_MODE_GAS: - return "WATER_HEATER_MODE_GAS"; + return ESPHOME_PSTR("WATER_HEATER_MODE_GAS"); default: - return "UNKNOWN"; + return ESPHOME_PSTR("UNKNOWN"); } } #endif @@ -389,32 +427,36 @@ template<> const char *proto_enum_to_string(enums::WaterHeaterCommandHasField value) { switch (value) { case enums::WATER_HEATER_COMMAND_HAS_NONE: - return "WATER_HEATER_COMMAND_HAS_NONE"; + return ESPHOME_PSTR("WATER_HEATER_COMMAND_HAS_NONE"); case enums::WATER_HEATER_COMMAND_HAS_MODE: - return "WATER_HEATER_COMMAND_HAS_MODE"; + return ESPHOME_PSTR("WATER_HEATER_COMMAND_HAS_MODE"); case enums::WATER_HEATER_COMMAND_HAS_TARGET_TEMPERATURE: - return "WATER_HEATER_COMMAND_HAS_TARGET_TEMPERATURE"; + return ESPHOME_PSTR("WATER_HEATER_COMMAND_HAS_TARGET_TEMPERATURE"); case enums::WATER_HEATER_COMMAND_HAS_STATE: - return "WATER_HEATER_COMMAND_HAS_STATE"; + return ESPHOME_PSTR("WATER_HEATER_COMMAND_HAS_STATE"); case enums::WATER_HEATER_COMMAND_HAS_TARGET_TEMPERATURE_LOW: - return "WATER_HEATER_COMMAND_HAS_TARGET_TEMPERATURE_LOW"; + return ESPHOME_PSTR("WATER_HEATER_COMMAND_HAS_TARGET_TEMPERATURE_LOW"); case enums::WATER_HEATER_COMMAND_HAS_TARGET_TEMPERATURE_HIGH: - return "WATER_HEATER_COMMAND_HAS_TARGET_TEMPERATURE_HIGH"; + return ESPHOME_PSTR("WATER_HEATER_COMMAND_HAS_TARGET_TEMPERATURE_HIGH"); + case enums::WATER_HEATER_COMMAND_HAS_ON_STATE: + return ESPHOME_PSTR("WATER_HEATER_COMMAND_HAS_ON_STATE"); + case enums::WATER_HEATER_COMMAND_HAS_AWAY_STATE: + return ESPHOME_PSTR("WATER_HEATER_COMMAND_HAS_AWAY_STATE"); default: - return "UNKNOWN"; + return ESPHOME_PSTR("UNKNOWN"); } } #ifdef USE_NUMBER template<> const char *proto_enum_to_string(enums::NumberMode value) { switch (value) { case enums::NUMBER_MODE_AUTO: - return "NUMBER_MODE_AUTO"; + return ESPHOME_PSTR("NUMBER_MODE_AUTO"); case enums::NUMBER_MODE_BOX: - return "NUMBER_MODE_BOX"; + return ESPHOME_PSTR("NUMBER_MODE_BOX"); case enums::NUMBER_MODE_SLIDER: - return "NUMBER_MODE_SLIDER"; + return ESPHOME_PSTR("NUMBER_MODE_SLIDER"); default: - return "UNKNOWN"; + return ESPHOME_PSTR("UNKNOWN"); } } #endif @@ -422,31 +464,31 @@ template<> const char *proto_enum_to_string(enums::NumberMode template<> const char *proto_enum_to_string(enums::LockState value) { switch (value) { case enums::LOCK_STATE_NONE: - return "LOCK_STATE_NONE"; + return ESPHOME_PSTR("LOCK_STATE_NONE"); case enums::LOCK_STATE_LOCKED: - return "LOCK_STATE_LOCKED"; + return ESPHOME_PSTR("LOCK_STATE_LOCKED"); case enums::LOCK_STATE_UNLOCKED: - return "LOCK_STATE_UNLOCKED"; + return ESPHOME_PSTR("LOCK_STATE_UNLOCKED"); case enums::LOCK_STATE_JAMMED: - return "LOCK_STATE_JAMMED"; + return ESPHOME_PSTR("LOCK_STATE_JAMMED"); case enums::LOCK_STATE_LOCKING: - return "LOCK_STATE_LOCKING"; + return ESPHOME_PSTR("LOCK_STATE_LOCKING"); case enums::LOCK_STATE_UNLOCKING: - return "LOCK_STATE_UNLOCKING"; + return ESPHOME_PSTR("LOCK_STATE_UNLOCKING"); default: - return "UNKNOWN"; + return ESPHOME_PSTR("UNKNOWN"); } } template<> const char *proto_enum_to_string(enums::LockCommand value) { switch (value) { case enums::LOCK_UNLOCK: - return "LOCK_UNLOCK"; + return ESPHOME_PSTR("LOCK_UNLOCK"); case enums::LOCK_LOCK: - return "LOCK_LOCK"; + return ESPHOME_PSTR("LOCK_LOCK"); case enums::LOCK_OPEN: - return "LOCK_OPEN"; + return ESPHOME_PSTR("LOCK_OPEN"); default: - return "UNKNOWN"; + return ESPHOME_PSTR("UNKNOWN"); } } #endif @@ -454,65 +496,65 @@ template<> const char *proto_enum_to_string(enums::LockComma template<> const char *proto_enum_to_string(enums::MediaPlayerState value) { switch (value) { case enums::MEDIA_PLAYER_STATE_NONE: - return "MEDIA_PLAYER_STATE_NONE"; + return ESPHOME_PSTR("MEDIA_PLAYER_STATE_NONE"); case enums::MEDIA_PLAYER_STATE_IDLE: - return "MEDIA_PLAYER_STATE_IDLE"; + return ESPHOME_PSTR("MEDIA_PLAYER_STATE_IDLE"); case enums::MEDIA_PLAYER_STATE_PLAYING: - return "MEDIA_PLAYER_STATE_PLAYING"; + return ESPHOME_PSTR("MEDIA_PLAYER_STATE_PLAYING"); case enums::MEDIA_PLAYER_STATE_PAUSED: - return "MEDIA_PLAYER_STATE_PAUSED"; + return ESPHOME_PSTR("MEDIA_PLAYER_STATE_PAUSED"); case enums::MEDIA_PLAYER_STATE_ANNOUNCING: - return "MEDIA_PLAYER_STATE_ANNOUNCING"; + return ESPHOME_PSTR("MEDIA_PLAYER_STATE_ANNOUNCING"); case enums::MEDIA_PLAYER_STATE_OFF: - return "MEDIA_PLAYER_STATE_OFF"; + return ESPHOME_PSTR("MEDIA_PLAYER_STATE_OFF"); case enums::MEDIA_PLAYER_STATE_ON: - return "MEDIA_PLAYER_STATE_ON"; + return ESPHOME_PSTR("MEDIA_PLAYER_STATE_ON"); default: - return "UNKNOWN"; + return ESPHOME_PSTR("UNKNOWN"); } } template<> const char *proto_enum_to_string(enums::MediaPlayerCommand value) { switch (value) { case enums::MEDIA_PLAYER_COMMAND_PLAY: - return "MEDIA_PLAYER_COMMAND_PLAY"; + return ESPHOME_PSTR("MEDIA_PLAYER_COMMAND_PLAY"); case enums::MEDIA_PLAYER_COMMAND_PAUSE: - return "MEDIA_PLAYER_COMMAND_PAUSE"; + return ESPHOME_PSTR("MEDIA_PLAYER_COMMAND_PAUSE"); case enums::MEDIA_PLAYER_COMMAND_STOP: - return "MEDIA_PLAYER_COMMAND_STOP"; + return ESPHOME_PSTR("MEDIA_PLAYER_COMMAND_STOP"); case enums::MEDIA_PLAYER_COMMAND_MUTE: - return "MEDIA_PLAYER_COMMAND_MUTE"; + return ESPHOME_PSTR("MEDIA_PLAYER_COMMAND_MUTE"); case enums::MEDIA_PLAYER_COMMAND_UNMUTE: - return "MEDIA_PLAYER_COMMAND_UNMUTE"; + return ESPHOME_PSTR("MEDIA_PLAYER_COMMAND_UNMUTE"); case enums::MEDIA_PLAYER_COMMAND_TOGGLE: - return "MEDIA_PLAYER_COMMAND_TOGGLE"; + return ESPHOME_PSTR("MEDIA_PLAYER_COMMAND_TOGGLE"); case enums::MEDIA_PLAYER_COMMAND_VOLUME_UP: - return "MEDIA_PLAYER_COMMAND_VOLUME_UP"; + return ESPHOME_PSTR("MEDIA_PLAYER_COMMAND_VOLUME_UP"); case enums::MEDIA_PLAYER_COMMAND_VOLUME_DOWN: - return "MEDIA_PLAYER_COMMAND_VOLUME_DOWN"; + return ESPHOME_PSTR("MEDIA_PLAYER_COMMAND_VOLUME_DOWN"); case enums::MEDIA_PLAYER_COMMAND_ENQUEUE: - return "MEDIA_PLAYER_COMMAND_ENQUEUE"; + return ESPHOME_PSTR("MEDIA_PLAYER_COMMAND_ENQUEUE"); case enums::MEDIA_PLAYER_COMMAND_REPEAT_ONE: - return "MEDIA_PLAYER_COMMAND_REPEAT_ONE"; + return ESPHOME_PSTR("MEDIA_PLAYER_COMMAND_REPEAT_ONE"); case enums::MEDIA_PLAYER_COMMAND_REPEAT_OFF: - return "MEDIA_PLAYER_COMMAND_REPEAT_OFF"; + return ESPHOME_PSTR("MEDIA_PLAYER_COMMAND_REPEAT_OFF"); case enums::MEDIA_PLAYER_COMMAND_CLEAR_PLAYLIST: - return "MEDIA_PLAYER_COMMAND_CLEAR_PLAYLIST"; + return ESPHOME_PSTR("MEDIA_PLAYER_COMMAND_CLEAR_PLAYLIST"); case enums::MEDIA_PLAYER_COMMAND_TURN_ON: - return "MEDIA_PLAYER_COMMAND_TURN_ON"; + return ESPHOME_PSTR("MEDIA_PLAYER_COMMAND_TURN_ON"); case enums::MEDIA_PLAYER_COMMAND_TURN_OFF: - return "MEDIA_PLAYER_COMMAND_TURN_OFF"; + return ESPHOME_PSTR("MEDIA_PLAYER_COMMAND_TURN_OFF"); default: - return "UNKNOWN"; + return ESPHOME_PSTR("UNKNOWN"); } } template<> const char *proto_enum_to_string(enums::MediaPlayerFormatPurpose value) { switch (value) { case enums::MEDIA_PLAYER_FORMAT_PURPOSE_DEFAULT: - return "MEDIA_PLAYER_FORMAT_PURPOSE_DEFAULT"; + return ESPHOME_PSTR("MEDIA_PLAYER_FORMAT_PURPOSE_DEFAULT"); case enums::MEDIA_PLAYER_FORMAT_PURPOSE_ANNOUNCEMENT: - return "MEDIA_PLAYER_FORMAT_PURPOSE_ANNOUNCEMENT"; + return ESPHOME_PSTR("MEDIA_PLAYER_FORMAT_PURPOSE_ANNOUNCEMENT"); default: - return "UNKNOWN"; + return ESPHOME_PSTR("UNKNOWN"); } } #endif @@ -521,49 +563,49 @@ template<> const char *proto_enum_to_string(enums::BluetoothDeviceRequestType value) { switch (value) { case enums::BLUETOOTH_DEVICE_REQUEST_TYPE_CONNECT: - return "BLUETOOTH_DEVICE_REQUEST_TYPE_CONNECT"; + return ESPHOME_PSTR("BLUETOOTH_DEVICE_REQUEST_TYPE_CONNECT"); case enums::BLUETOOTH_DEVICE_REQUEST_TYPE_DISCONNECT: - return "BLUETOOTH_DEVICE_REQUEST_TYPE_DISCONNECT"; + return ESPHOME_PSTR("BLUETOOTH_DEVICE_REQUEST_TYPE_DISCONNECT"); case enums::BLUETOOTH_DEVICE_REQUEST_TYPE_PAIR: - return "BLUETOOTH_DEVICE_REQUEST_TYPE_PAIR"; + return ESPHOME_PSTR("BLUETOOTH_DEVICE_REQUEST_TYPE_PAIR"); case enums::BLUETOOTH_DEVICE_REQUEST_TYPE_UNPAIR: - return "BLUETOOTH_DEVICE_REQUEST_TYPE_UNPAIR"; + return ESPHOME_PSTR("BLUETOOTH_DEVICE_REQUEST_TYPE_UNPAIR"); case enums::BLUETOOTH_DEVICE_REQUEST_TYPE_CONNECT_V3_WITH_CACHE: - return "BLUETOOTH_DEVICE_REQUEST_TYPE_CONNECT_V3_WITH_CACHE"; + return ESPHOME_PSTR("BLUETOOTH_DEVICE_REQUEST_TYPE_CONNECT_V3_WITH_CACHE"); case enums::BLUETOOTH_DEVICE_REQUEST_TYPE_CONNECT_V3_WITHOUT_CACHE: - return "BLUETOOTH_DEVICE_REQUEST_TYPE_CONNECT_V3_WITHOUT_CACHE"; + return ESPHOME_PSTR("BLUETOOTH_DEVICE_REQUEST_TYPE_CONNECT_V3_WITHOUT_CACHE"); case enums::BLUETOOTH_DEVICE_REQUEST_TYPE_CLEAR_CACHE: - return "BLUETOOTH_DEVICE_REQUEST_TYPE_CLEAR_CACHE"; + return ESPHOME_PSTR("BLUETOOTH_DEVICE_REQUEST_TYPE_CLEAR_CACHE"); default: - return "UNKNOWN"; + return ESPHOME_PSTR("UNKNOWN"); } } template<> const char *proto_enum_to_string(enums::BluetoothScannerState value) { switch (value) { case enums::BLUETOOTH_SCANNER_STATE_IDLE: - return "BLUETOOTH_SCANNER_STATE_IDLE"; + return ESPHOME_PSTR("BLUETOOTH_SCANNER_STATE_IDLE"); case enums::BLUETOOTH_SCANNER_STATE_STARTING: - return "BLUETOOTH_SCANNER_STATE_STARTING"; + return ESPHOME_PSTR("BLUETOOTH_SCANNER_STATE_STARTING"); case enums::BLUETOOTH_SCANNER_STATE_RUNNING: - return "BLUETOOTH_SCANNER_STATE_RUNNING"; + return ESPHOME_PSTR("BLUETOOTH_SCANNER_STATE_RUNNING"); case enums::BLUETOOTH_SCANNER_STATE_FAILED: - return "BLUETOOTH_SCANNER_STATE_FAILED"; + return ESPHOME_PSTR("BLUETOOTH_SCANNER_STATE_FAILED"); case enums::BLUETOOTH_SCANNER_STATE_STOPPING: - return "BLUETOOTH_SCANNER_STATE_STOPPING"; + return ESPHOME_PSTR("BLUETOOTH_SCANNER_STATE_STOPPING"); case enums::BLUETOOTH_SCANNER_STATE_STOPPED: - return "BLUETOOTH_SCANNER_STATE_STOPPED"; + return ESPHOME_PSTR("BLUETOOTH_SCANNER_STATE_STOPPED"); default: - return "UNKNOWN"; + return ESPHOME_PSTR("UNKNOWN"); } } template<> const char *proto_enum_to_string(enums::BluetoothScannerMode value) { switch (value) { case enums::BLUETOOTH_SCANNER_MODE_PASSIVE: - return "BLUETOOTH_SCANNER_MODE_PASSIVE"; + return ESPHOME_PSTR("BLUETOOTH_SCANNER_MODE_PASSIVE"); case enums::BLUETOOTH_SCANNER_MODE_ACTIVE: - return "BLUETOOTH_SCANNER_MODE_ACTIVE"; + return ESPHOME_PSTR("BLUETOOTH_SCANNER_MODE_ACTIVE"); default: - return "UNKNOWN"; + return ESPHOME_PSTR("UNKNOWN"); } } #endif @@ -571,76 +613,76 @@ template<> const char *proto_enum_to_string(enums::VoiceAssistantSubscribeFlag value) { switch (value) { case enums::VOICE_ASSISTANT_SUBSCRIBE_NONE: - return "VOICE_ASSISTANT_SUBSCRIBE_NONE"; + return ESPHOME_PSTR("VOICE_ASSISTANT_SUBSCRIBE_NONE"); case enums::VOICE_ASSISTANT_SUBSCRIBE_API_AUDIO: - return "VOICE_ASSISTANT_SUBSCRIBE_API_AUDIO"; + return ESPHOME_PSTR("VOICE_ASSISTANT_SUBSCRIBE_API_AUDIO"); default: - return "UNKNOWN"; + return ESPHOME_PSTR("UNKNOWN"); } } template<> const char *proto_enum_to_string(enums::VoiceAssistantRequestFlag value) { switch (value) { case enums::VOICE_ASSISTANT_REQUEST_NONE: - return "VOICE_ASSISTANT_REQUEST_NONE"; + return ESPHOME_PSTR("VOICE_ASSISTANT_REQUEST_NONE"); case enums::VOICE_ASSISTANT_REQUEST_USE_VAD: - return "VOICE_ASSISTANT_REQUEST_USE_VAD"; + return ESPHOME_PSTR("VOICE_ASSISTANT_REQUEST_USE_VAD"); case enums::VOICE_ASSISTANT_REQUEST_USE_WAKE_WORD: - return "VOICE_ASSISTANT_REQUEST_USE_WAKE_WORD"; + return ESPHOME_PSTR("VOICE_ASSISTANT_REQUEST_USE_WAKE_WORD"); default: - return "UNKNOWN"; + return ESPHOME_PSTR("UNKNOWN"); } } #ifdef USE_VOICE_ASSISTANT template<> const char *proto_enum_to_string(enums::VoiceAssistantEvent value) { switch (value) { case enums::VOICE_ASSISTANT_ERROR: - return "VOICE_ASSISTANT_ERROR"; + return ESPHOME_PSTR("VOICE_ASSISTANT_ERROR"); case enums::VOICE_ASSISTANT_RUN_START: - return "VOICE_ASSISTANT_RUN_START"; + return ESPHOME_PSTR("VOICE_ASSISTANT_RUN_START"); case enums::VOICE_ASSISTANT_RUN_END: - return "VOICE_ASSISTANT_RUN_END"; + return ESPHOME_PSTR("VOICE_ASSISTANT_RUN_END"); case enums::VOICE_ASSISTANT_STT_START: - return "VOICE_ASSISTANT_STT_START"; + return ESPHOME_PSTR("VOICE_ASSISTANT_STT_START"); case enums::VOICE_ASSISTANT_STT_END: - return "VOICE_ASSISTANT_STT_END"; + return ESPHOME_PSTR("VOICE_ASSISTANT_STT_END"); case enums::VOICE_ASSISTANT_INTENT_START: - return "VOICE_ASSISTANT_INTENT_START"; + return ESPHOME_PSTR("VOICE_ASSISTANT_INTENT_START"); case enums::VOICE_ASSISTANT_INTENT_END: - return "VOICE_ASSISTANT_INTENT_END"; + return ESPHOME_PSTR("VOICE_ASSISTANT_INTENT_END"); case enums::VOICE_ASSISTANT_TTS_START: - return "VOICE_ASSISTANT_TTS_START"; + return ESPHOME_PSTR("VOICE_ASSISTANT_TTS_START"); case enums::VOICE_ASSISTANT_TTS_END: - return "VOICE_ASSISTANT_TTS_END"; + return ESPHOME_PSTR("VOICE_ASSISTANT_TTS_END"); case enums::VOICE_ASSISTANT_WAKE_WORD_START: - return "VOICE_ASSISTANT_WAKE_WORD_START"; + return ESPHOME_PSTR("VOICE_ASSISTANT_WAKE_WORD_START"); case enums::VOICE_ASSISTANT_WAKE_WORD_END: - return "VOICE_ASSISTANT_WAKE_WORD_END"; + return ESPHOME_PSTR("VOICE_ASSISTANT_WAKE_WORD_END"); case enums::VOICE_ASSISTANT_STT_VAD_START: - return "VOICE_ASSISTANT_STT_VAD_START"; + return ESPHOME_PSTR("VOICE_ASSISTANT_STT_VAD_START"); case enums::VOICE_ASSISTANT_STT_VAD_END: - return "VOICE_ASSISTANT_STT_VAD_END"; + return ESPHOME_PSTR("VOICE_ASSISTANT_STT_VAD_END"); case enums::VOICE_ASSISTANT_TTS_STREAM_START: - return "VOICE_ASSISTANT_TTS_STREAM_START"; + return ESPHOME_PSTR("VOICE_ASSISTANT_TTS_STREAM_START"); case enums::VOICE_ASSISTANT_TTS_STREAM_END: - return "VOICE_ASSISTANT_TTS_STREAM_END"; + return ESPHOME_PSTR("VOICE_ASSISTANT_TTS_STREAM_END"); case enums::VOICE_ASSISTANT_INTENT_PROGRESS: - return "VOICE_ASSISTANT_INTENT_PROGRESS"; + return ESPHOME_PSTR("VOICE_ASSISTANT_INTENT_PROGRESS"); default: - return "UNKNOWN"; + return ESPHOME_PSTR("UNKNOWN"); } } template<> const char *proto_enum_to_string(enums::VoiceAssistantTimerEvent value) { switch (value) { case enums::VOICE_ASSISTANT_TIMER_STARTED: - return "VOICE_ASSISTANT_TIMER_STARTED"; + return ESPHOME_PSTR("VOICE_ASSISTANT_TIMER_STARTED"); case enums::VOICE_ASSISTANT_TIMER_UPDATED: - return "VOICE_ASSISTANT_TIMER_UPDATED"; + return ESPHOME_PSTR("VOICE_ASSISTANT_TIMER_UPDATED"); case enums::VOICE_ASSISTANT_TIMER_CANCELLED: - return "VOICE_ASSISTANT_TIMER_CANCELLED"; + return ESPHOME_PSTR("VOICE_ASSISTANT_TIMER_CANCELLED"); case enums::VOICE_ASSISTANT_TIMER_FINISHED: - return "VOICE_ASSISTANT_TIMER_FINISHED"; + return ESPHOME_PSTR("VOICE_ASSISTANT_TIMER_FINISHED"); default: - return "UNKNOWN"; + return ESPHOME_PSTR("UNKNOWN"); } } #endif @@ -648,48 +690,48 @@ template<> const char *proto_enum_to_string(enu template<> const char *proto_enum_to_string(enums::AlarmControlPanelState value) { switch (value) { case enums::ALARM_STATE_DISARMED: - return "ALARM_STATE_DISARMED"; + return ESPHOME_PSTR("ALARM_STATE_DISARMED"); case enums::ALARM_STATE_ARMED_HOME: - return "ALARM_STATE_ARMED_HOME"; + return ESPHOME_PSTR("ALARM_STATE_ARMED_HOME"); case enums::ALARM_STATE_ARMED_AWAY: - return "ALARM_STATE_ARMED_AWAY"; + return ESPHOME_PSTR("ALARM_STATE_ARMED_AWAY"); case enums::ALARM_STATE_ARMED_NIGHT: - return "ALARM_STATE_ARMED_NIGHT"; + return ESPHOME_PSTR("ALARM_STATE_ARMED_NIGHT"); case enums::ALARM_STATE_ARMED_VACATION: - return "ALARM_STATE_ARMED_VACATION"; + return ESPHOME_PSTR("ALARM_STATE_ARMED_VACATION"); case enums::ALARM_STATE_ARMED_CUSTOM_BYPASS: - return "ALARM_STATE_ARMED_CUSTOM_BYPASS"; + return ESPHOME_PSTR("ALARM_STATE_ARMED_CUSTOM_BYPASS"); case enums::ALARM_STATE_PENDING: - return "ALARM_STATE_PENDING"; + return ESPHOME_PSTR("ALARM_STATE_PENDING"); case enums::ALARM_STATE_ARMING: - return "ALARM_STATE_ARMING"; + return ESPHOME_PSTR("ALARM_STATE_ARMING"); case enums::ALARM_STATE_DISARMING: - return "ALARM_STATE_DISARMING"; + return ESPHOME_PSTR("ALARM_STATE_DISARMING"); case enums::ALARM_STATE_TRIGGERED: - return "ALARM_STATE_TRIGGERED"; + return ESPHOME_PSTR("ALARM_STATE_TRIGGERED"); default: - return "UNKNOWN"; + return ESPHOME_PSTR("UNKNOWN"); } } template<> const char *proto_enum_to_string(enums::AlarmControlPanelStateCommand value) { switch (value) { case enums::ALARM_CONTROL_PANEL_DISARM: - return "ALARM_CONTROL_PANEL_DISARM"; + return ESPHOME_PSTR("ALARM_CONTROL_PANEL_DISARM"); case enums::ALARM_CONTROL_PANEL_ARM_AWAY: - return "ALARM_CONTROL_PANEL_ARM_AWAY"; + return ESPHOME_PSTR("ALARM_CONTROL_PANEL_ARM_AWAY"); case enums::ALARM_CONTROL_PANEL_ARM_HOME: - return "ALARM_CONTROL_PANEL_ARM_HOME"; + return ESPHOME_PSTR("ALARM_CONTROL_PANEL_ARM_HOME"); case enums::ALARM_CONTROL_PANEL_ARM_NIGHT: - return "ALARM_CONTROL_PANEL_ARM_NIGHT"; + return ESPHOME_PSTR("ALARM_CONTROL_PANEL_ARM_NIGHT"); case enums::ALARM_CONTROL_PANEL_ARM_VACATION: - return "ALARM_CONTROL_PANEL_ARM_VACATION"; + return ESPHOME_PSTR("ALARM_CONTROL_PANEL_ARM_VACATION"); case enums::ALARM_CONTROL_PANEL_ARM_CUSTOM_BYPASS: - return "ALARM_CONTROL_PANEL_ARM_CUSTOM_BYPASS"; + return ESPHOME_PSTR("ALARM_CONTROL_PANEL_ARM_CUSTOM_BYPASS"); case enums::ALARM_CONTROL_PANEL_TRIGGER: - return "ALARM_CONTROL_PANEL_TRIGGER"; + return ESPHOME_PSTR("ALARM_CONTROL_PANEL_TRIGGER"); default: - return "UNKNOWN"; + return ESPHOME_PSTR("UNKNOWN"); } } #endif @@ -697,11 +739,11 @@ const char *proto_enum_to_string(enums::Al template<> const char *proto_enum_to_string(enums::TextMode value) { switch (value) { case enums::TEXT_MODE_TEXT: - return "TEXT_MODE_TEXT"; + return ESPHOME_PSTR("TEXT_MODE_TEXT"); case enums::TEXT_MODE_PASSWORD: - return "TEXT_MODE_PASSWORD"; + return ESPHOME_PSTR("TEXT_MODE_PASSWORD"); default: - return "UNKNOWN"; + return ESPHOME_PSTR("UNKNOWN"); } } #endif @@ -709,13 +751,13 @@ template<> const char *proto_enum_to_string(enums::TextMode val template<> const char *proto_enum_to_string(enums::ValveOperation value) { switch (value) { case enums::VALVE_OPERATION_IDLE: - return "VALVE_OPERATION_IDLE"; + return ESPHOME_PSTR("VALVE_OPERATION_IDLE"); case enums::VALVE_OPERATION_IS_OPENING: - return "VALVE_OPERATION_IS_OPENING"; + return ESPHOME_PSTR("VALVE_OPERATION_IS_OPENING"); case enums::VALVE_OPERATION_IS_CLOSING: - return "VALVE_OPERATION_IS_CLOSING"; + return ESPHOME_PSTR("VALVE_OPERATION_IS_CLOSING"); default: - return "UNKNOWN"; + return ESPHOME_PSTR("UNKNOWN"); } } #endif @@ -723,13 +765,13 @@ template<> const char *proto_enum_to_string(enums::ValveO template<> const char *proto_enum_to_string(enums::UpdateCommand value) { switch (value) { case enums::UPDATE_COMMAND_NONE: - return "UPDATE_COMMAND_NONE"; + return ESPHOME_PSTR("UPDATE_COMMAND_NONE"); case enums::UPDATE_COMMAND_UPDATE: - return "UPDATE_COMMAND_UPDATE"; + return ESPHOME_PSTR("UPDATE_COMMAND_UPDATE"); case enums::UPDATE_COMMAND_CHECK: - return "UPDATE_COMMAND_CHECK"; + return ESPHOME_PSTR("UPDATE_COMMAND_CHECK"); default: - return "UNKNOWN"; + return ESPHOME_PSTR("UNKNOWN"); } } #endif @@ -737,1777 +779,1904 @@ template<> const char *proto_enum_to_string(enums::UpdateC template<> const char *proto_enum_to_string(enums::ZWaveProxyRequestType value) { switch (value) { case enums::ZWAVE_PROXY_REQUEST_TYPE_SUBSCRIBE: - return "ZWAVE_PROXY_REQUEST_TYPE_SUBSCRIBE"; + return ESPHOME_PSTR("ZWAVE_PROXY_REQUEST_TYPE_SUBSCRIBE"); case enums::ZWAVE_PROXY_REQUEST_TYPE_UNSUBSCRIBE: - return "ZWAVE_PROXY_REQUEST_TYPE_UNSUBSCRIBE"; + return ESPHOME_PSTR("ZWAVE_PROXY_REQUEST_TYPE_UNSUBSCRIBE"); case enums::ZWAVE_PROXY_REQUEST_TYPE_HOME_ID_CHANGE: - return "ZWAVE_PROXY_REQUEST_TYPE_HOME_ID_CHANGE"; + return ESPHOME_PSTR("ZWAVE_PROXY_REQUEST_TYPE_HOME_ID_CHANGE"); default: - return "UNKNOWN"; + return ESPHOME_PSTR("UNKNOWN"); + } +} +#endif +#ifdef USE_SERIAL_PROXY +template<> const char *proto_enum_to_string(enums::SerialProxyParity value) { + switch (value) { + case enums::SERIAL_PROXY_PARITY_NONE: + return ESPHOME_PSTR("SERIAL_PROXY_PARITY_NONE"); + case enums::SERIAL_PROXY_PARITY_EVEN: + return ESPHOME_PSTR("SERIAL_PROXY_PARITY_EVEN"); + case enums::SERIAL_PROXY_PARITY_ODD: + return ESPHOME_PSTR("SERIAL_PROXY_PARITY_ODD"); + default: + return ESPHOME_PSTR("UNKNOWN"); + } +} +template<> const char *proto_enum_to_string(enums::SerialProxyRequestType value) { + switch (value) { + case enums::SERIAL_PROXY_REQUEST_TYPE_SUBSCRIBE: + return ESPHOME_PSTR("SERIAL_PROXY_REQUEST_TYPE_SUBSCRIBE"); + case enums::SERIAL_PROXY_REQUEST_TYPE_UNSUBSCRIBE: + return ESPHOME_PSTR("SERIAL_PROXY_REQUEST_TYPE_UNSUBSCRIBE"); + case enums::SERIAL_PROXY_REQUEST_TYPE_FLUSH: + return ESPHOME_PSTR("SERIAL_PROXY_REQUEST_TYPE_FLUSH"); + default: + return ESPHOME_PSTR("UNKNOWN"); + } +} +template<> const char *proto_enum_to_string(enums::SerialProxyStatus value) { + switch (value) { + case enums::SERIAL_PROXY_STATUS_OK: + return ESPHOME_PSTR("SERIAL_PROXY_STATUS_OK"); + case enums::SERIAL_PROXY_STATUS_ASSUMED_SUCCESS: + return ESPHOME_PSTR("SERIAL_PROXY_STATUS_ASSUMED_SUCCESS"); + case enums::SERIAL_PROXY_STATUS_ERROR: + return ESPHOME_PSTR("SERIAL_PROXY_STATUS_ERROR"); + case enums::SERIAL_PROXY_STATUS_TIMEOUT: + return ESPHOME_PSTR("SERIAL_PROXY_STATUS_TIMEOUT"); + case enums::SERIAL_PROXY_STATUS_NOT_SUPPORTED: + return ESPHOME_PSTR("SERIAL_PROXY_STATUS_NOT_SUPPORTED"); + default: + return ESPHOME_PSTR("UNKNOWN"); } } #endif const char *HelloRequest::dump_to(DumpBuffer &out) const { - MessageDumpHelper helper(out, "HelloRequest"); - dump_field(out, "client_info", this->client_info); - dump_field(out, "api_version_major", this->api_version_major); - dump_field(out, "api_version_minor", this->api_version_minor); + MessageDumpHelper helper(out, ESPHOME_PSTR("HelloRequest")); + dump_field(out, ESPHOME_PSTR("client_info"), this->client_info); + dump_field(out, ESPHOME_PSTR("api_version_major"), this->api_version_major); + dump_field(out, ESPHOME_PSTR("api_version_minor"), this->api_version_minor); return out.c_str(); } const char *HelloResponse::dump_to(DumpBuffer &out) const { - MessageDumpHelper helper(out, "HelloResponse"); - dump_field(out, "api_version_major", this->api_version_major); - dump_field(out, "api_version_minor", this->api_version_minor); - dump_field(out, "server_info", this->server_info); - dump_field(out, "name", this->name); + MessageDumpHelper helper(out, ESPHOME_PSTR("HelloResponse")); + dump_field(out, ESPHOME_PSTR("api_version_major"), this->api_version_major); + dump_field(out, ESPHOME_PSTR("api_version_minor"), this->api_version_minor); + dump_field(out, ESPHOME_PSTR("server_info"), this->server_info); + dump_field(out, ESPHOME_PSTR("name"), this->name); return out.c_str(); } const char *DisconnectRequest::dump_to(DumpBuffer &out) const { - out.append("DisconnectRequest {}"); + out.append_p(ESPHOME_PSTR("DisconnectRequest {}")); return out.c_str(); } const char *DisconnectResponse::dump_to(DumpBuffer &out) const { - out.append("DisconnectResponse {}"); + out.append_p(ESPHOME_PSTR("DisconnectResponse {}")); return out.c_str(); } const char *PingRequest::dump_to(DumpBuffer &out) const { - out.append("PingRequest {}"); + out.append_p(ESPHOME_PSTR("PingRequest {}")); return out.c_str(); } const char *PingResponse::dump_to(DumpBuffer &out) const { - out.append("PingResponse {}"); - return out.c_str(); -} -const char *DeviceInfoRequest::dump_to(DumpBuffer &out) const { - out.append("DeviceInfoRequest {}"); + out.append_p(ESPHOME_PSTR("PingResponse {}")); return out.c_str(); } #ifdef USE_AREAS const char *AreaInfo::dump_to(DumpBuffer &out) const { - MessageDumpHelper helper(out, "AreaInfo"); - dump_field(out, "area_id", this->area_id); - dump_field(out, "name", this->name); + MessageDumpHelper helper(out, ESPHOME_PSTR("AreaInfo")); + dump_field(out, ESPHOME_PSTR("area_id"), this->area_id); + dump_field(out, ESPHOME_PSTR("name"), this->name); return out.c_str(); } #endif #ifdef USE_DEVICES const char *DeviceInfo::dump_to(DumpBuffer &out) const { - MessageDumpHelper helper(out, "DeviceInfo"); - dump_field(out, "device_id", this->device_id); - dump_field(out, "name", this->name); - dump_field(out, "area_id", this->area_id); + MessageDumpHelper helper(out, ESPHOME_PSTR("DeviceInfo")); + dump_field(out, ESPHOME_PSTR("device_id"), this->device_id); + dump_field(out, ESPHOME_PSTR("name"), this->name); + dump_field(out, ESPHOME_PSTR("area_id"), this->area_id); + return out.c_str(); +} +#endif +#ifdef USE_SERIAL_PROXY +const char *SerialProxyInfo::dump_to(DumpBuffer &out) const { + MessageDumpHelper helper(out, ESPHOME_PSTR("SerialProxyInfo")); + dump_field(out, ESPHOME_PSTR("name"), this->name); + dump_field(out, ESPHOME_PSTR("port_type"), static_cast(this->port_type)); return out.c_str(); } #endif const char *DeviceInfoResponse::dump_to(DumpBuffer &out) const { - MessageDumpHelper helper(out, "DeviceInfoResponse"); - dump_field(out, "name", this->name); - dump_field(out, "mac_address", this->mac_address); - dump_field(out, "esphome_version", this->esphome_version); - dump_field(out, "compilation_time", this->compilation_time); - dump_field(out, "model", this->model); + MessageDumpHelper helper(out, ESPHOME_PSTR("DeviceInfoResponse")); + dump_field(out, ESPHOME_PSTR("name"), this->name); + dump_field(out, ESPHOME_PSTR("mac_address"), this->mac_address); + dump_field(out, ESPHOME_PSTR("esphome_version"), this->esphome_version); + dump_field(out, ESPHOME_PSTR("compilation_time"), this->compilation_time); + dump_field(out, ESPHOME_PSTR("model"), this->model); #ifdef USE_DEEP_SLEEP - dump_field(out, "has_deep_sleep", this->has_deep_sleep); + dump_field(out, ESPHOME_PSTR("has_deep_sleep"), this->has_deep_sleep); #endif #ifdef ESPHOME_PROJECT_NAME - dump_field(out, "project_name", this->project_name); + dump_field(out, ESPHOME_PSTR("project_name"), this->project_name); #endif #ifdef ESPHOME_PROJECT_NAME - dump_field(out, "project_version", this->project_version); + dump_field(out, ESPHOME_PSTR("project_version"), this->project_version); #endif #ifdef USE_WEBSERVER - dump_field(out, "webserver_port", this->webserver_port); + dump_field(out, ESPHOME_PSTR("webserver_port"), this->webserver_port); #endif #ifdef USE_BLUETOOTH_PROXY - dump_field(out, "bluetooth_proxy_feature_flags", this->bluetooth_proxy_feature_flags); + dump_field(out, ESPHOME_PSTR("bluetooth_proxy_feature_flags"), this->bluetooth_proxy_feature_flags); #endif - dump_field(out, "manufacturer", this->manufacturer); - dump_field(out, "friendly_name", this->friendly_name); + dump_field(out, ESPHOME_PSTR("manufacturer"), this->manufacturer); + dump_field(out, ESPHOME_PSTR("friendly_name"), this->friendly_name); #ifdef USE_VOICE_ASSISTANT - dump_field(out, "voice_assistant_feature_flags", this->voice_assistant_feature_flags); + dump_field(out, ESPHOME_PSTR("voice_assistant_feature_flags"), this->voice_assistant_feature_flags); #endif #ifdef USE_AREAS - dump_field(out, "suggested_area", this->suggested_area); + dump_field(out, ESPHOME_PSTR("suggested_area"), this->suggested_area); #endif #ifdef USE_BLUETOOTH_PROXY - dump_field(out, "bluetooth_mac_address", this->bluetooth_mac_address); + dump_field(out, ESPHOME_PSTR("bluetooth_mac_address"), this->bluetooth_mac_address); #endif #ifdef USE_API_NOISE - dump_field(out, "api_encryption_supported", this->api_encryption_supported); + dump_field(out, ESPHOME_PSTR("api_encryption_supported"), this->api_encryption_supported); #endif #ifdef USE_DEVICES for (const auto &it : this->devices) { - out.append(" devices: "); + out.append(4, ' ').append_p(ESPHOME_PSTR("devices")).append(": "); it.dump_to(out); out.append("\n"); } #endif #ifdef USE_AREAS for (const auto &it : this->areas) { - out.append(" areas: "); + out.append(4, ' ').append_p(ESPHOME_PSTR("areas")).append(": "); it.dump_to(out); out.append("\n"); } #endif #ifdef USE_AREAS - out.append(" area: "); + out.append(2, ' ').append_p(ESPHOME_PSTR("area")).append(": "); this->area.dump_to(out); out.append("\n"); #endif #ifdef USE_ZWAVE_PROXY - dump_field(out, "zwave_proxy_feature_flags", this->zwave_proxy_feature_flags); + dump_field(out, ESPHOME_PSTR("zwave_proxy_feature_flags"), this->zwave_proxy_feature_flags); #endif #ifdef USE_ZWAVE_PROXY - dump_field(out, "zwave_home_id", this->zwave_home_id); + dump_field(out, ESPHOME_PSTR("zwave_home_id"), this->zwave_home_id); +#endif +#ifdef USE_SERIAL_PROXY + for (const auto &it : this->serial_proxies) { + out.append(4, ' ').append_p(ESPHOME_PSTR("serial_proxies")).append(": "); + it.dump_to(out); + out.append("\n"); + } #endif - return out.c_str(); -} -const char *ListEntitiesRequest::dump_to(DumpBuffer &out) const { - out.append("ListEntitiesRequest {}"); return out.c_str(); } const char *ListEntitiesDoneResponse::dump_to(DumpBuffer &out) const { - out.append("ListEntitiesDoneResponse {}"); - return out.c_str(); -} -const char *SubscribeStatesRequest::dump_to(DumpBuffer &out) const { - out.append("SubscribeStatesRequest {}"); + out.append_p(ESPHOME_PSTR("ListEntitiesDoneResponse {}")); return out.c_str(); } #ifdef USE_BINARY_SENSOR const char *ListEntitiesBinarySensorResponse::dump_to(DumpBuffer &out) const { - MessageDumpHelper helper(out, "ListEntitiesBinarySensorResponse"); - dump_field(out, "object_id", this->object_id); - dump_field(out, "key", this->key); - dump_field(out, "name", this->name); - dump_field(out, "device_class", this->device_class); - dump_field(out, "is_status_binary_sensor", this->is_status_binary_sensor); - dump_field(out, "disabled_by_default", this->disabled_by_default); + MessageDumpHelper helper(out, ESPHOME_PSTR("ListEntitiesBinarySensorResponse")); + dump_field(out, ESPHOME_PSTR("object_id"), this->object_id); + dump_field(out, ESPHOME_PSTR("key"), this->key); + dump_field(out, ESPHOME_PSTR("name"), this->name); + dump_field(out, ESPHOME_PSTR("device_class"), this->device_class); + dump_field(out, ESPHOME_PSTR("is_status_binary_sensor"), this->is_status_binary_sensor); + dump_field(out, ESPHOME_PSTR("disabled_by_default"), this->disabled_by_default); #ifdef USE_ENTITY_ICON - dump_field(out, "icon", this->icon); + dump_field(out, ESPHOME_PSTR("icon"), this->icon); #endif - dump_field(out, "entity_category", static_cast(this->entity_category)); + dump_field(out, ESPHOME_PSTR("entity_category"), static_cast(this->entity_category)); #ifdef USE_DEVICES - dump_field(out, "device_id", this->device_id); + dump_field(out, ESPHOME_PSTR("device_id"), this->device_id); #endif return out.c_str(); } const char *BinarySensorStateResponse::dump_to(DumpBuffer &out) const { - MessageDumpHelper helper(out, "BinarySensorStateResponse"); - dump_field(out, "key", this->key); - dump_field(out, "state", this->state); - dump_field(out, "missing_state", this->missing_state); + MessageDumpHelper helper(out, ESPHOME_PSTR("BinarySensorStateResponse")); + dump_field(out, ESPHOME_PSTR("key"), this->key); + dump_field(out, ESPHOME_PSTR("state"), this->state); + dump_field(out, ESPHOME_PSTR("missing_state"), this->missing_state); #ifdef USE_DEVICES - dump_field(out, "device_id", this->device_id); + dump_field(out, ESPHOME_PSTR("device_id"), this->device_id); #endif return out.c_str(); } #endif #ifdef USE_COVER const char *ListEntitiesCoverResponse::dump_to(DumpBuffer &out) const { - MessageDumpHelper helper(out, "ListEntitiesCoverResponse"); - dump_field(out, "object_id", this->object_id); - dump_field(out, "key", this->key); - dump_field(out, "name", this->name); - dump_field(out, "assumed_state", this->assumed_state); - dump_field(out, "supports_position", this->supports_position); - dump_field(out, "supports_tilt", this->supports_tilt); - dump_field(out, "device_class", this->device_class); - dump_field(out, "disabled_by_default", this->disabled_by_default); + MessageDumpHelper helper(out, ESPHOME_PSTR("ListEntitiesCoverResponse")); + dump_field(out, ESPHOME_PSTR("object_id"), this->object_id); + dump_field(out, ESPHOME_PSTR("key"), this->key); + dump_field(out, ESPHOME_PSTR("name"), this->name); + dump_field(out, ESPHOME_PSTR("assumed_state"), this->assumed_state); + dump_field(out, ESPHOME_PSTR("supports_position"), this->supports_position); + dump_field(out, ESPHOME_PSTR("supports_tilt"), this->supports_tilt); + dump_field(out, ESPHOME_PSTR("device_class"), this->device_class); + dump_field(out, ESPHOME_PSTR("disabled_by_default"), this->disabled_by_default); #ifdef USE_ENTITY_ICON - dump_field(out, "icon", this->icon); + dump_field(out, ESPHOME_PSTR("icon"), this->icon); #endif - dump_field(out, "entity_category", static_cast(this->entity_category)); - dump_field(out, "supports_stop", this->supports_stop); + dump_field(out, ESPHOME_PSTR("entity_category"), static_cast(this->entity_category)); + dump_field(out, ESPHOME_PSTR("supports_stop"), this->supports_stop); #ifdef USE_DEVICES - dump_field(out, "device_id", this->device_id); + dump_field(out, ESPHOME_PSTR("device_id"), this->device_id); #endif return out.c_str(); } const char *CoverStateResponse::dump_to(DumpBuffer &out) const { - MessageDumpHelper helper(out, "CoverStateResponse"); - dump_field(out, "key", this->key); - dump_field(out, "position", this->position); - dump_field(out, "tilt", this->tilt); - dump_field(out, "current_operation", static_cast(this->current_operation)); + MessageDumpHelper helper(out, ESPHOME_PSTR("CoverStateResponse")); + dump_field(out, ESPHOME_PSTR("key"), this->key); + dump_field(out, ESPHOME_PSTR("position"), this->position); + dump_field(out, ESPHOME_PSTR("tilt"), this->tilt); + dump_field(out, ESPHOME_PSTR("current_operation"), static_cast(this->current_operation)); #ifdef USE_DEVICES - dump_field(out, "device_id", this->device_id); + dump_field(out, ESPHOME_PSTR("device_id"), this->device_id); #endif return out.c_str(); } const char *CoverCommandRequest::dump_to(DumpBuffer &out) const { - MessageDumpHelper helper(out, "CoverCommandRequest"); - dump_field(out, "key", this->key); - dump_field(out, "has_position", this->has_position); - dump_field(out, "position", this->position); - dump_field(out, "has_tilt", this->has_tilt); - dump_field(out, "tilt", this->tilt); - dump_field(out, "stop", this->stop); + MessageDumpHelper helper(out, ESPHOME_PSTR("CoverCommandRequest")); + dump_field(out, ESPHOME_PSTR("key"), this->key); + dump_field(out, ESPHOME_PSTR("has_position"), this->has_position); + dump_field(out, ESPHOME_PSTR("position"), this->position); + dump_field(out, ESPHOME_PSTR("has_tilt"), this->has_tilt); + dump_field(out, ESPHOME_PSTR("tilt"), this->tilt); + dump_field(out, ESPHOME_PSTR("stop"), this->stop); #ifdef USE_DEVICES - dump_field(out, "device_id", this->device_id); + dump_field(out, ESPHOME_PSTR("device_id"), this->device_id); #endif return out.c_str(); } #endif #ifdef USE_FAN const char *ListEntitiesFanResponse::dump_to(DumpBuffer &out) const { - MessageDumpHelper helper(out, "ListEntitiesFanResponse"); - dump_field(out, "object_id", this->object_id); - dump_field(out, "key", this->key); - dump_field(out, "name", this->name); - dump_field(out, "supports_oscillation", this->supports_oscillation); - dump_field(out, "supports_speed", this->supports_speed); - dump_field(out, "supports_direction", this->supports_direction); - dump_field(out, "supported_speed_count", this->supported_speed_count); - dump_field(out, "disabled_by_default", this->disabled_by_default); + MessageDumpHelper helper(out, ESPHOME_PSTR("ListEntitiesFanResponse")); + dump_field(out, ESPHOME_PSTR("object_id"), this->object_id); + dump_field(out, ESPHOME_PSTR("key"), this->key); + dump_field(out, ESPHOME_PSTR("name"), this->name); + dump_field(out, ESPHOME_PSTR("supports_oscillation"), this->supports_oscillation); + dump_field(out, ESPHOME_PSTR("supports_speed"), this->supports_speed); + dump_field(out, ESPHOME_PSTR("supports_direction"), this->supports_direction); + dump_field(out, ESPHOME_PSTR("supported_speed_count"), this->supported_speed_count); + dump_field(out, ESPHOME_PSTR("disabled_by_default"), this->disabled_by_default); #ifdef USE_ENTITY_ICON - dump_field(out, "icon", this->icon); + dump_field(out, ESPHOME_PSTR("icon"), this->icon); #endif - dump_field(out, "entity_category", static_cast(this->entity_category)); + dump_field(out, ESPHOME_PSTR("entity_category"), static_cast(this->entity_category)); for (const auto &it : *this->supported_preset_modes) { - dump_field(out, "supported_preset_modes", it, 4); + dump_field(out, ESPHOME_PSTR("supported_preset_modes"), it, 4); } #ifdef USE_DEVICES - dump_field(out, "device_id", this->device_id); + dump_field(out, ESPHOME_PSTR("device_id"), this->device_id); #endif return out.c_str(); } const char *FanStateResponse::dump_to(DumpBuffer &out) const { - MessageDumpHelper helper(out, "FanStateResponse"); - dump_field(out, "key", this->key); - dump_field(out, "state", this->state); - dump_field(out, "oscillating", this->oscillating); - dump_field(out, "direction", static_cast(this->direction)); - dump_field(out, "speed_level", this->speed_level); - dump_field(out, "preset_mode", this->preset_mode); + MessageDumpHelper helper(out, ESPHOME_PSTR("FanStateResponse")); + dump_field(out, ESPHOME_PSTR("key"), this->key); + dump_field(out, ESPHOME_PSTR("state"), this->state); + dump_field(out, ESPHOME_PSTR("oscillating"), this->oscillating); + dump_field(out, ESPHOME_PSTR("direction"), static_cast(this->direction)); + dump_field(out, ESPHOME_PSTR("speed_level"), this->speed_level); + dump_field(out, ESPHOME_PSTR("preset_mode"), this->preset_mode); #ifdef USE_DEVICES - dump_field(out, "device_id", this->device_id); + dump_field(out, ESPHOME_PSTR("device_id"), this->device_id); #endif return out.c_str(); } const char *FanCommandRequest::dump_to(DumpBuffer &out) const { - MessageDumpHelper helper(out, "FanCommandRequest"); - dump_field(out, "key", this->key); - dump_field(out, "has_state", this->has_state); - dump_field(out, "state", this->state); - dump_field(out, "has_oscillating", this->has_oscillating); - dump_field(out, "oscillating", this->oscillating); - dump_field(out, "has_direction", this->has_direction); - dump_field(out, "direction", static_cast(this->direction)); - dump_field(out, "has_speed_level", this->has_speed_level); - dump_field(out, "speed_level", this->speed_level); - dump_field(out, "has_preset_mode", this->has_preset_mode); - dump_field(out, "preset_mode", this->preset_mode); + MessageDumpHelper helper(out, ESPHOME_PSTR("FanCommandRequest")); + dump_field(out, ESPHOME_PSTR("key"), this->key); + dump_field(out, ESPHOME_PSTR("has_state"), this->has_state); + dump_field(out, ESPHOME_PSTR("state"), this->state); + dump_field(out, ESPHOME_PSTR("has_oscillating"), this->has_oscillating); + dump_field(out, ESPHOME_PSTR("oscillating"), this->oscillating); + dump_field(out, ESPHOME_PSTR("has_direction"), this->has_direction); + dump_field(out, ESPHOME_PSTR("direction"), static_cast(this->direction)); + dump_field(out, ESPHOME_PSTR("has_speed_level"), this->has_speed_level); + dump_field(out, ESPHOME_PSTR("speed_level"), this->speed_level); + dump_field(out, ESPHOME_PSTR("has_preset_mode"), this->has_preset_mode); + dump_field(out, ESPHOME_PSTR("preset_mode"), this->preset_mode); #ifdef USE_DEVICES - dump_field(out, "device_id", this->device_id); + dump_field(out, ESPHOME_PSTR("device_id"), this->device_id); #endif return out.c_str(); } #endif #ifdef USE_LIGHT const char *ListEntitiesLightResponse::dump_to(DumpBuffer &out) const { - MessageDumpHelper helper(out, "ListEntitiesLightResponse"); - dump_field(out, "object_id", this->object_id); - dump_field(out, "key", this->key); - dump_field(out, "name", this->name); + MessageDumpHelper helper(out, ESPHOME_PSTR("ListEntitiesLightResponse")); + dump_field(out, ESPHOME_PSTR("object_id"), this->object_id); + dump_field(out, ESPHOME_PSTR("key"), this->key); + dump_field(out, ESPHOME_PSTR("name"), this->name); for (const auto &it : *this->supported_color_modes) { - dump_field(out, "supported_color_modes", static_cast(it), 4); + dump_field(out, ESPHOME_PSTR("supported_color_modes"), static_cast(it), 4); } - dump_field(out, "min_mireds", this->min_mireds); - dump_field(out, "max_mireds", this->max_mireds); + dump_field(out, ESPHOME_PSTR("min_mireds"), this->min_mireds); + dump_field(out, ESPHOME_PSTR("max_mireds"), this->max_mireds); for (const auto &it : *this->effects) { - dump_field(out, "effects", it, 4); + dump_field(out, ESPHOME_PSTR("effects"), it, 4); } - dump_field(out, "disabled_by_default", this->disabled_by_default); + dump_field(out, ESPHOME_PSTR("disabled_by_default"), this->disabled_by_default); #ifdef USE_ENTITY_ICON - dump_field(out, "icon", this->icon); + dump_field(out, ESPHOME_PSTR("icon"), this->icon); #endif - dump_field(out, "entity_category", static_cast(this->entity_category)); + dump_field(out, ESPHOME_PSTR("entity_category"), static_cast(this->entity_category)); #ifdef USE_DEVICES - dump_field(out, "device_id", this->device_id); + dump_field(out, ESPHOME_PSTR("device_id"), this->device_id); #endif return out.c_str(); } const char *LightStateResponse::dump_to(DumpBuffer &out) const { - MessageDumpHelper helper(out, "LightStateResponse"); - dump_field(out, "key", this->key); - dump_field(out, "state", this->state); - dump_field(out, "brightness", this->brightness); - dump_field(out, "color_mode", static_cast(this->color_mode)); - dump_field(out, "color_brightness", this->color_brightness); - dump_field(out, "red", this->red); - dump_field(out, "green", this->green); - dump_field(out, "blue", this->blue); - dump_field(out, "white", this->white); - dump_field(out, "color_temperature", this->color_temperature); - dump_field(out, "cold_white", this->cold_white); - dump_field(out, "warm_white", this->warm_white); - dump_field(out, "effect", this->effect); + MessageDumpHelper helper(out, ESPHOME_PSTR("LightStateResponse")); + dump_field(out, ESPHOME_PSTR("key"), this->key); + dump_field(out, ESPHOME_PSTR("state"), this->state); + dump_field(out, ESPHOME_PSTR("brightness"), this->brightness); + dump_field(out, ESPHOME_PSTR("color_mode"), static_cast(this->color_mode)); + dump_field(out, ESPHOME_PSTR("color_brightness"), this->color_brightness); + dump_field(out, ESPHOME_PSTR("red"), this->red); + dump_field(out, ESPHOME_PSTR("green"), this->green); + dump_field(out, ESPHOME_PSTR("blue"), this->blue); + dump_field(out, ESPHOME_PSTR("white"), this->white); + dump_field(out, ESPHOME_PSTR("color_temperature"), this->color_temperature); + dump_field(out, ESPHOME_PSTR("cold_white"), this->cold_white); + dump_field(out, ESPHOME_PSTR("warm_white"), this->warm_white); + dump_field(out, ESPHOME_PSTR("effect"), this->effect); #ifdef USE_DEVICES - dump_field(out, "device_id", this->device_id); + dump_field(out, ESPHOME_PSTR("device_id"), this->device_id); #endif return out.c_str(); } const char *LightCommandRequest::dump_to(DumpBuffer &out) const { - MessageDumpHelper helper(out, "LightCommandRequest"); - dump_field(out, "key", this->key); - dump_field(out, "has_state", this->has_state); - dump_field(out, "state", this->state); - dump_field(out, "has_brightness", this->has_brightness); - dump_field(out, "brightness", this->brightness); - dump_field(out, "has_color_mode", this->has_color_mode); - dump_field(out, "color_mode", static_cast(this->color_mode)); - dump_field(out, "has_color_brightness", this->has_color_brightness); - dump_field(out, "color_brightness", this->color_brightness); - dump_field(out, "has_rgb", this->has_rgb); - dump_field(out, "red", this->red); - dump_field(out, "green", this->green); - dump_field(out, "blue", this->blue); - dump_field(out, "has_white", this->has_white); - dump_field(out, "white", this->white); - dump_field(out, "has_color_temperature", this->has_color_temperature); - dump_field(out, "color_temperature", this->color_temperature); - dump_field(out, "has_cold_white", this->has_cold_white); - dump_field(out, "cold_white", this->cold_white); - dump_field(out, "has_warm_white", this->has_warm_white); - dump_field(out, "warm_white", this->warm_white); - dump_field(out, "has_transition_length", this->has_transition_length); - dump_field(out, "transition_length", this->transition_length); - dump_field(out, "has_flash_length", this->has_flash_length); - dump_field(out, "flash_length", this->flash_length); - dump_field(out, "has_effect", this->has_effect); - dump_field(out, "effect", this->effect); + MessageDumpHelper helper(out, ESPHOME_PSTR("LightCommandRequest")); + dump_field(out, ESPHOME_PSTR("key"), this->key); + dump_field(out, ESPHOME_PSTR("has_state"), this->has_state); + dump_field(out, ESPHOME_PSTR("state"), this->state); + dump_field(out, ESPHOME_PSTR("has_brightness"), this->has_brightness); + dump_field(out, ESPHOME_PSTR("brightness"), this->brightness); + dump_field(out, ESPHOME_PSTR("has_color_mode"), this->has_color_mode); + dump_field(out, ESPHOME_PSTR("color_mode"), static_cast(this->color_mode)); + dump_field(out, ESPHOME_PSTR("has_color_brightness"), this->has_color_brightness); + dump_field(out, ESPHOME_PSTR("color_brightness"), this->color_brightness); + dump_field(out, ESPHOME_PSTR("has_rgb"), this->has_rgb); + dump_field(out, ESPHOME_PSTR("red"), this->red); + dump_field(out, ESPHOME_PSTR("green"), this->green); + dump_field(out, ESPHOME_PSTR("blue"), this->blue); + dump_field(out, ESPHOME_PSTR("has_white"), this->has_white); + dump_field(out, ESPHOME_PSTR("white"), this->white); + dump_field(out, ESPHOME_PSTR("has_color_temperature"), this->has_color_temperature); + dump_field(out, ESPHOME_PSTR("color_temperature"), this->color_temperature); + dump_field(out, ESPHOME_PSTR("has_cold_white"), this->has_cold_white); + dump_field(out, ESPHOME_PSTR("cold_white"), this->cold_white); + dump_field(out, ESPHOME_PSTR("has_warm_white"), this->has_warm_white); + dump_field(out, ESPHOME_PSTR("warm_white"), this->warm_white); + dump_field(out, ESPHOME_PSTR("has_transition_length"), this->has_transition_length); + dump_field(out, ESPHOME_PSTR("transition_length"), this->transition_length); + dump_field(out, ESPHOME_PSTR("has_flash_length"), this->has_flash_length); + dump_field(out, ESPHOME_PSTR("flash_length"), this->flash_length); + dump_field(out, ESPHOME_PSTR("has_effect"), this->has_effect); + dump_field(out, ESPHOME_PSTR("effect"), this->effect); #ifdef USE_DEVICES - dump_field(out, "device_id", this->device_id); + dump_field(out, ESPHOME_PSTR("device_id"), this->device_id); #endif return out.c_str(); } #endif #ifdef USE_SENSOR const char *ListEntitiesSensorResponse::dump_to(DumpBuffer &out) const { - MessageDumpHelper helper(out, "ListEntitiesSensorResponse"); - dump_field(out, "object_id", this->object_id); - dump_field(out, "key", this->key); - dump_field(out, "name", this->name); + MessageDumpHelper helper(out, ESPHOME_PSTR("ListEntitiesSensorResponse")); + dump_field(out, ESPHOME_PSTR("object_id"), this->object_id); + dump_field(out, ESPHOME_PSTR("key"), this->key); + dump_field(out, ESPHOME_PSTR("name"), this->name); #ifdef USE_ENTITY_ICON - dump_field(out, "icon", this->icon); + dump_field(out, ESPHOME_PSTR("icon"), this->icon); #endif - dump_field(out, "unit_of_measurement", this->unit_of_measurement); - dump_field(out, "accuracy_decimals", this->accuracy_decimals); - dump_field(out, "force_update", this->force_update); - dump_field(out, "device_class", this->device_class); - dump_field(out, "state_class", static_cast(this->state_class)); - dump_field(out, "disabled_by_default", this->disabled_by_default); - dump_field(out, "entity_category", static_cast(this->entity_category)); + dump_field(out, ESPHOME_PSTR("unit_of_measurement"), this->unit_of_measurement); + dump_field(out, ESPHOME_PSTR("accuracy_decimals"), this->accuracy_decimals); + dump_field(out, ESPHOME_PSTR("force_update"), this->force_update); + dump_field(out, ESPHOME_PSTR("device_class"), this->device_class); + dump_field(out, ESPHOME_PSTR("state_class"), static_cast(this->state_class)); + dump_field(out, ESPHOME_PSTR("disabled_by_default"), this->disabled_by_default); + dump_field(out, ESPHOME_PSTR("entity_category"), static_cast(this->entity_category)); #ifdef USE_DEVICES - dump_field(out, "device_id", this->device_id); + dump_field(out, ESPHOME_PSTR("device_id"), this->device_id); #endif return out.c_str(); } const char *SensorStateResponse::dump_to(DumpBuffer &out) const { - MessageDumpHelper helper(out, "SensorStateResponse"); - dump_field(out, "key", this->key); - dump_field(out, "state", this->state); - dump_field(out, "missing_state", this->missing_state); + MessageDumpHelper helper(out, ESPHOME_PSTR("SensorStateResponse")); + dump_field(out, ESPHOME_PSTR("key"), this->key); + dump_field(out, ESPHOME_PSTR("state"), this->state); + dump_field(out, ESPHOME_PSTR("missing_state"), this->missing_state); #ifdef USE_DEVICES - dump_field(out, "device_id", this->device_id); + dump_field(out, ESPHOME_PSTR("device_id"), this->device_id); #endif return out.c_str(); } #endif #ifdef USE_SWITCH const char *ListEntitiesSwitchResponse::dump_to(DumpBuffer &out) const { - MessageDumpHelper helper(out, "ListEntitiesSwitchResponse"); - dump_field(out, "object_id", this->object_id); - dump_field(out, "key", this->key); - dump_field(out, "name", this->name); + MessageDumpHelper helper(out, ESPHOME_PSTR("ListEntitiesSwitchResponse")); + dump_field(out, ESPHOME_PSTR("object_id"), this->object_id); + dump_field(out, ESPHOME_PSTR("key"), this->key); + dump_field(out, ESPHOME_PSTR("name"), this->name); #ifdef USE_ENTITY_ICON - dump_field(out, "icon", this->icon); + dump_field(out, ESPHOME_PSTR("icon"), this->icon); #endif - dump_field(out, "assumed_state", this->assumed_state); - dump_field(out, "disabled_by_default", this->disabled_by_default); - dump_field(out, "entity_category", static_cast(this->entity_category)); - dump_field(out, "device_class", this->device_class); + dump_field(out, ESPHOME_PSTR("assumed_state"), this->assumed_state); + dump_field(out, ESPHOME_PSTR("disabled_by_default"), this->disabled_by_default); + dump_field(out, ESPHOME_PSTR("entity_category"), static_cast(this->entity_category)); + dump_field(out, ESPHOME_PSTR("device_class"), this->device_class); #ifdef USE_DEVICES - dump_field(out, "device_id", this->device_id); + dump_field(out, ESPHOME_PSTR("device_id"), this->device_id); #endif return out.c_str(); } const char *SwitchStateResponse::dump_to(DumpBuffer &out) const { - MessageDumpHelper helper(out, "SwitchStateResponse"); - dump_field(out, "key", this->key); - dump_field(out, "state", this->state); + MessageDumpHelper helper(out, ESPHOME_PSTR("SwitchStateResponse")); + dump_field(out, ESPHOME_PSTR("key"), this->key); + dump_field(out, ESPHOME_PSTR("state"), this->state); #ifdef USE_DEVICES - dump_field(out, "device_id", this->device_id); + dump_field(out, ESPHOME_PSTR("device_id"), this->device_id); #endif return out.c_str(); } const char *SwitchCommandRequest::dump_to(DumpBuffer &out) const { - MessageDumpHelper helper(out, "SwitchCommandRequest"); - dump_field(out, "key", this->key); - dump_field(out, "state", this->state); + MessageDumpHelper helper(out, ESPHOME_PSTR("SwitchCommandRequest")); + dump_field(out, ESPHOME_PSTR("key"), this->key); + dump_field(out, ESPHOME_PSTR("state"), this->state); #ifdef USE_DEVICES - dump_field(out, "device_id", this->device_id); + dump_field(out, ESPHOME_PSTR("device_id"), this->device_id); #endif return out.c_str(); } #endif #ifdef USE_TEXT_SENSOR const char *ListEntitiesTextSensorResponse::dump_to(DumpBuffer &out) const { - MessageDumpHelper helper(out, "ListEntitiesTextSensorResponse"); - dump_field(out, "object_id", this->object_id); - dump_field(out, "key", this->key); - dump_field(out, "name", this->name); + MessageDumpHelper helper(out, ESPHOME_PSTR("ListEntitiesTextSensorResponse")); + dump_field(out, ESPHOME_PSTR("object_id"), this->object_id); + dump_field(out, ESPHOME_PSTR("key"), this->key); + dump_field(out, ESPHOME_PSTR("name"), this->name); #ifdef USE_ENTITY_ICON - dump_field(out, "icon", this->icon); + dump_field(out, ESPHOME_PSTR("icon"), this->icon); #endif - dump_field(out, "disabled_by_default", this->disabled_by_default); - dump_field(out, "entity_category", static_cast(this->entity_category)); - dump_field(out, "device_class", this->device_class); + dump_field(out, ESPHOME_PSTR("disabled_by_default"), this->disabled_by_default); + dump_field(out, ESPHOME_PSTR("entity_category"), static_cast(this->entity_category)); + dump_field(out, ESPHOME_PSTR("device_class"), this->device_class); #ifdef USE_DEVICES - dump_field(out, "device_id", this->device_id); + dump_field(out, ESPHOME_PSTR("device_id"), this->device_id); #endif return out.c_str(); } const char *TextSensorStateResponse::dump_to(DumpBuffer &out) const { - MessageDumpHelper helper(out, "TextSensorStateResponse"); - dump_field(out, "key", this->key); - dump_field(out, "state", this->state); - dump_field(out, "missing_state", this->missing_state); + MessageDumpHelper helper(out, ESPHOME_PSTR("TextSensorStateResponse")); + dump_field(out, ESPHOME_PSTR("key"), this->key); + dump_field(out, ESPHOME_PSTR("state"), this->state); + dump_field(out, ESPHOME_PSTR("missing_state"), this->missing_state); #ifdef USE_DEVICES - dump_field(out, "device_id", this->device_id); + dump_field(out, ESPHOME_PSTR("device_id"), this->device_id); #endif return out.c_str(); } #endif const char *SubscribeLogsRequest::dump_to(DumpBuffer &out) const { - MessageDumpHelper helper(out, "SubscribeLogsRequest"); - dump_field(out, "level", static_cast(this->level)); - dump_field(out, "dump_config", this->dump_config); + MessageDumpHelper helper(out, ESPHOME_PSTR("SubscribeLogsRequest")); + dump_field(out, ESPHOME_PSTR("level"), static_cast(this->level)); + dump_field(out, ESPHOME_PSTR("dump_config"), this->dump_config); return out.c_str(); } const char *SubscribeLogsResponse::dump_to(DumpBuffer &out) const { - MessageDumpHelper helper(out, "SubscribeLogsResponse"); - dump_field(out, "level", static_cast(this->level)); - dump_bytes_field(out, "message", this->message_ptr_, this->message_len_); + MessageDumpHelper helper(out, ESPHOME_PSTR("SubscribeLogsResponse")); + dump_field(out, ESPHOME_PSTR("level"), static_cast(this->level)); + dump_bytes_field(out, ESPHOME_PSTR("message"), this->message_ptr_, this->message_len_); return out.c_str(); } #ifdef USE_API_NOISE const char *NoiseEncryptionSetKeyRequest::dump_to(DumpBuffer &out) const { - MessageDumpHelper helper(out, "NoiseEncryptionSetKeyRequest"); - dump_bytes_field(out, "key", this->key, this->key_len); + MessageDumpHelper helper(out, ESPHOME_PSTR("NoiseEncryptionSetKeyRequest")); + dump_bytes_field(out, ESPHOME_PSTR("key"), this->key, this->key_len); return out.c_str(); } const char *NoiseEncryptionSetKeyResponse::dump_to(DumpBuffer &out) const { - MessageDumpHelper helper(out, "NoiseEncryptionSetKeyResponse"); - dump_field(out, "success", this->success); + MessageDumpHelper helper(out, ESPHOME_PSTR("NoiseEncryptionSetKeyResponse")); + dump_field(out, ESPHOME_PSTR("success"), this->success); return out.c_str(); } #endif #ifdef USE_API_HOMEASSISTANT_SERVICES -const char *SubscribeHomeassistantServicesRequest::dump_to(DumpBuffer &out) const { - out.append("SubscribeHomeassistantServicesRequest {}"); - return out.c_str(); -} const char *HomeassistantServiceMap::dump_to(DumpBuffer &out) const { - MessageDumpHelper helper(out, "HomeassistantServiceMap"); - dump_field(out, "key", this->key); - dump_field(out, "value", this->value); + MessageDumpHelper helper(out, ESPHOME_PSTR("HomeassistantServiceMap")); + dump_field(out, ESPHOME_PSTR("key"), this->key); + dump_field(out, ESPHOME_PSTR("value"), this->value); return out.c_str(); } const char *HomeassistantActionRequest::dump_to(DumpBuffer &out) const { - MessageDumpHelper helper(out, "HomeassistantActionRequest"); - dump_field(out, "service", this->service); + MessageDumpHelper helper(out, ESPHOME_PSTR("HomeassistantActionRequest")); + dump_field(out, ESPHOME_PSTR("service"), this->service); for (const auto &it : this->data) { - out.append(" data: "); + out.append(4, ' ').append_p(ESPHOME_PSTR("data")).append(": "); it.dump_to(out); out.append("\n"); } for (const auto &it : this->data_template) { - out.append(" data_template: "); + out.append(4, ' ').append_p(ESPHOME_PSTR("data_template")).append(": "); it.dump_to(out); out.append("\n"); } for (const auto &it : this->variables) { - out.append(" variables: "); + out.append(4, ' ').append_p(ESPHOME_PSTR("variables")).append(": "); it.dump_to(out); out.append("\n"); } - dump_field(out, "is_event", this->is_event); + dump_field(out, ESPHOME_PSTR("is_event"), this->is_event); #ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES - dump_field(out, "call_id", this->call_id); + dump_field(out, ESPHOME_PSTR("call_id"), this->call_id); #endif #ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES_JSON - dump_field(out, "wants_response", this->wants_response); + dump_field(out, ESPHOME_PSTR("wants_response"), this->wants_response); #endif #ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES_JSON - dump_field(out, "response_template", this->response_template); + dump_field(out, ESPHOME_PSTR("response_template"), this->response_template); #endif return out.c_str(); } #endif #ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES const char *HomeassistantActionResponse::dump_to(DumpBuffer &out) const { - MessageDumpHelper helper(out, "HomeassistantActionResponse"); - dump_field(out, "call_id", this->call_id); - dump_field(out, "success", this->success); - dump_field(out, "error_message", this->error_message); + MessageDumpHelper helper(out, ESPHOME_PSTR("HomeassistantActionResponse")); + dump_field(out, ESPHOME_PSTR("call_id"), this->call_id); + dump_field(out, ESPHOME_PSTR("success"), this->success); + dump_field(out, ESPHOME_PSTR("error_message"), this->error_message); #ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES_JSON - dump_bytes_field(out, "response_data", this->response_data, this->response_data_len); + dump_bytes_field(out, ESPHOME_PSTR("response_data"), this->response_data, this->response_data_len); #endif return out.c_str(); } #endif #ifdef USE_API_HOMEASSISTANT_STATES -const char *SubscribeHomeAssistantStatesRequest::dump_to(DumpBuffer &out) const { - out.append("SubscribeHomeAssistantStatesRequest {}"); - return out.c_str(); -} const char *SubscribeHomeAssistantStateResponse::dump_to(DumpBuffer &out) const { - MessageDumpHelper helper(out, "SubscribeHomeAssistantStateResponse"); - dump_field(out, "entity_id", this->entity_id); - dump_field(out, "attribute", this->attribute); - dump_field(out, "once", this->once); + MessageDumpHelper helper(out, ESPHOME_PSTR("SubscribeHomeAssistantStateResponse")); + dump_field(out, ESPHOME_PSTR("entity_id"), this->entity_id); + dump_field(out, ESPHOME_PSTR("attribute"), this->attribute); + dump_field(out, ESPHOME_PSTR("once"), this->once); return out.c_str(); } const char *HomeAssistantStateResponse::dump_to(DumpBuffer &out) const { - MessageDumpHelper helper(out, "HomeAssistantStateResponse"); - dump_field(out, "entity_id", this->entity_id); - dump_field(out, "state", this->state); - dump_field(out, "attribute", this->attribute); + MessageDumpHelper helper(out, ESPHOME_PSTR("HomeAssistantStateResponse")); + dump_field(out, ESPHOME_PSTR("entity_id"), this->entity_id); + dump_field(out, ESPHOME_PSTR("state"), this->state); + dump_field(out, ESPHOME_PSTR("attribute"), this->attribute); return out.c_str(); } #endif const char *GetTimeRequest::dump_to(DumpBuffer &out) const { - out.append("GetTimeRequest {}"); + out.append_p(ESPHOME_PSTR("GetTimeRequest {}")); + return out.c_str(); +} +const char *DSTRule::dump_to(DumpBuffer &out) const { + MessageDumpHelper helper(out, ESPHOME_PSTR("DSTRule")); + dump_field(out, ESPHOME_PSTR("time_seconds"), this->time_seconds); + dump_field(out, ESPHOME_PSTR("day"), this->day); + dump_field(out, ESPHOME_PSTR("type"), static_cast(this->type)); + dump_field(out, ESPHOME_PSTR("month"), this->month); + dump_field(out, ESPHOME_PSTR("week"), this->week); + dump_field(out, ESPHOME_PSTR("day_of_week"), this->day_of_week); + return out.c_str(); +} +const char *ParsedTimezone::dump_to(DumpBuffer &out) const { + MessageDumpHelper helper(out, ESPHOME_PSTR("ParsedTimezone")); + dump_field(out, ESPHOME_PSTR("std_offset_seconds"), this->std_offset_seconds); + dump_field(out, ESPHOME_PSTR("dst_offset_seconds"), this->dst_offset_seconds); + out.append(2, ' ').append_p(ESPHOME_PSTR("dst_start")).append(": "); + this->dst_start.dump_to(out); + out.append("\n"); + out.append(2, ' ').append_p(ESPHOME_PSTR("dst_end")).append(": "); + this->dst_end.dump_to(out); + out.append("\n"); return out.c_str(); } const char *GetTimeResponse::dump_to(DumpBuffer &out) const { - MessageDumpHelper helper(out, "GetTimeResponse"); - dump_field(out, "epoch_seconds", this->epoch_seconds); - dump_field(out, "timezone", this->timezone); + MessageDumpHelper helper(out, ESPHOME_PSTR("GetTimeResponse")); + dump_field(out, ESPHOME_PSTR("epoch_seconds"), this->epoch_seconds); + dump_field(out, ESPHOME_PSTR("timezone"), this->timezone); + out.append(2, ' ').append_p(ESPHOME_PSTR("parsed_timezone")).append(": "); + this->parsed_timezone.dump_to(out); + out.append("\n"); return out.c_str(); } #ifdef USE_API_USER_DEFINED_ACTIONS const char *ListEntitiesServicesArgument::dump_to(DumpBuffer &out) const { - MessageDumpHelper helper(out, "ListEntitiesServicesArgument"); - dump_field(out, "name", this->name); - dump_field(out, "type", static_cast(this->type)); + MessageDumpHelper helper(out, ESPHOME_PSTR("ListEntitiesServicesArgument")); + dump_field(out, ESPHOME_PSTR("name"), this->name); + dump_field(out, ESPHOME_PSTR("type"), static_cast(this->type)); return out.c_str(); } const char *ListEntitiesServicesResponse::dump_to(DumpBuffer &out) const { - MessageDumpHelper helper(out, "ListEntitiesServicesResponse"); - dump_field(out, "name", this->name); - dump_field(out, "key", this->key); + MessageDumpHelper helper(out, ESPHOME_PSTR("ListEntitiesServicesResponse")); + dump_field(out, ESPHOME_PSTR("name"), this->name); + dump_field(out, ESPHOME_PSTR("key"), this->key); for (const auto &it : this->args) { - out.append(" args: "); + out.append(4, ' ').append_p(ESPHOME_PSTR("args")).append(": "); it.dump_to(out); out.append("\n"); } - dump_field(out, "supports_response", static_cast(this->supports_response)); + dump_field(out, ESPHOME_PSTR("supports_response"), static_cast(this->supports_response)); return out.c_str(); } const char *ExecuteServiceArgument::dump_to(DumpBuffer &out) const { - MessageDumpHelper helper(out, "ExecuteServiceArgument"); - dump_field(out, "bool_", this->bool_); - dump_field(out, "legacy_int", this->legacy_int); - dump_field(out, "float_", this->float_); - dump_field(out, "string_", this->string_); - dump_field(out, "int_", this->int_); + MessageDumpHelper helper(out, ESPHOME_PSTR("ExecuteServiceArgument")); + dump_field(out, ESPHOME_PSTR("bool_"), this->bool_); + dump_field(out, ESPHOME_PSTR("legacy_int"), this->legacy_int); + dump_field(out, ESPHOME_PSTR("float_"), this->float_); + dump_field(out, ESPHOME_PSTR("string_"), this->string_); + dump_field(out, ESPHOME_PSTR("int_"), this->int_); for (const auto it : this->bool_array) { - dump_field(out, "bool_array", static_cast(it), 4); + dump_field(out, ESPHOME_PSTR("bool_array"), static_cast(it), 4); } for (const auto &it : this->int_array) { - dump_field(out, "int_array", it, 4); + dump_field(out, ESPHOME_PSTR("int_array"), it, 4); } for (const auto &it : this->float_array) { - dump_field(out, "float_array", it, 4); + dump_field(out, ESPHOME_PSTR("float_array"), it, 4); } for (const auto &it : this->string_array) { - dump_field(out, "string_array", it, 4); + dump_field(out, ESPHOME_PSTR("string_array"), it, 4); } return out.c_str(); } const char *ExecuteServiceRequest::dump_to(DumpBuffer &out) const { - MessageDumpHelper helper(out, "ExecuteServiceRequest"); - dump_field(out, "key", this->key); + MessageDumpHelper helper(out, ESPHOME_PSTR("ExecuteServiceRequest")); + dump_field(out, ESPHOME_PSTR("key"), this->key); for (const auto &it : this->args) { - out.append(" args: "); + out.append(4, ' ').append_p(ESPHOME_PSTR("args")).append(": "); it.dump_to(out); out.append("\n"); } #ifdef USE_API_USER_DEFINED_ACTION_RESPONSES - dump_field(out, "call_id", this->call_id); + dump_field(out, ESPHOME_PSTR("call_id"), this->call_id); #endif #ifdef USE_API_USER_DEFINED_ACTION_RESPONSES - dump_field(out, "return_response", this->return_response); + dump_field(out, ESPHOME_PSTR("return_response"), this->return_response); #endif return out.c_str(); } #endif #ifdef USE_API_USER_DEFINED_ACTION_RESPONSES const char *ExecuteServiceResponse::dump_to(DumpBuffer &out) const { - MessageDumpHelper helper(out, "ExecuteServiceResponse"); - dump_field(out, "call_id", this->call_id); - dump_field(out, "success", this->success); - dump_field(out, "error_message", this->error_message); + MessageDumpHelper helper(out, ESPHOME_PSTR("ExecuteServiceResponse")); + dump_field(out, ESPHOME_PSTR("call_id"), this->call_id); + dump_field(out, ESPHOME_PSTR("success"), this->success); + dump_field(out, ESPHOME_PSTR("error_message"), this->error_message); #ifdef USE_API_USER_DEFINED_ACTION_RESPONSES_JSON - dump_bytes_field(out, "response_data", this->response_data, this->response_data_len); + dump_bytes_field(out, ESPHOME_PSTR("response_data"), this->response_data, this->response_data_len); #endif return out.c_str(); } #endif #ifdef USE_CAMERA const char *ListEntitiesCameraResponse::dump_to(DumpBuffer &out) const { - MessageDumpHelper helper(out, "ListEntitiesCameraResponse"); - dump_field(out, "object_id", this->object_id); - dump_field(out, "key", this->key); - dump_field(out, "name", this->name); - dump_field(out, "disabled_by_default", this->disabled_by_default); + MessageDumpHelper helper(out, ESPHOME_PSTR("ListEntitiesCameraResponse")); + dump_field(out, ESPHOME_PSTR("object_id"), this->object_id); + dump_field(out, ESPHOME_PSTR("key"), this->key); + dump_field(out, ESPHOME_PSTR("name"), this->name); + dump_field(out, ESPHOME_PSTR("disabled_by_default"), this->disabled_by_default); #ifdef USE_ENTITY_ICON - dump_field(out, "icon", this->icon); + dump_field(out, ESPHOME_PSTR("icon"), this->icon); #endif - dump_field(out, "entity_category", static_cast(this->entity_category)); + dump_field(out, ESPHOME_PSTR("entity_category"), static_cast(this->entity_category)); #ifdef USE_DEVICES - dump_field(out, "device_id", this->device_id); + dump_field(out, ESPHOME_PSTR("device_id"), this->device_id); #endif return out.c_str(); } const char *CameraImageResponse::dump_to(DumpBuffer &out) const { - MessageDumpHelper helper(out, "CameraImageResponse"); - dump_field(out, "key", this->key); - dump_bytes_field(out, "data", this->data_ptr_, this->data_len_); - dump_field(out, "done", this->done); + MessageDumpHelper helper(out, ESPHOME_PSTR("CameraImageResponse")); + dump_field(out, ESPHOME_PSTR("key"), this->key); + dump_bytes_field(out, ESPHOME_PSTR("data"), this->data_ptr_, this->data_len_); + dump_field(out, ESPHOME_PSTR("done"), this->done); #ifdef USE_DEVICES - dump_field(out, "device_id", this->device_id); + dump_field(out, ESPHOME_PSTR("device_id"), this->device_id); #endif return out.c_str(); } const char *CameraImageRequest::dump_to(DumpBuffer &out) const { - MessageDumpHelper helper(out, "CameraImageRequest"); - dump_field(out, "single", this->single); - dump_field(out, "stream", this->stream); + MessageDumpHelper helper(out, ESPHOME_PSTR("CameraImageRequest")); + dump_field(out, ESPHOME_PSTR("single"), this->single); + dump_field(out, ESPHOME_PSTR("stream"), this->stream); return out.c_str(); } #endif #ifdef USE_CLIMATE const char *ListEntitiesClimateResponse::dump_to(DumpBuffer &out) const { - MessageDumpHelper helper(out, "ListEntitiesClimateResponse"); - dump_field(out, "object_id", this->object_id); - dump_field(out, "key", this->key); - dump_field(out, "name", this->name); - dump_field(out, "supports_current_temperature", this->supports_current_temperature); - dump_field(out, "supports_two_point_target_temperature", this->supports_two_point_target_temperature); + MessageDumpHelper helper(out, ESPHOME_PSTR("ListEntitiesClimateResponse")); + dump_field(out, ESPHOME_PSTR("object_id"), this->object_id); + dump_field(out, ESPHOME_PSTR("key"), this->key); + dump_field(out, ESPHOME_PSTR("name"), this->name); + dump_field(out, ESPHOME_PSTR("supports_current_temperature"), this->supports_current_temperature); + dump_field(out, ESPHOME_PSTR("supports_two_point_target_temperature"), this->supports_two_point_target_temperature); for (const auto &it : *this->supported_modes) { - dump_field(out, "supported_modes", static_cast(it), 4); + dump_field(out, ESPHOME_PSTR("supported_modes"), static_cast(it), 4); } - dump_field(out, "visual_min_temperature", this->visual_min_temperature); - dump_field(out, "visual_max_temperature", this->visual_max_temperature); - dump_field(out, "visual_target_temperature_step", this->visual_target_temperature_step); - dump_field(out, "supports_action", this->supports_action); + dump_field(out, ESPHOME_PSTR("visual_min_temperature"), this->visual_min_temperature); + dump_field(out, ESPHOME_PSTR("visual_max_temperature"), this->visual_max_temperature); + dump_field(out, ESPHOME_PSTR("visual_target_temperature_step"), this->visual_target_temperature_step); + dump_field(out, ESPHOME_PSTR("supports_action"), this->supports_action); for (const auto &it : *this->supported_fan_modes) { - dump_field(out, "supported_fan_modes", static_cast(it), 4); + dump_field(out, ESPHOME_PSTR("supported_fan_modes"), static_cast(it), 4); } for (const auto &it : *this->supported_swing_modes) { - dump_field(out, "supported_swing_modes", static_cast(it), 4); + dump_field(out, ESPHOME_PSTR("supported_swing_modes"), static_cast(it), 4); } for (const auto &it : *this->supported_custom_fan_modes) { - dump_field(out, "supported_custom_fan_modes", it, 4); + dump_field(out, ESPHOME_PSTR("supported_custom_fan_modes"), it, 4); } for (const auto &it : *this->supported_presets) { - dump_field(out, "supported_presets", static_cast(it), 4); + dump_field(out, ESPHOME_PSTR("supported_presets"), static_cast(it), 4); } for (const auto &it : *this->supported_custom_presets) { - dump_field(out, "supported_custom_presets", it, 4); + dump_field(out, ESPHOME_PSTR("supported_custom_presets"), it, 4); } - dump_field(out, "disabled_by_default", this->disabled_by_default); + dump_field(out, ESPHOME_PSTR("disabled_by_default"), this->disabled_by_default); #ifdef USE_ENTITY_ICON - dump_field(out, "icon", this->icon); + dump_field(out, ESPHOME_PSTR("icon"), this->icon); #endif - dump_field(out, "entity_category", static_cast(this->entity_category)); - dump_field(out, "visual_current_temperature_step", this->visual_current_temperature_step); - dump_field(out, "supports_current_humidity", this->supports_current_humidity); - dump_field(out, "supports_target_humidity", this->supports_target_humidity); - dump_field(out, "visual_min_humidity", this->visual_min_humidity); - dump_field(out, "visual_max_humidity", this->visual_max_humidity); + dump_field(out, ESPHOME_PSTR("entity_category"), static_cast(this->entity_category)); + dump_field(out, ESPHOME_PSTR("visual_current_temperature_step"), this->visual_current_temperature_step); + dump_field(out, ESPHOME_PSTR("supports_current_humidity"), this->supports_current_humidity); + dump_field(out, ESPHOME_PSTR("supports_target_humidity"), this->supports_target_humidity); + dump_field(out, ESPHOME_PSTR("visual_min_humidity"), this->visual_min_humidity); + dump_field(out, ESPHOME_PSTR("visual_max_humidity"), this->visual_max_humidity); #ifdef USE_DEVICES - dump_field(out, "device_id", this->device_id); + dump_field(out, ESPHOME_PSTR("device_id"), this->device_id); #endif - dump_field(out, "feature_flags", this->feature_flags); + dump_field(out, ESPHOME_PSTR("feature_flags"), this->feature_flags); return out.c_str(); } const char *ClimateStateResponse::dump_to(DumpBuffer &out) const { - MessageDumpHelper helper(out, "ClimateStateResponse"); - dump_field(out, "key", this->key); - dump_field(out, "mode", static_cast(this->mode)); - dump_field(out, "current_temperature", this->current_temperature); - dump_field(out, "target_temperature", this->target_temperature); - dump_field(out, "target_temperature_low", this->target_temperature_low); - dump_field(out, "target_temperature_high", this->target_temperature_high); - dump_field(out, "action", static_cast(this->action)); - dump_field(out, "fan_mode", static_cast(this->fan_mode)); - dump_field(out, "swing_mode", static_cast(this->swing_mode)); - dump_field(out, "custom_fan_mode", this->custom_fan_mode); - dump_field(out, "preset", static_cast(this->preset)); - dump_field(out, "custom_preset", this->custom_preset); - dump_field(out, "current_humidity", this->current_humidity); - dump_field(out, "target_humidity", this->target_humidity); + MessageDumpHelper helper(out, ESPHOME_PSTR("ClimateStateResponse")); + dump_field(out, ESPHOME_PSTR("key"), this->key); + dump_field(out, ESPHOME_PSTR("mode"), static_cast(this->mode)); + dump_field(out, ESPHOME_PSTR("current_temperature"), this->current_temperature); + dump_field(out, ESPHOME_PSTR("target_temperature"), this->target_temperature); + dump_field(out, ESPHOME_PSTR("target_temperature_low"), this->target_temperature_low); + dump_field(out, ESPHOME_PSTR("target_temperature_high"), this->target_temperature_high); + dump_field(out, ESPHOME_PSTR("action"), static_cast(this->action)); + dump_field(out, ESPHOME_PSTR("fan_mode"), static_cast(this->fan_mode)); + dump_field(out, ESPHOME_PSTR("swing_mode"), static_cast(this->swing_mode)); + dump_field(out, ESPHOME_PSTR("custom_fan_mode"), this->custom_fan_mode); + dump_field(out, ESPHOME_PSTR("preset"), static_cast(this->preset)); + dump_field(out, ESPHOME_PSTR("custom_preset"), this->custom_preset); + dump_field(out, ESPHOME_PSTR("current_humidity"), this->current_humidity); + dump_field(out, ESPHOME_PSTR("target_humidity"), this->target_humidity); #ifdef USE_DEVICES - dump_field(out, "device_id", this->device_id); + dump_field(out, ESPHOME_PSTR("device_id"), this->device_id); #endif return out.c_str(); } const char *ClimateCommandRequest::dump_to(DumpBuffer &out) const { - MessageDumpHelper helper(out, "ClimateCommandRequest"); - dump_field(out, "key", this->key); - dump_field(out, "has_mode", this->has_mode); - dump_field(out, "mode", static_cast(this->mode)); - dump_field(out, "has_target_temperature", this->has_target_temperature); - dump_field(out, "target_temperature", this->target_temperature); - dump_field(out, "has_target_temperature_low", this->has_target_temperature_low); - dump_field(out, "target_temperature_low", this->target_temperature_low); - dump_field(out, "has_target_temperature_high", this->has_target_temperature_high); - dump_field(out, "target_temperature_high", this->target_temperature_high); - dump_field(out, "has_fan_mode", this->has_fan_mode); - dump_field(out, "fan_mode", static_cast(this->fan_mode)); - dump_field(out, "has_swing_mode", this->has_swing_mode); - dump_field(out, "swing_mode", static_cast(this->swing_mode)); - dump_field(out, "has_custom_fan_mode", this->has_custom_fan_mode); - dump_field(out, "custom_fan_mode", this->custom_fan_mode); - dump_field(out, "has_preset", this->has_preset); - dump_field(out, "preset", static_cast(this->preset)); - dump_field(out, "has_custom_preset", this->has_custom_preset); - dump_field(out, "custom_preset", this->custom_preset); - dump_field(out, "has_target_humidity", this->has_target_humidity); - dump_field(out, "target_humidity", this->target_humidity); + MessageDumpHelper helper(out, ESPHOME_PSTR("ClimateCommandRequest")); + dump_field(out, ESPHOME_PSTR("key"), this->key); + dump_field(out, ESPHOME_PSTR("has_mode"), this->has_mode); + dump_field(out, ESPHOME_PSTR("mode"), static_cast(this->mode)); + dump_field(out, ESPHOME_PSTR("has_target_temperature"), this->has_target_temperature); + dump_field(out, ESPHOME_PSTR("target_temperature"), this->target_temperature); + dump_field(out, ESPHOME_PSTR("has_target_temperature_low"), this->has_target_temperature_low); + dump_field(out, ESPHOME_PSTR("target_temperature_low"), this->target_temperature_low); + dump_field(out, ESPHOME_PSTR("has_target_temperature_high"), this->has_target_temperature_high); + dump_field(out, ESPHOME_PSTR("target_temperature_high"), this->target_temperature_high); + dump_field(out, ESPHOME_PSTR("has_fan_mode"), this->has_fan_mode); + dump_field(out, ESPHOME_PSTR("fan_mode"), static_cast(this->fan_mode)); + dump_field(out, ESPHOME_PSTR("has_swing_mode"), this->has_swing_mode); + dump_field(out, ESPHOME_PSTR("swing_mode"), static_cast(this->swing_mode)); + dump_field(out, ESPHOME_PSTR("has_custom_fan_mode"), this->has_custom_fan_mode); + dump_field(out, ESPHOME_PSTR("custom_fan_mode"), this->custom_fan_mode); + dump_field(out, ESPHOME_PSTR("has_preset"), this->has_preset); + dump_field(out, ESPHOME_PSTR("preset"), static_cast(this->preset)); + dump_field(out, ESPHOME_PSTR("has_custom_preset"), this->has_custom_preset); + dump_field(out, ESPHOME_PSTR("custom_preset"), this->custom_preset); + dump_field(out, ESPHOME_PSTR("has_target_humidity"), this->has_target_humidity); + dump_field(out, ESPHOME_PSTR("target_humidity"), this->target_humidity); #ifdef USE_DEVICES - dump_field(out, "device_id", this->device_id); + dump_field(out, ESPHOME_PSTR("device_id"), this->device_id); #endif return out.c_str(); } #endif #ifdef USE_WATER_HEATER const char *ListEntitiesWaterHeaterResponse::dump_to(DumpBuffer &out) const { - MessageDumpHelper helper(out, "ListEntitiesWaterHeaterResponse"); - dump_field(out, "object_id", this->object_id); - dump_field(out, "key", this->key); - dump_field(out, "name", this->name); + MessageDumpHelper helper(out, ESPHOME_PSTR("ListEntitiesWaterHeaterResponse")); + dump_field(out, ESPHOME_PSTR("object_id"), this->object_id); + dump_field(out, ESPHOME_PSTR("key"), this->key); + dump_field(out, ESPHOME_PSTR("name"), this->name); #ifdef USE_ENTITY_ICON - dump_field(out, "icon", this->icon); + dump_field(out, ESPHOME_PSTR("icon"), this->icon); #endif - dump_field(out, "disabled_by_default", this->disabled_by_default); - dump_field(out, "entity_category", static_cast(this->entity_category)); + dump_field(out, ESPHOME_PSTR("disabled_by_default"), this->disabled_by_default); + dump_field(out, ESPHOME_PSTR("entity_category"), static_cast(this->entity_category)); #ifdef USE_DEVICES - dump_field(out, "device_id", this->device_id); + dump_field(out, ESPHOME_PSTR("device_id"), this->device_id); #endif - dump_field(out, "min_temperature", this->min_temperature); - dump_field(out, "max_temperature", this->max_temperature); - dump_field(out, "target_temperature_step", this->target_temperature_step); + dump_field(out, ESPHOME_PSTR("min_temperature"), this->min_temperature); + dump_field(out, ESPHOME_PSTR("max_temperature"), this->max_temperature); + dump_field(out, ESPHOME_PSTR("target_temperature_step"), this->target_temperature_step); for (const auto &it : *this->supported_modes) { - dump_field(out, "supported_modes", static_cast(it), 4); + dump_field(out, ESPHOME_PSTR("supported_modes"), static_cast(it), 4); } - dump_field(out, "supported_features", this->supported_features); + dump_field(out, ESPHOME_PSTR("supported_features"), this->supported_features); return out.c_str(); } const char *WaterHeaterStateResponse::dump_to(DumpBuffer &out) const { - MessageDumpHelper helper(out, "WaterHeaterStateResponse"); - dump_field(out, "key", this->key); - dump_field(out, "current_temperature", this->current_temperature); - dump_field(out, "target_temperature", this->target_temperature); - dump_field(out, "mode", static_cast(this->mode)); + MessageDumpHelper helper(out, ESPHOME_PSTR("WaterHeaterStateResponse")); + dump_field(out, ESPHOME_PSTR("key"), this->key); + dump_field(out, ESPHOME_PSTR("current_temperature"), this->current_temperature); + dump_field(out, ESPHOME_PSTR("target_temperature"), this->target_temperature); + dump_field(out, ESPHOME_PSTR("mode"), static_cast(this->mode)); #ifdef USE_DEVICES - dump_field(out, "device_id", this->device_id); + dump_field(out, ESPHOME_PSTR("device_id"), this->device_id); #endif - dump_field(out, "state", this->state); - dump_field(out, "target_temperature_low", this->target_temperature_low); - dump_field(out, "target_temperature_high", this->target_temperature_high); + dump_field(out, ESPHOME_PSTR("state"), this->state); + dump_field(out, ESPHOME_PSTR("target_temperature_low"), this->target_temperature_low); + dump_field(out, ESPHOME_PSTR("target_temperature_high"), this->target_temperature_high); return out.c_str(); } const char *WaterHeaterCommandRequest::dump_to(DumpBuffer &out) const { - MessageDumpHelper helper(out, "WaterHeaterCommandRequest"); - dump_field(out, "key", this->key); - dump_field(out, "has_fields", this->has_fields); - dump_field(out, "mode", static_cast(this->mode)); - dump_field(out, "target_temperature", this->target_temperature); + MessageDumpHelper helper(out, ESPHOME_PSTR("WaterHeaterCommandRequest")); + dump_field(out, ESPHOME_PSTR("key"), this->key); + dump_field(out, ESPHOME_PSTR("has_fields"), this->has_fields); + dump_field(out, ESPHOME_PSTR("mode"), static_cast(this->mode)); + dump_field(out, ESPHOME_PSTR("target_temperature"), this->target_temperature); #ifdef USE_DEVICES - dump_field(out, "device_id", this->device_id); + dump_field(out, ESPHOME_PSTR("device_id"), this->device_id); #endif - dump_field(out, "state", this->state); - dump_field(out, "target_temperature_low", this->target_temperature_low); - dump_field(out, "target_temperature_high", this->target_temperature_high); + dump_field(out, ESPHOME_PSTR("state"), this->state); + dump_field(out, ESPHOME_PSTR("target_temperature_low"), this->target_temperature_low); + dump_field(out, ESPHOME_PSTR("target_temperature_high"), this->target_temperature_high); return out.c_str(); } #endif #ifdef USE_NUMBER const char *ListEntitiesNumberResponse::dump_to(DumpBuffer &out) const { - MessageDumpHelper helper(out, "ListEntitiesNumberResponse"); - dump_field(out, "object_id", this->object_id); - dump_field(out, "key", this->key); - dump_field(out, "name", this->name); + MessageDumpHelper helper(out, ESPHOME_PSTR("ListEntitiesNumberResponse")); + dump_field(out, ESPHOME_PSTR("object_id"), this->object_id); + dump_field(out, ESPHOME_PSTR("key"), this->key); + dump_field(out, ESPHOME_PSTR("name"), this->name); #ifdef USE_ENTITY_ICON - dump_field(out, "icon", this->icon); + dump_field(out, ESPHOME_PSTR("icon"), this->icon); #endif - dump_field(out, "min_value", this->min_value); - dump_field(out, "max_value", this->max_value); - dump_field(out, "step", this->step); - dump_field(out, "disabled_by_default", this->disabled_by_default); - dump_field(out, "entity_category", static_cast(this->entity_category)); - dump_field(out, "unit_of_measurement", this->unit_of_measurement); - dump_field(out, "mode", static_cast(this->mode)); - dump_field(out, "device_class", this->device_class); + dump_field(out, ESPHOME_PSTR("min_value"), this->min_value); + dump_field(out, ESPHOME_PSTR("max_value"), this->max_value); + dump_field(out, ESPHOME_PSTR("step"), this->step); + dump_field(out, ESPHOME_PSTR("disabled_by_default"), this->disabled_by_default); + dump_field(out, ESPHOME_PSTR("entity_category"), static_cast(this->entity_category)); + dump_field(out, ESPHOME_PSTR("unit_of_measurement"), this->unit_of_measurement); + dump_field(out, ESPHOME_PSTR("mode"), static_cast(this->mode)); + dump_field(out, ESPHOME_PSTR("device_class"), this->device_class); #ifdef USE_DEVICES - dump_field(out, "device_id", this->device_id); + dump_field(out, ESPHOME_PSTR("device_id"), this->device_id); #endif return out.c_str(); } const char *NumberStateResponse::dump_to(DumpBuffer &out) const { - MessageDumpHelper helper(out, "NumberStateResponse"); - dump_field(out, "key", this->key); - dump_field(out, "state", this->state); - dump_field(out, "missing_state", this->missing_state); + MessageDumpHelper helper(out, ESPHOME_PSTR("NumberStateResponse")); + dump_field(out, ESPHOME_PSTR("key"), this->key); + dump_field(out, ESPHOME_PSTR("state"), this->state); + dump_field(out, ESPHOME_PSTR("missing_state"), this->missing_state); #ifdef USE_DEVICES - dump_field(out, "device_id", this->device_id); + dump_field(out, ESPHOME_PSTR("device_id"), this->device_id); #endif return out.c_str(); } const char *NumberCommandRequest::dump_to(DumpBuffer &out) const { - MessageDumpHelper helper(out, "NumberCommandRequest"); - dump_field(out, "key", this->key); - dump_field(out, "state", this->state); + MessageDumpHelper helper(out, ESPHOME_PSTR("NumberCommandRequest")); + dump_field(out, ESPHOME_PSTR("key"), this->key); + dump_field(out, ESPHOME_PSTR("state"), this->state); #ifdef USE_DEVICES - dump_field(out, "device_id", this->device_id); + dump_field(out, ESPHOME_PSTR("device_id"), this->device_id); #endif return out.c_str(); } #endif #ifdef USE_SELECT const char *ListEntitiesSelectResponse::dump_to(DumpBuffer &out) const { - MessageDumpHelper helper(out, "ListEntitiesSelectResponse"); - dump_field(out, "object_id", this->object_id); - dump_field(out, "key", this->key); - dump_field(out, "name", this->name); + MessageDumpHelper helper(out, ESPHOME_PSTR("ListEntitiesSelectResponse")); + dump_field(out, ESPHOME_PSTR("object_id"), this->object_id); + dump_field(out, ESPHOME_PSTR("key"), this->key); + dump_field(out, ESPHOME_PSTR("name"), this->name); #ifdef USE_ENTITY_ICON - dump_field(out, "icon", this->icon); + dump_field(out, ESPHOME_PSTR("icon"), this->icon); #endif for (const auto &it : *this->options) { - dump_field(out, "options", it, 4); + dump_field(out, ESPHOME_PSTR("options"), it, 4); } - dump_field(out, "disabled_by_default", this->disabled_by_default); - dump_field(out, "entity_category", static_cast(this->entity_category)); + dump_field(out, ESPHOME_PSTR("disabled_by_default"), this->disabled_by_default); + dump_field(out, ESPHOME_PSTR("entity_category"), static_cast(this->entity_category)); #ifdef USE_DEVICES - dump_field(out, "device_id", this->device_id); + dump_field(out, ESPHOME_PSTR("device_id"), this->device_id); #endif return out.c_str(); } const char *SelectStateResponse::dump_to(DumpBuffer &out) const { - MessageDumpHelper helper(out, "SelectStateResponse"); - dump_field(out, "key", this->key); - dump_field(out, "state", this->state); - dump_field(out, "missing_state", this->missing_state); + MessageDumpHelper helper(out, ESPHOME_PSTR("SelectStateResponse")); + dump_field(out, ESPHOME_PSTR("key"), this->key); + dump_field(out, ESPHOME_PSTR("state"), this->state); + dump_field(out, ESPHOME_PSTR("missing_state"), this->missing_state); #ifdef USE_DEVICES - dump_field(out, "device_id", this->device_id); + dump_field(out, ESPHOME_PSTR("device_id"), this->device_id); #endif return out.c_str(); } const char *SelectCommandRequest::dump_to(DumpBuffer &out) const { - MessageDumpHelper helper(out, "SelectCommandRequest"); - dump_field(out, "key", this->key); - dump_field(out, "state", this->state); + MessageDumpHelper helper(out, ESPHOME_PSTR("SelectCommandRequest")); + dump_field(out, ESPHOME_PSTR("key"), this->key); + dump_field(out, ESPHOME_PSTR("state"), this->state); #ifdef USE_DEVICES - dump_field(out, "device_id", this->device_id); + dump_field(out, ESPHOME_PSTR("device_id"), this->device_id); #endif return out.c_str(); } #endif #ifdef USE_SIREN const char *ListEntitiesSirenResponse::dump_to(DumpBuffer &out) const { - MessageDumpHelper helper(out, "ListEntitiesSirenResponse"); - dump_field(out, "object_id", this->object_id); - dump_field(out, "key", this->key); - dump_field(out, "name", this->name); + MessageDumpHelper helper(out, ESPHOME_PSTR("ListEntitiesSirenResponse")); + dump_field(out, ESPHOME_PSTR("object_id"), this->object_id); + dump_field(out, ESPHOME_PSTR("key"), this->key); + dump_field(out, ESPHOME_PSTR("name"), this->name); #ifdef USE_ENTITY_ICON - dump_field(out, "icon", this->icon); + dump_field(out, ESPHOME_PSTR("icon"), this->icon); #endif - dump_field(out, "disabled_by_default", this->disabled_by_default); + dump_field(out, ESPHOME_PSTR("disabled_by_default"), this->disabled_by_default); for (const auto &it : *this->tones) { - dump_field(out, "tones", it, 4); + dump_field(out, ESPHOME_PSTR("tones"), it, 4); } - dump_field(out, "supports_duration", this->supports_duration); - dump_field(out, "supports_volume", this->supports_volume); - dump_field(out, "entity_category", static_cast(this->entity_category)); + dump_field(out, ESPHOME_PSTR("supports_duration"), this->supports_duration); + dump_field(out, ESPHOME_PSTR("supports_volume"), this->supports_volume); + dump_field(out, ESPHOME_PSTR("entity_category"), static_cast(this->entity_category)); #ifdef USE_DEVICES - dump_field(out, "device_id", this->device_id); + dump_field(out, ESPHOME_PSTR("device_id"), this->device_id); #endif return out.c_str(); } const char *SirenStateResponse::dump_to(DumpBuffer &out) const { - MessageDumpHelper helper(out, "SirenStateResponse"); - dump_field(out, "key", this->key); - dump_field(out, "state", this->state); + MessageDumpHelper helper(out, ESPHOME_PSTR("SirenStateResponse")); + dump_field(out, ESPHOME_PSTR("key"), this->key); + dump_field(out, ESPHOME_PSTR("state"), this->state); #ifdef USE_DEVICES - dump_field(out, "device_id", this->device_id); + dump_field(out, ESPHOME_PSTR("device_id"), this->device_id); #endif return out.c_str(); } const char *SirenCommandRequest::dump_to(DumpBuffer &out) const { - MessageDumpHelper helper(out, "SirenCommandRequest"); - dump_field(out, "key", this->key); - dump_field(out, "has_state", this->has_state); - dump_field(out, "state", this->state); - dump_field(out, "has_tone", this->has_tone); - dump_field(out, "tone", this->tone); - dump_field(out, "has_duration", this->has_duration); - dump_field(out, "duration", this->duration); - dump_field(out, "has_volume", this->has_volume); - dump_field(out, "volume", this->volume); + MessageDumpHelper helper(out, ESPHOME_PSTR("SirenCommandRequest")); + dump_field(out, ESPHOME_PSTR("key"), this->key); + dump_field(out, ESPHOME_PSTR("has_state"), this->has_state); + dump_field(out, ESPHOME_PSTR("state"), this->state); + dump_field(out, ESPHOME_PSTR("has_tone"), this->has_tone); + dump_field(out, ESPHOME_PSTR("tone"), this->tone); + dump_field(out, ESPHOME_PSTR("has_duration"), this->has_duration); + dump_field(out, ESPHOME_PSTR("duration"), this->duration); + dump_field(out, ESPHOME_PSTR("has_volume"), this->has_volume); + dump_field(out, ESPHOME_PSTR("volume"), this->volume); #ifdef USE_DEVICES - dump_field(out, "device_id", this->device_id); + dump_field(out, ESPHOME_PSTR("device_id"), this->device_id); #endif return out.c_str(); } #endif #ifdef USE_LOCK const char *ListEntitiesLockResponse::dump_to(DumpBuffer &out) const { - MessageDumpHelper helper(out, "ListEntitiesLockResponse"); - dump_field(out, "object_id", this->object_id); - dump_field(out, "key", this->key); - dump_field(out, "name", this->name); + MessageDumpHelper helper(out, ESPHOME_PSTR("ListEntitiesLockResponse")); + dump_field(out, ESPHOME_PSTR("object_id"), this->object_id); + dump_field(out, ESPHOME_PSTR("key"), this->key); + dump_field(out, ESPHOME_PSTR("name"), this->name); #ifdef USE_ENTITY_ICON - dump_field(out, "icon", this->icon); + dump_field(out, ESPHOME_PSTR("icon"), this->icon); #endif - dump_field(out, "disabled_by_default", this->disabled_by_default); - dump_field(out, "entity_category", static_cast(this->entity_category)); - dump_field(out, "assumed_state", this->assumed_state); - dump_field(out, "supports_open", this->supports_open); - dump_field(out, "requires_code", this->requires_code); - dump_field(out, "code_format", this->code_format); + dump_field(out, ESPHOME_PSTR("disabled_by_default"), this->disabled_by_default); + dump_field(out, ESPHOME_PSTR("entity_category"), static_cast(this->entity_category)); + dump_field(out, ESPHOME_PSTR("assumed_state"), this->assumed_state); + dump_field(out, ESPHOME_PSTR("supports_open"), this->supports_open); + dump_field(out, ESPHOME_PSTR("requires_code"), this->requires_code); + dump_field(out, ESPHOME_PSTR("code_format"), this->code_format); #ifdef USE_DEVICES - dump_field(out, "device_id", this->device_id); + dump_field(out, ESPHOME_PSTR("device_id"), this->device_id); #endif return out.c_str(); } const char *LockStateResponse::dump_to(DumpBuffer &out) const { - MessageDumpHelper helper(out, "LockStateResponse"); - dump_field(out, "key", this->key); - dump_field(out, "state", static_cast(this->state)); + MessageDumpHelper helper(out, ESPHOME_PSTR("LockStateResponse")); + dump_field(out, ESPHOME_PSTR("key"), this->key); + dump_field(out, ESPHOME_PSTR("state"), static_cast(this->state)); #ifdef USE_DEVICES - dump_field(out, "device_id", this->device_id); + dump_field(out, ESPHOME_PSTR("device_id"), this->device_id); #endif return out.c_str(); } const char *LockCommandRequest::dump_to(DumpBuffer &out) const { - MessageDumpHelper helper(out, "LockCommandRequest"); - dump_field(out, "key", this->key); - dump_field(out, "command", static_cast(this->command)); - dump_field(out, "has_code", this->has_code); - dump_field(out, "code", this->code); + MessageDumpHelper helper(out, ESPHOME_PSTR("LockCommandRequest")); + dump_field(out, ESPHOME_PSTR("key"), this->key); + dump_field(out, ESPHOME_PSTR("command"), static_cast(this->command)); + dump_field(out, ESPHOME_PSTR("has_code"), this->has_code); + dump_field(out, ESPHOME_PSTR("code"), this->code); #ifdef USE_DEVICES - dump_field(out, "device_id", this->device_id); + dump_field(out, ESPHOME_PSTR("device_id"), this->device_id); #endif return out.c_str(); } #endif #ifdef USE_BUTTON const char *ListEntitiesButtonResponse::dump_to(DumpBuffer &out) const { - MessageDumpHelper helper(out, "ListEntitiesButtonResponse"); - dump_field(out, "object_id", this->object_id); - dump_field(out, "key", this->key); - dump_field(out, "name", this->name); + MessageDumpHelper helper(out, ESPHOME_PSTR("ListEntitiesButtonResponse")); + dump_field(out, ESPHOME_PSTR("object_id"), this->object_id); + dump_field(out, ESPHOME_PSTR("key"), this->key); + dump_field(out, ESPHOME_PSTR("name"), this->name); #ifdef USE_ENTITY_ICON - dump_field(out, "icon", this->icon); + dump_field(out, ESPHOME_PSTR("icon"), this->icon); #endif - dump_field(out, "disabled_by_default", this->disabled_by_default); - dump_field(out, "entity_category", static_cast(this->entity_category)); - dump_field(out, "device_class", this->device_class); + dump_field(out, ESPHOME_PSTR("disabled_by_default"), this->disabled_by_default); + dump_field(out, ESPHOME_PSTR("entity_category"), static_cast(this->entity_category)); + dump_field(out, ESPHOME_PSTR("device_class"), this->device_class); #ifdef USE_DEVICES - dump_field(out, "device_id", this->device_id); + dump_field(out, ESPHOME_PSTR("device_id"), this->device_id); #endif return out.c_str(); } const char *ButtonCommandRequest::dump_to(DumpBuffer &out) const { - MessageDumpHelper helper(out, "ButtonCommandRequest"); - dump_field(out, "key", this->key); + MessageDumpHelper helper(out, ESPHOME_PSTR("ButtonCommandRequest")); + dump_field(out, ESPHOME_PSTR("key"), this->key); #ifdef USE_DEVICES - dump_field(out, "device_id", this->device_id); + dump_field(out, ESPHOME_PSTR("device_id"), this->device_id); #endif return out.c_str(); } #endif #ifdef USE_MEDIA_PLAYER const char *MediaPlayerSupportedFormat::dump_to(DumpBuffer &out) const { - MessageDumpHelper helper(out, "MediaPlayerSupportedFormat"); - dump_field(out, "format", this->format); - dump_field(out, "sample_rate", this->sample_rate); - dump_field(out, "num_channels", this->num_channels); - dump_field(out, "purpose", static_cast(this->purpose)); - dump_field(out, "sample_bytes", this->sample_bytes); + MessageDumpHelper helper(out, ESPHOME_PSTR("MediaPlayerSupportedFormat")); + dump_field(out, ESPHOME_PSTR("format"), this->format); + dump_field(out, ESPHOME_PSTR("sample_rate"), this->sample_rate); + dump_field(out, ESPHOME_PSTR("num_channels"), this->num_channels); + dump_field(out, ESPHOME_PSTR("purpose"), static_cast(this->purpose)); + dump_field(out, ESPHOME_PSTR("sample_bytes"), this->sample_bytes); return out.c_str(); } const char *ListEntitiesMediaPlayerResponse::dump_to(DumpBuffer &out) const { - MessageDumpHelper helper(out, "ListEntitiesMediaPlayerResponse"); - dump_field(out, "object_id", this->object_id); - dump_field(out, "key", this->key); - dump_field(out, "name", this->name); + MessageDumpHelper helper(out, ESPHOME_PSTR("ListEntitiesMediaPlayerResponse")); + dump_field(out, ESPHOME_PSTR("object_id"), this->object_id); + dump_field(out, ESPHOME_PSTR("key"), this->key); + dump_field(out, ESPHOME_PSTR("name"), this->name); #ifdef USE_ENTITY_ICON - dump_field(out, "icon", this->icon); + dump_field(out, ESPHOME_PSTR("icon"), this->icon); #endif - dump_field(out, "disabled_by_default", this->disabled_by_default); - dump_field(out, "entity_category", static_cast(this->entity_category)); - dump_field(out, "supports_pause", this->supports_pause); + dump_field(out, ESPHOME_PSTR("disabled_by_default"), this->disabled_by_default); + dump_field(out, ESPHOME_PSTR("entity_category"), static_cast(this->entity_category)); + dump_field(out, ESPHOME_PSTR("supports_pause"), this->supports_pause); for (const auto &it : this->supported_formats) { - out.append(" supported_formats: "); + out.append(4, ' ').append_p(ESPHOME_PSTR("supported_formats")).append(": "); it.dump_to(out); out.append("\n"); } #ifdef USE_DEVICES - dump_field(out, "device_id", this->device_id); + dump_field(out, ESPHOME_PSTR("device_id"), this->device_id); #endif - dump_field(out, "feature_flags", this->feature_flags); + dump_field(out, ESPHOME_PSTR("feature_flags"), this->feature_flags); return out.c_str(); } const char *MediaPlayerStateResponse::dump_to(DumpBuffer &out) const { - MessageDumpHelper helper(out, "MediaPlayerStateResponse"); - dump_field(out, "key", this->key); - dump_field(out, "state", static_cast(this->state)); - dump_field(out, "volume", this->volume); - dump_field(out, "muted", this->muted); + MessageDumpHelper helper(out, ESPHOME_PSTR("MediaPlayerStateResponse")); + dump_field(out, ESPHOME_PSTR("key"), this->key); + dump_field(out, ESPHOME_PSTR("state"), static_cast(this->state)); + dump_field(out, ESPHOME_PSTR("volume"), this->volume); + dump_field(out, ESPHOME_PSTR("muted"), this->muted); #ifdef USE_DEVICES - dump_field(out, "device_id", this->device_id); + dump_field(out, ESPHOME_PSTR("device_id"), this->device_id); #endif return out.c_str(); } const char *MediaPlayerCommandRequest::dump_to(DumpBuffer &out) const { - MessageDumpHelper helper(out, "MediaPlayerCommandRequest"); - dump_field(out, "key", this->key); - dump_field(out, "has_command", this->has_command); - dump_field(out, "command", static_cast(this->command)); - dump_field(out, "has_volume", this->has_volume); - dump_field(out, "volume", this->volume); - dump_field(out, "has_media_url", this->has_media_url); - dump_field(out, "media_url", this->media_url); - dump_field(out, "has_announcement", this->has_announcement); - dump_field(out, "announcement", this->announcement); + MessageDumpHelper helper(out, ESPHOME_PSTR("MediaPlayerCommandRequest")); + dump_field(out, ESPHOME_PSTR("key"), this->key); + dump_field(out, ESPHOME_PSTR("has_command"), this->has_command); + dump_field(out, ESPHOME_PSTR("command"), static_cast(this->command)); + dump_field(out, ESPHOME_PSTR("has_volume"), this->has_volume); + dump_field(out, ESPHOME_PSTR("volume"), this->volume); + dump_field(out, ESPHOME_PSTR("has_media_url"), this->has_media_url); + dump_field(out, ESPHOME_PSTR("media_url"), this->media_url); + dump_field(out, ESPHOME_PSTR("has_announcement"), this->has_announcement); + dump_field(out, ESPHOME_PSTR("announcement"), this->announcement); #ifdef USE_DEVICES - dump_field(out, "device_id", this->device_id); + dump_field(out, ESPHOME_PSTR("device_id"), this->device_id); #endif return out.c_str(); } #endif #ifdef USE_BLUETOOTH_PROXY const char *SubscribeBluetoothLEAdvertisementsRequest::dump_to(DumpBuffer &out) const { - MessageDumpHelper helper(out, "SubscribeBluetoothLEAdvertisementsRequest"); - dump_field(out, "flags", this->flags); + MessageDumpHelper helper(out, ESPHOME_PSTR("SubscribeBluetoothLEAdvertisementsRequest")); + dump_field(out, ESPHOME_PSTR("flags"), this->flags); return out.c_str(); } const char *BluetoothLERawAdvertisement::dump_to(DumpBuffer &out) const { - MessageDumpHelper helper(out, "BluetoothLERawAdvertisement"); - dump_field(out, "address", this->address); - dump_field(out, "rssi", this->rssi); - dump_field(out, "address_type", this->address_type); - dump_bytes_field(out, "data", this->data, this->data_len); + MessageDumpHelper helper(out, ESPHOME_PSTR("BluetoothLERawAdvertisement")); + dump_field(out, ESPHOME_PSTR("address"), this->address); + dump_field(out, ESPHOME_PSTR("rssi"), this->rssi); + dump_field(out, ESPHOME_PSTR("address_type"), this->address_type); + dump_bytes_field(out, ESPHOME_PSTR("data"), this->data, this->data_len); return out.c_str(); } const char *BluetoothLERawAdvertisementsResponse::dump_to(DumpBuffer &out) const { - MessageDumpHelper helper(out, "BluetoothLERawAdvertisementsResponse"); + MessageDumpHelper helper(out, ESPHOME_PSTR("BluetoothLERawAdvertisementsResponse")); for (uint16_t i = 0; i < this->advertisements_len; i++) { - out.append(" advertisements: "); + out.append(4, ' ').append_p(ESPHOME_PSTR("advertisements")).append(": "); this->advertisements[i].dump_to(out); out.append("\n"); } return out.c_str(); } const char *BluetoothDeviceRequest::dump_to(DumpBuffer &out) const { - MessageDumpHelper helper(out, "BluetoothDeviceRequest"); - dump_field(out, "address", this->address); - dump_field(out, "request_type", static_cast(this->request_type)); - dump_field(out, "has_address_type", this->has_address_type); - dump_field(out, "address_type", this->address_type); + MessageDumpHelper helper(out, ESPHOME_PSTR("BluetoothDeviceRequest")); + dump_field(out, ESPHOME_PSTR("address"), this->address); + dump_field(out, ESPHOME_PSTR("request_type"), static_cast(this->request_type)); + dump_field(out, ESPHOME_PSTR("has_address_type"), this->has_address_type); + dump_field(out, ESPHOME_PSTR("address_type"), this->address_type); return out.c_str(); } const char *BluetoothDeviceConnectionResponse::dump_to(DumpBuffer &out) const { - MessageDumpHelper helper(out, "BluetoothDeviceConnectionResponse"); - dump_field(out, "address", this->address); - dump_field(out, "connected", this->connected); - dump_field(out, "mtu", this->mtu); - dump_field(out, "error", this->error); + MessageDumpHelper helper(out, ESPHOME_PSTR("BluetoothDeviceConnectionResponse")); + dump_field(out, ESPHOME_PSTR("address"), this->address); + dump_field(out, ESPHOME_PSTR("connected"), this->connected); + dump_field(out, ESPHOME_PSTR("mtu"), this->mtu); + dump_field(out, ESPHOME_PSTR("error"), this->error); return out.c_str(); } const char *BluetoothGATTGetServicesRequest::dump_to(DumpBuffer &out) const { - MessageDumpHelper helper(out, "BluetoothGATTGetServicesRequest"); - dump_field(out, "address", this->address); + MessageDumpHelper helper(out, ESPHOME_PSTR("BluetoothGATTGetServicesRequest")); + dump_field(out, ESPHOME_PSTR("address"), this->address); return out.c_str(); } const char *BluetoothGATTDescriptor::dump_to(DumpBuffer &out) const { - MessageDumpHelper helper(out, "BluetoothGATTDescriptor"); + MessageDumpHelper helper(out, ESPHOME_PSTR("BluetoothGATTDescriptor")); for (const auto &it : this->uuid) { - dump_field(out, "uuid", it, 4); + dump_field(out, ESPHOME_PSTR("uuid"), it, 4); } - dump_field(out, "handle", this->handle); - dump_field(out, "short_uuid", this->short_uuid); + dump_field(out, ESPHOME_PSTR("handle"), this->handle); + dump_field(out, ESPHOME_PSTR("short_uuid"), this->short_uuid); return out.c_str(); } const char *BluetoothGATTCharacteristic::dump_to(DumpBuffer &out) const { - MessageDumpHelper helper(out, "BluetoothGATTCharacteristic"); + MessageDumpHelper helper(out, ESPHOME_PSTR("BluetoothGATTCharacteristic")); for (const auto &it : this->uuid) { - dump_field(out, "uuid", it, 4); + dump_field(out, ESPHOME_PSTR("uuid"), it, 4); } - dump_field(out, "handle", this->handle); - dump_field(out, "properties", this->properties); + dump_field(out, ESPHOME_PSTR("handle"), this->handle); + dump_field(out, ESPHOME_PSTR("properties"), this->properties); for (const auto &it : this->descriptors) { - out.append(" descriptors: "); + out.append(4, ' ').append_p(ESPHOME_PSTR("descriptors")).append(": "); it.dump_to(out); out.append("\n"); } - dump_field(out, "short_uuid", this->short_uuid); + dump_field(out, ESPHOME_PSTR("short_uuid"), this->short_uuid); return out.c_str(); } const char *BluetoothGATTService::dump_to(DumpBuffer &out) const { - MessageDumpHelper helper(out, "BluetoothGATTService"); + MessageDumpHelper helper(out, ESPHOME_PSTR("BluetoothGATTService")); for (const auto &it : this->uuid) { - dump_field(out, "uuid", it, 4); + dump_field(out, ESPHOME_PSTR("uuid"), it, 4); } - dump_field(out, "handle", this->handle); + dump_field(out, ESPHOME_PSTR("handle"), this->handle); for (const auto &it : this->characteristics) { - out.append(" characteristics: "); + out.append(4, ' ').append_p(ESPHOME_PSTR("characteristics")).append(": "); it.dump_to(out); out.append("\n"); } - dump_field(out, "short_uuid", this->short_uuid); + dump_field(out, ESPHOME_PSTR("short_uuid"), this->short_uuid); return out.c_str(); } const char *BluetoothGATTGetServicesResponse::dump_to(DumpBuffer &out) const { - MessageDumpHelper helper(out, "BluetoothGATTGetServicesResponse"); - dump_field(out, "address", this->address); + MessageDumpHelper helper(out, ESPHOME_PSTR("BluetoothGATTGetServicesResponse")); + dump_field(out, ESPHOME_PSTR("address"), this->address); for (const auto &it : this->services) { - out.append(" services: "); + out.append(4, ' ').append_p(ESPHOME_PSTR("services")).append(": "); it.dump_to(out); out.append("\n"); } return out.c_str(); } const char *BluetoothGATTGetServicesDoneResponse::dump_to(DumpBuffer &out) const { - MessageDumpHelper helper(out, "BluetoothGATTGetServicesDoneResponse"); - dump_field(out, "address", this->address); + MessageDumpHelper helper(out, ESPHOME_PSTR("BluetoothGATTGetServicesDoneResponse")); + dump_field(out, ESPHOME_PSTR("address"), this->address); return out.c_str(); } const char *BluetoothGATTReadRequest::dump_to(DumpBuffer &out) const { - MessageDumpHelper helper(out, "BluetoothGATTReadRequest"); - dump_field(out, "address", this->address); - dump_field(out, "handle", this->handle); + MessageDumpHelper helper(out, ESPHOME_PSTR("BluetoothGATTReadRequest")); + dump_field(out, ESPHOME_PSTR("address"), this->address); + dump_field(out, ESPHOME_PSTR("handle"), this->handle); return out.c_str(); } const char *BluetoothGATTReadResponse::dump_to(DumpBuffer &out) const { - MessageDumpHelper helper(out, "BluetoothGATTReadResponse"); - dump_field(out, "address", this->address); - dump_field(out, "handle", this->handle); - dump_bytes_field(out, "data", this->data_ptr_, this->data_len_); + MessageDumpHelper helper(out, ESPHOME_PSTR("BluetoothGATTReadResponse")); + dump_field(out, ESPHOME_PSTR("address"), this->address); + dump_field(out, ESPHOME_PSTR("handle"), this->handle); + dump_bytes_field(out, ESPHOME_PSTR("data"), this->data_ptr_, this->data_len_); return out.c_str(); } const char *BluetoothGATTWriteRequest::dump_to(DumpBuffer &out) const { - MessageDumpHelper helper(out, "BluetoothGATTWriteRequest"); - dump_field(out, "address", this->address); - dump_field(out, "handle", this->handle); - dump_field(out, "response", this->response); - dump_bytes_field(out, "data", this->data, this->data_len); + MessageDumpHelper helper(out, ESPHOME_PSTR("BluetoothGATTWriteRequest")); + dump_field(out, ESPHOME_PSTR("address"), this->address); + dump_field(out, ESPHOME_PSTR("handle"), this->handle); + dump_field(out, ESPHOME_PSTR("response"), this->response); + dump_bytes_field(out, ESPHOME_PSTR("data"), this->data, this->data_len); return out.c_str(); } const char *BluetoothGATTReadDescriptorRequest::dump_to(DumpBuffer &out) const { - MessageDumpHelper helper(out, "BluetoothGATTReadDescriptorRequest"); - dump_field(out, "address", this->address); - dump_field(out, "handle", this->handle); + MessageDumpHelper helper(out, ESPHOME_PSTR("BluetoothGATTReadDescriptorRequest")); + dump_field(out, ESPHOME_PSTR("address"), this->address); + dump_field(out, ESPHOME_PSTR("handle"), this->handle); return out.c_str(); } const char *BluetoothGATTWriteDescriptorRequest::dump_to(DumpBuffer &out) const { - MessageDumpHelper helper(out, "BluetoothGATTWriteDescriptorRequest"); - dump_field(out, "address", this->address); - dump_field(out, "handle", this->handle); - dump_bytes_field(out, "data", this->data, this->data_len); + MessageDumpHelper helper(out, ESPHOME_PSTR("BluetoothGATTWriteDescriptorRequest")); + dump_field(out, ESPHOME_PSTR("address"), this->address); + dump_field(out, ESPHOME_PSTR("handle"), this->handle); + dump_bytes_field(out, ESPHOME_PSTR("data"), this->data, this->data_len); return out.c_str(); } const char *BluetoothGATTNotifyRequest::dump_to(DumpBuffer &out) const { - MessageDumpHelper helper(out, "BluetoothGATTNotifyRequest"); - dump_field(out, "address", this->address); - dump_field(out, "handle", this->handle); - dump_field(out, "enable", this->enable); + MessageDumpHelper helper(out, ESPHOME_PSTR("BluetoothGATTNotifyRequest")); + dump_field(out, ESPHOME_PSTR("address"), this->address); + dump_field(out, ESPHOME_PSTR("handle"), this->handle); + dump_field(out, ESPHOME_PSTR("enable"), this->enable); return out.c_str(); } const char *BluetoothGATTNotifyDataResponse::dump_to(DumpBuffer &out) const { - MessageDumpHelper helper(out, "BluetoothGATTNotifyDataResponse"); - dump_field(out, "address", this->address); - dump_field(out, "handle", this->handle); - dump_bytes_field(out, "data", this->data_ptr_, this->data_len_); - return out.c_str(); -} -const char *SubscribeBluetoothConnectionsFreeRequest::dump_to(DumpBuffer &out) const { - out.append("SubscribeBluetoothConnectionsFreeRequest {}"); + MessageDumpHelper helper(out, ESPHOME_PSTR("BluetoothGATTNotifyDataResponse")); + dump_field(out, ESPHOME_PSTR("address"), this->address); + dump_field(out, ESPHOME_PSTR("handle"), this->handle); + dump_bytes_field(out, ESPHOME_PSTR("data"), this->data_ptr_, this->data_len_); return out.c_str(); } const char *BluetoothConnectionsFreeResponse::dump_to(DumpBuffer &out) const { - MessageDumpHelper helper(out, "BluetoothConnectionsFreeResponse"); - dump_field(out, "free", this->free); - dump_field(out, "limit", this->limit); + MessageDumpHelper helper(out, ESPHOME_PSTR("BluetoothConnectionsFreeResponse")); + dump_field(out, ESPHOME_PSTR("free"), this->free); + dump_field(out, ESPHOME_PSTR("limit"), this->limit); for (const auto &it : this->allocated) { - dump_field(out, "allocated", it, 4); + dump_field(out, ESPHOME_PSTR("allocated"), it, 4); } return out.c_str(); } const char *BluetoothGATTErrorResponse::dump_to(DumpBuffer &out) const { - MessageDumpHelper helper(out, "BluetoothGATTErrorResponse"); - dump_field(out, "address", this->address); - dump_field(out, "handle", this->handle); - dump_field(out, "error", this->error); + MessageDumpHelper helper(out, ESPHOME_PSTR("BluetoothGATTErrorResponse")); + dump_field(out, ESPHOME_PSTR("address"), this->address); + dump_field(out, ESPHOME_PSTR("handle"), this->handle); + dump_field(out, ESPHOME_PSTR("error"), this->error); return out.c_str(); } const char *BluetoothGATTWriteResponse::dump_to(DumpBuffer &out) const { - MessageDumpHelper helper(out, "BluetoothGATTWriteResponse"); - dump_field(out, "address", this->address); - dump_field(out, "handle", this->handle); + MessageDumpHelper helper(out, ESPHOME_PSTR("BluetoothGATTWriteResponse")); + dump_field(out, ESPHOME_PSTR("address"), this->address); + dump_field(out, ESPHOME_PSTR("handle"), this->handle); return out.c_str(); } const char *BluetoothGATTNotifyResponse::dump_to(DumpBuffer &out) const { - MessageDumpHelper helper(out, "BluetoothGATTNotifyResponse"); - dump_field(out, "address", this->address); - dump_field(out, "handle", this->handle); + MessageDumpHelper helper(out, ESPHOME_PSTR("BluetoothGATTNotifyResponse")); + dump_field(out, ESPHOME_PSTR("address"), this->address); + dump_field(out, ESPHOME_PSTR("handle"), this->handle); return out.c_str(); } const char *BluetoothDevicePairingResponse::dump_to(DumpBuffer &out) const { - MessageDumpHelper helper(out, "BluetoothDevicePairingResponse"); - dump_field(out, "address", this->address); - dump_field(out, "paired", this->paired); - dump_field(out, "error", this->error); + MessageDumpHelper helper(out, ESPHOME_PSTR("BluetoothDevicePairingResponse")); + dump_field(out, ESPHOME_PSTR("address"), this->address); + dump_field(out, ESPHOME_PSTR("paired"), this->paired); + dump_field(out, ESPHOME_PSTR("error"), this->error); return out.c_str(); } const char *BluetoothDeviceUnpairingResponse::dump_to(DumpBuffer &out) const { - MessageDumpHelper helper(out, "BluetoothDeviceUnpairingResponse"); - dump_field(out, "address", this->address); - dump_field(out, "success", this->success); - dump_field(out, "error", this->error); - return out.c_str(); -} -const char *UnsubscribeBluetoothLEAdvertisementsRequest::dump_to(DumpBuffer &out) const { - out.append("UnsubscribeBluetoothLEAdvertisementsRequest {}"); + MessageDumpHelper helper(out, ESPHOME_PSTR("BluetoothDeviceUnpairingResponse")); + dump_field(out, ESPHOME_PSTR("address"), this->address); + dump_field(out, ESPHOME_PSTR("success"), this->success); + dump_field(out, ESPHOME_PSTR("error"), this->error); return out.c_str(); } const char *BluetoothDeviceClearCacheResponse::dump_to(DumpBuffer &out) const { - MessageDumpHelper helper(out, "BluetoothDeviceClearCacheResponse"); - dump_field(out, "address", this->address); - dump_field(out, "success", this->success); - dump_field(out, "error", this->error); + MessageDumpHelper helper(out, ESPHOME_PSTR("BluetoothDeviceClearCacheResponse")); + dump_field(out, ESPHOME_PSTR("address"), this->address); + dump_field(out, ESPHOME_PSTR("success"), this->success); + dump_field(out, ESPHOME_PSTR("error"), this->error); return out.c_str(); } const char *BluetoothScannerStateResponse::dump_to(DumpBuffer &out) const { - MessageDumpHelper helper(out, "BluetoothScannerStateResponse"); - dump_field(out, "state", static_cast(this->state)); - dump_field(out, "mode", static_cast(this->mode)); - dump_field(out, "configured_mode", static_cast(this->configured_mode)); + MessageDumpHelper helper(out, ESPHOME_PSTR("BluetoothScannerStateResponse")); + dump_field(out, ESPHOME_PSTR("state"), static_cast(this->state)); + dump_field(out, ESPHOME_PSTR("mode"), static_cast(this->mode)); + dump_field(out, ESPHOME_PSTR("configured_mode"), static_cast(this->configured_mode)); return out.c_str(); } const char *BluetoothScannerSetModeRequest::dump_to(DumpBuffer &out) const { - MessageDumpHelper helper(out, "BluetoothScannerSetModeRequest"); - dump_field(out, "mode", static_cast(this->mode)); + MessageDumpHelper helper(out, ESPHOME_PSTR("BluetoothScannerSetModeRequest")); + dump_field(out, ESPHOME_PSTR("mode"), static_cast(this->mode)); return out.c_str(); } #endif #ifdef USE_VOICE_ASSISTANT const char *SubscribeVoiceAssistantRequest::dump_to(DumpBuffer &out) const { - MessageDumpHelper helper(out, "SubscribeVoiceAssistantRequest"); - dump_field(out, "subscribe", this->subscribe); - dump_field(out, "flags", this->flags); + MessageDumpHelper helper(out, ESPHOME_PSTR("SubscribeVoiceAssistantRequest")); + dump_field(out, ESPHOME_PSTR("subscribe"), this->subscribe); + dump_field(out, ESPHOME_PSTR("flags"), this->flags); return out.c_str(); } const char *VoiceAssistantAudioSettings::dump_to(DumpBuffer &out) const { - MessageDumpHelper helper(out, "VoiceAssistantAudioSettings"); - dump_field(out, "noise_suppression_level", this->noise_suppression_level); - dump_field(out, "auto_gain", this->auto_gain); - dump_field(out, "volume_multiplier", this->volume_multiplier); + MessageDumpHelper helper(out, ESPHOME_PSTR("VoiceAssistantAudioSettings")); + dump_field(out, ESPHOME_PSTR("noise_suppression_level"), this->noise_suppression_level); + dump_field(out, ESPHOME_PSTR("auto_gain"), this->auto_gain); + dump_field(out, ESPHOME_PSTR("volume_multiplier"), this->volume_multiplier); return out.c_str(); } const char *VoiceAssistantRequest::dump_to(DumpBuffer &out) const { - MessageDumpHelper helper(out, "VoiceAssistantRequest"); - dump_field(out, "start", this->start); - dump_field(out, "conversation_id", this->conversation_id); - dump_field(out, "flags", this->flags); - out.append(" audio_settings: "); + MessageDumpHelper helper(out, ESPHOME_PSTR("VoiceAssistantRequest")); + dump_field(out, ESPHOME_PSTR("start"), this->start); + dump_field(out, ESPHOME_PSTR("conversation_id"), this->conversation_id); + dump_field(out, ESPHOME_PSTR("flags"), this->flags); + out.append(2, ' ').append_p(ESPHOME_PSTR("audio_settings")).append(": "); this->audio_settings.dump_to(out); out.append("\n"); - dump_field(out, "wake_word_phrase", this->wake_word_phrase); + dump_field(out, ESPHOME_PSTR("wake_word_phrase"), this->wake_word_phrase); return out.c_str(); } const char *VoiceAssistantResponse::dump_to(DumpBuffer &out) const { - MessageDumpHelper helper(out, "VoiceAssistantResponse"); - dump_field(out, "port", this->port); - dump_field(out, "error", this->error); + MessageDumpHelper helper(out, ESPHOME_PSTR("VoiceAssistantResponse")); + dump_field(out, ESPHOME_PSTR("port"), this->port); + dump_field(out, ESPHOME_PSTR("error"), this->error); return out.c_str(); } const char *VoiceAssistantEventData::dump_to(DumpBuffer &out) const { - MessageDumpHelper helper(out, "VoiceAssistantEventData"); - dump_field(out, "name", this->name); - dump_field(out, "value", this->value); + MessageDumpHelper helper(out, ESPHOME_PSTR("VoiceAssistantEventData")); + dump_field(out, ESPHOME_PSTR("name"), this->name); + dump_field(out, ESPHOME_PSTR("value"), this->value); return out.c_str(); } const char *VoiceAssistantEventResponse::dump_to(DumpBuffer &out) const { - MessageDumpHelper helper(out, "VoiceAssistantEventResponse"); - dump_field(out, "event_type", static_cast(this->event_type)); + MessageDumpHelper helper(out, ESPHOME_PSTR("VoiceAssistantEventResponse")); + dump_field(out, ESPHOME_PSTR("event_type"), static_cast(this->event_type)); for (const auto &it : this->data) { - out.append(" data: "); + out.append(4, ' ').append_p(ESPHOME_PSTR("data")).append(": "); it.dump_to(out); out.append("\n"); } return out.c_str(); } const char *VoiceAssistantAudio::dump_to(DumpBuffer &out) const { - MessageDumpHelper helper(out, "VoiceAssistantAudio"); - dump_bytes_field(out, "data", this->data, this->data_len); - dump_field(out, "end", this->end); + MessageDumpHelper helper(out, ESPHOME_PSTR("VoiceAssistantAudio")); + dump_bytes_field(out, ESPHOME_PSTR("data"), this->data, this->data_len); + dump_field(out, ESPHOME_PSTR("end"), this->end); return out.c_str(); } const char *VoiceAssistantTimerEventResponse::dump_to(DumpBuffer &out) const { - MessageDumpHelper helper(out, "VoiceAssistantTimerEventResponse"); - dump_field(out, "event_type", static_cast(this->event_type)); - dump_field(out, "timer_id", this->timer_id); - dump_field(out, "name", this->name); - dump_field(out, "total_seconds", this->total_seconds); - dump_field(out, "seconds_left", this->seconds_left); - dump_field(out, "is_active", this->is_active); + MessageDumpHelper helper(out, ESPHOME_PSTR("VoiceAssistantTimerEventResponse")); + dump_field(out, ESPHOME_PSTR("event_type"), static_cast(this->event_type)); + dump_field(out, ESPHOME_PSTR("timer_id"), this->timer_id); + dump_field(out, ESPHOME_PSTR("name"), this->name); + dump_field(out, ESPHOME_PSTR("total_seconds"), this->total_seconds); + dump_field(out, ESPHOME_PSTR("seconds_left"), this->seconds_left); + dump_field(out, ESPHOME_PSTR("is_active"), this->is_active); return out.c_str(); } const char *VoiceAssistantAnnounceRequest::dump_to(DumpBuffer &out) const { - MessageDumpHelper helper(out, "VoiceAssistantAnnounceRequest"); - dump_field(out, "media_id", this->media_id); - dump_field(out, "text", this->text); - dump_field(out, "preannounce_media_id", this->preannounce_media_id); - dump_field(out, "start_conversation", this->start_conversation); + MessageDumpHelper helper(out, ESPHOME_PSTR("VoiceAssistantAnnounceRequest")); + dump_field(out, ESPHOME_PSTR("media_id"), this->media_id); + dump_field(out, ESPHOME_PSTR("text"), this->text); + dump_field(out, ESPHOME_PSTR("preannounce_media_id"), this->preannounce_media_id); + dump_field(out, ESPHOME_PSTR("start_conversation"), this->start_conversation); return out.c_str(); } const char *VoiceAssistantAnnounceFinished::dump_to(DumpBuffer &out) const { - MessageDumpHelper helper(out, "VoiceAssistantAnnounceFinished"); - dump_field(out, "success", this->success); + MessageDumpHelper helper(out, ESPHOME_PSTR("VoiceAssistantAnnounceFinished")); + dump_field(out, ESPHOME_PSTR("success"), this->success); return out.c_str(); } const char *VoiceAssistantWakeWord::dump_to(DumpBuffer &out) const { - MessageDumpHelper helper(out, "VoiceAssistantWakeWord"); - dump_field(out, "id", this->id); - dump_field(out, "wake_word", this->wake_word); + MessageDumpHelper helper(out, ESPHOME_PSTR("VoiceAssistantWakeWord")); + dump_field(out, ESPHOME_PSTR("id"), this->id); + dump_field(out, ESPHOME_PSTR("wake_word"), this->wake_word); for (const auto &it : this->trained_languages) { - dump_field(out, "trained_languages", it, 4); + dump_field(out, ESPHOME_PSTR("trained_languages"), it, 4); } return out.c_str(); } const char *VoiceAssistantExternalWakeWord::dump_to(DumpBuffer &out) const { - MessageDumpHelper helper(out, "VoiceAssistantExternalWakeWord"); - dump_field(out, "id", this->id); - dump_field(out, "wake_word", this->wake_word); + MessageDumpHelper helper(out, ESPHOME_PSTR("VoiceAssistantExternalWakeWord")); + dump_field(out, ESPHOME_PSTR("id"), this->id); + dump_field(out, ESPHOME_PSTR("wake_word"), this->wake_word); for (const auto &it : this->trained_languages) { - dump_field(out, "trained_languages", it, 4); + dump_field(out, ESPHOME_PSTR("trained_languages"), it, 4); } - dump_field(out, "model_type", this->model_type); - dump_field(out, "model_size", this->model_size); - dump_field(out, "model_hash", this->model_hash); - dump_field(out, "url", this->url); + dump_field(out, ESPHOME_PSTR("model_type"), this->model_type); + dump_field(out, ESPHOME_PSTR("model_size"), this->model_size); + dump_field(out, ESPHOME_PSTR("model_hash"), this->model_hash); + dump_field(out, ESPHOME_PSTR("url"), this->url); return out.c_str(); } const char *VoiceAssistantConfigurationRequest::dump_to(DumpBuffer &out) const { - MessageDumpHelper helper(out, "VoiceAssistantConfigurationRequest"); + MessageDumpHelper helper(out, ESPHOME_PSTR("VoiceAssistantConfigurationRequest")); for (const auto &it : this->external_wake_words) { - out.append(" external_wake_words: "); + out.append(4, ' ').append_p(ESPHOME_PSTR("external_wake_words")).append(": "); it.dump_to(out); out.append("\n"); } return out.c_str(); } const char *VoiceAssistantConfigurationResponse::dump_to(DumpBuffer &out) const { - MessageDumpHelper helper(out, "VoiceAssistantConfigurationResponse"); + MessageDumpHelper helper(out, ESPHOME_PSTR("VoiceAssistantConfigurationResponse")); for (const auto &it : this->available_wake_words) { - out.append(" available_wake_words: "); + out.append(4, ' ').append_p(ESPHOME_PSTR("available_wake_words")).append(": "); it.dump_to(out); out.append("\n"); } for (const auto &it : *this->active_wake_words) { - dump_field(out, "active_wake_words", it, 4); + dump_field(out, ESPHOME_PSTR("active_wake_words"), it, 4); } - dump_field(out, "max_active_wake_words", this->max_active_wake_words); + dump_field(out, ESPHOME_PSTR("max_active_wake_words"), this->max_active_wake_words); return out.c_str(); } const char *VoiceAssistantSetConfiguration::dump_to(DumpBuffer &out) const { - MessageDumpHelper helper(out, "VoiceAssistantSetConfiguration"); + MessageDumpHelper helper(out, ESPHOME_PSTR("VoiceAssistantSetConfiguration")); for (const auto &it : this->active_wake_words) { - dump_field(out, "active_wake_words", it, 4); + dump_field(out, ESPHOME_PSTR("active_wake_words"), it, 4); } return out.c_str(); } #endif #ifdef USE_ALARM_CONTROL_PANEL const char *ListEntitiesAlarmControlPanelResponse::dump_to(DumpBuffer &out) const { - MessageDumpHelper helper(out, "ListEntitiesAlarmControlPanelResponse"); - dump_field(out, "object_id", this->object_id); - dump_field(out, "key", this->key); - dump_field(out, "name", this->name); + MessageDumpHelper helper(out, ESPHOME_PSTR("ListEntitiesAlarmControlPanelResponse")); + dump_field(out, ESPHOME_PSTR("object_id"), this->object_id); + dump_field(out, ESPHOME_PSTR("key"), this->key); + dump_field(out, ESPHOME_PSTR("name"), this->name); #ifdef USE_ENTITY_ICON - dump_field(out, "icon", this->icon); + dump_field(out, ESPHOME_PSTR("icon"), this->icon); #endif - dump_field(out, "disabled_by_default", this->disabled_by_default); - dump_field(out, "entity_category", static_cast(this->entity_category)); - dump_field(out, "supported_features", this->supported_features); - dump_field(out, "requires_code", this->requires_code); - dump_field(out, "requires_code_to_arm", this->requires_code_to_arm); + dump_field(out, ESPHOME_PSTR("disabled_by_default"), this->disabled_by_default); + dump_field(out, ESPHOME_PSTR("entity_category"), static_cast(this->entity_category)); + dump_field(out, ESPHOME_PSTR("supported_features"), this->supported_features); + dump_field(out, ESPHOME_PSTR("requires_code"), this->requires_code); + dump_field(out, ESPHOME_PSTR("requires_code_to_arm"), this->requires_code_to_arm); #ifdef USE_DEVICES - dump_field(out, "device_id", this->device_id); + dump_field(out, ESPHOME_PSTR("device_id"), this->device_id); #endif return out.c_str(); } const char *AlarmControlPanelStateResponse::dump_to(DumpBuffer &out) const { - MessageDumpHelper helper(out, "AlarmControlPanelStateResponse"); - dump_field(out, "key", this->key); - dump_field(out, "state", static_cast(this->state)); + MessageDumpHelper helper(out, ESPHOME_PSTR("AlarmControlPanelStateResponse")); + dump_field(out, ESPHOME_PSTR("key"), this->key); + dump_field(out, ESPHOME_PSTR("state"), static_cast(this->state)); #ifdef USE_DEVICES - dump_field(out, "device_id", this->device_id); + dump_field(out, ESPHOME_PSTR("device_id"), this->device_id); #endif return out.c_str(); } const char *AlarmControlPanelCommandRequest::dump_to(DumpBuffer &out) const { - MessageDumpHelper helper(out, "AlarmControlPanelCommandRequest"); - dump_field(out, "key", this->key); - dump_field(out, "command", static_cast(this->command)); - dump_field(out, "code", this->code); + MessageDumpHelper helper(out, ESPHOME_PSTR("AlarmControlPanelCommandRequest")); + dump_field(out, ESPHOME_PSTR("key"), this->key); + dump_field(out, ESPHOME_PSTR("command"), static_cast(this->command)); + dump_field(out, ESPHOME_PSTR("code"), this->code); #ifdef USE_DEVICES - dump_field(out, "device_id", this->device_id); + dump_field(out, ESPHOME_PSTR("device_id"), this->device_id); #endif return out.c_str(); } #endif #ifdef USE_TEXT const char *ListEntitiesTextResponse::dump_to(DumpBuffer &out) const { - MessageDumpHelper helper(out, "ListEntitiesTextResponse"); - dump_field(out, "object_id", this->object_id); - dump_field(out, "key", this->key); - dump_field(out, "name", this->name); + MessageDumpHelper helper(out, ESPHOME_PSTR("ListEntitiesTextResponse")); + dump_field(out, ESPHOME_PSTR("object_id"), this->object_id); + dump_field(out, ESPHOME_PSTR("key"), this->key); + dump_field(out, ESPHOME_PSTR("name"), this->name); #ifdef USE_ENTITY_ICON - dump_field(out, "icon", this->icon); + dump_field(out, ESPHOME_PSTR("icon"), this->icon); #endif - dump_field(out, "disabled_by_default", this->disabled_by_default); - dump_field(out, "entity_category", static_cast(this->entity_category)); - dump_field(out, "min_length", this->min_length); - dump_field(out, "max_length", this->max_length); - dump_field(out, "pattern", this->pattern); - dump_field(out, "mode", static_cast(this->mode)); + dump_field(out, ESPHOME_PSTR("disabled_by_default"), this->disabled_by_default); + dump_field(out, ESPHOME_PSTR("entity_category"), static_cast(this->entity_category)); + dump_field(out, ESPHOME_PSTR("min_length"), this->min_length); + dump_field(out, ESPHOME_PSTR("max_length"), this->max_length); + dump_field(out, ESPHOME_PSTR("pattern"), this->pattern); + dump_field(out, ESPHOME_PSTR("mode"), static_cast(this->mode)); #ifdef USE_DEVICES - dump_field(out, "device_id", this->device_id); + dump_field(out, ESPHOME_PSTR("device_id"), this->device_id); #endif return out.c_str(); } const char *TextStateResponse::dump_to(DumpBuffer &out) const { - MessageDumpHelper helper(out, "TextStateResponse"); - dump_field(out, "key", this->key); - dump_field(out, "state", this->state); - dump_field(out, "missing_state", this->missing_state); + MessageDumpHelper helper(out, ESPHOME_PSTR("TextStateResponse")); + dump_field(out, ESPHOME_PSTR("key"), this->key); + dump_field(out, ESPHOME_PSTR("state"), this->state); + dump_field(out, ESPHOME_PSTR("missing_state"), this->missing_state); #ifdef USE_DEVICES - dump_field(out, "device_id", this->device_id); + dump_field(out, ESPHOME_PSTR("device_id"), this->device_id); #endif return out.c_str(); } const char *TextCommandRequest::dump_to(DumpBuffer &out) const { - MessageDumpHelper helper(out, "TextCommandRequest"); - dump_field(out, "key", this->key); - dump_field(out, "state", this->state); + MessageDumpHelper helper(out, ESPHOME_PSTR("TextCommandRequest")); + dump_field(out, ESPHOME_PSTR("key"), this->key); + dump_field(out, ESPHOME_PSTR("state"), this->state); #ifdef USE_DEVICES - dump_field(out, "device_id", this->device_id); + dump_field(out, ESPHOME_PSTR("device_id"), this->device_id); #endif return out.c_str(); } #endif #ifdef USE_DATETIME_DATE const char *ListEntitiesDateResponse::dump_to(DumpBuffer &out) const { - MessageDumpHelper helper(out, "ListEntitiesDateResponse"); - dump_field(out, "object_id", this->object_id); - dump_field(out, "key", this->key); - dump_field(out, "name", this->name); + MessageDumpHelper helper(out, ESPHOME_PSTR("ListEntitiesDateResponse")); + dump_field(out, ESPHOME_PSTR("object_id"), this->object_id); + dump_field(out, ESPHOME_PSTR("key"), this->key); + dump_field(out, ESPHOME_PSTR("name"), this->name); #ifdef USE_ENTITY_ICON - dump_field(out, "icon", this->icon); + dump_field(out, ESPHOME_PSTR("icon"), this->icon); #endif - dump_field(out, "disabled_by_default", this->disabled_by_default); - dump_field(out, "entity_category", static_cast(this->entity_category)); + dump_field(out, ESPHOME_PSTR("disabled_by_default"), this->disabled_by_default); + dump_field(out, ESPHOME_PSTR("entity_category"), static_cast(this->entity_category)); #ifdef USE_DEVICES - dump_field(out, "device_id", this->device_id); + dump_field(out, ESPHOME_PSTR("device_id"), this->device_id); #endif return out.c_str(); } const char *DateStateResponse::dump_to(DumpBuffer &out) const { - MessageDumpHelper helper(out, "DateStateResponse"); - dump_field(out, "key", this->key); - dump_field(out, "missing_state", this->missing_state); - dump_field(out, "year", this->year); - dump_field(out, "month", this->month); - dump_field(out, "day", this->day); + MessageDumpHelper helper(out, ESPHOME_PSTR("DateStateResponse")); + dump_field(out, ESPHOME_PSTR("key"), this->key); + dump_field(out, ESPHOME_PSTR("missing_state"), this->missing_state); + dump_field(out, ESPHOME_PSTR("year"), this->year); + dump_field(out, ESPHOME_PSTR("month"), this->month); + dump_field(out, ESPHOME_PSTR("day"), this->day); #ifdef USE_DEVICES - dump_field(out, "device_id", this->device_id); + dump_field(out, ESPHOME_PSTR("device_id"), this->device_id); #endif return out.c_str(); } const char *DateCommandRequest::dump_to(DumpBuffer &out) const { - MessageDumpHelper helper(out, "DateCommandRequest"); - dump_field(out, "key", this->key); - dump_field(out, "year", this->year); - dump_field(out, "month", this->month); - dump_field(out, "day", this->day); + MessageDumpHelper helper(out, ESPHOME_PSTR("DateCommandRequest")); + dump_field(out, ESPHOME_PSTR("key"), this->key); + dump_field(out, ESPHOME_PSTR("year"), this->year); + dump_field(out, ESPHOME_PSTR("month"), this->month); + dump_field(out, ESPHOME_PSTR("day"), this->day); #ifdef USE_DEVICES - dump_field(out, "device_id", this->device_id); + dump_field(out, ESPHOME_PSTR("device_id"), this->device_id); #endif return out.c_str(); } #endif #ifdef USE_DATETIME_TIME const char *ListEntitiesTimeResponse::dump_to(DumpBuffer &out) const { - MessageDumpHelper helper(out, "ListEntitiesTimeResponse"); - dump_field(out, "object_id", this->object_id); - dump_field(out, "key", this->key); - dump_field(out, "name", this->name); + MessageDumpHelper helper(out, ESPHOME_PSTR("ListEntitiesTimeResponse")); + dump_field(out, ESPHOME_PSTR("object_id"), this->object_id); + dump_field(out, ESPHOME_PSTR("key"), this->key); + dump_field(out, ESPHOME_PSTR("name"), this->name); #ifdef USE_ENTITY_ICON - dump_field(out, "icon", this->icon); + dump_field(out, ESPHOME_PSTR("icon"), this->icon); #endif - dump_field(out, "disabled_by_default", this->disabled_by_default); - dump_field(out, "entity_category", static_cast(this->entity_category)); + dump_field(out, ESPHOME_PSTR("disabled_by_default"), this->disabled_by_default); + dump_field(out, ESPHOME_PSTR("entity_category"), static_cast(this->entity_category)); #ifdef USE_DEVICES - dump_field(out, "device_id", this->device_id); + dump_field(out, ESPHOME_PSTR("device_id"), this->device_id); #endif return out.c_str(); } const char *TimeStateResponse::dump_to(DumpBuffer &out) const { - MessageDumpHelper helper(out, "TimeStateResponse"); - dump_field(out, "key", this->key); - dump_field(out, "missing_state", this->missing_state); - dump_field(out, "hour", this->hour); - dump_field(out, "minute", this->minute); - dump_field(out, "second", this->second); + MessageDumpHelper helper(out, ESPHOME_PSTR("TimeStateResponse")); + dump_field(out, ESPHOME_PSTR("key"), this->key); + dump_field(out, ESPHOME_PSTR("missing_state"), this->missing_state); + dump_field(out, ESPHOME_PSTR("hour"), this->hour); + dump_field(out, ESPHOME_PSTR("minute"), this->minute); + dump_field(out, ESPHOME_PSTR("second"), this->second); #ifdef USE_DEVICES - dump_field(out, "device_id", this->device_id); + dump_field(out, ESPHOME_PSTR("device_id"), this->device_id); #endif return out.c_str(); } const char *TimeCommandRequest::dump_to(DumpBuffer &out) const { - MessageDumpHelper helper(out, "TimeCommandRequest"); - dump_field(out, "key", this->key); - dump_field(out, "hour", this->hour); - dump_field(out, "minute", this->minute); - dump_field(out, "second", this->second); + MessageDumpHelper helper(out, ESPHOME_PSTR("TimeCommandRequest")); + dump_field(out, ESPHOME_PSTR("key"), this->key); + dump_field(out, ESPHOME_PSTR("hour"), this->hour); + dump_field(out, ESPHOME_PSTR("minute"), this->minute); + dump_field(out, ESPHOME_PSTR("second"), this->second); #ifdef USE_DEVICES - dump_field(out, "device_id", this->device_id); + dump_field(out, ESPHOME_PSTR("device_id"), this->device_id); #endif return out.c_str(); } #endif #ifdef USE_EVENT const char *ListEntitiesEventResponse::dump_to(DumpBuffer &out) const { - MessageDumpHelper helper(out, "ListEntitiesEventResponse"); - dump_field(out, "object_id", this->object_id); - dump_field(out, "key", this->key); - dump_field(out, "name", this->name); + MessageDumpHelper helper(out, ESPHOME_PSTR("ListEntitiesEventResponse")); + dump_field(out, ESPHOME_PSTR("object_id"), this->object_id); + dump_field(out, ESPHOME_PSTR("key"), this->key); + dump_field(out, ESPHOME_PSTR("name"), this->name); #ifdef USE_ENTITY_ICON - dump_field(out, "icon", this->icon); + dump_field(out, ESPHOME_PSTR("icon"), this->icon); #endif - dump_field(out, "disabled_by_default", this->disabled_by_default); - dump_field(out, "entity_category", static_cast(this->entity_category)); - dump_field(out, "device_class", this->device_class); + dump_field(out, ESPHOME_PSTR("disabled_by_default"), this->disabled_by_default); + dump_field(out, ESPHOME_PSTR("entity_category"), static_cast(this->entity_category)); + dump_field(out, ESPHOME_PSTR("device_class"), this->device_class); for (const auto &it : *this->event_types) { - dump_field(out, "event_types", it, 4); + dump_field(out, ESPHOME_PSTR("event_types"), it, 4); } #ifdef USE_DEVICES - dump_field(out, "device_id", this->device_id); + dump_field(out, ESPHOME_PSTR("device_id"), this->device_id); #endif return out.c_str(); } const char *EventResponse::dump_to(DumpBuffer &out) const { - MessageDumpHelper helper(out, "EventResponse"); - dump_field(out, "key", this->key); - dump_field(out, "event_type", this->event_type); + MessageDumpHelper helper(out, ESPHOME_PSTR("EventResponse")); + dump_field(out, ESPHOME_PSTR("key"), this->key); + dump_field(out, ESPHOME_PSTR("event_type"), this->event_type); #ifdef USE_DEVICES - dump_field(out, "device_id", this->device_id); + dump_field(out, ESPHOME_PSTR("device_id"), this->device_id); #endif return out.c_str(); } #endif #ifdef USE_VALVE const char *ListEntitiesValveResponse::dump_to(DumpBuffer &out) const { - MessageDumpHelper helper(out, "ListEntitiesValveResponse"); - dump_field(out, "object_id", this->object_id); - dump_field(out, "key", this->key); - dump_field(out, "name", this->name); + MessageDumpHelper helper(out, ESPHOME_PSTR("ListEntitiesValveResponse")); + dump_field(out, ESPHOME_PSTR("object_id"), this->object_id); + dump_field(out, ESPHOME_PSTR("key"), this->key); + dump_field(out, ESPHOME_PSTR("name"), this->name); #ifdef USE_ENTITY_ICON - dump_field(out, "icon", this->icon); + dump_field(out, ESPHOME_PSTR("icon"), this->icon); #endif - dump_field(out, "disabled_by_default", this->disabled_by_default); - dump_field(out, "entity_category", static_cast(this->entity_category)); - dump_field(out, "device_class", this->device_class); - dump_field(out, "assumed_state", this->assumed_state); - dump_field(out, "supports_position", this->supports_position); - dump_field(out, "supports_stop", this->supports_stop); + dump_field(out, ESPHOME_PSTR("disabled_by_default"), this->disabled_by_default); + dump_field(out, ESPHOME_PSTR("entity_category"), static_cast(this->entity_category)); + dump_field(out, ESPHOME_PSTR("device_class"), this->device_class); + dump_field(out, ESPHOME_PSTR("assumed_state"), this->assumed_state); + dump_field(out, ESPHOME_PSTR("supports_position"), this->supports_position); + dump_field(out, ESPHOME_PSTR("supports_stop"), this->supports_stop); #ifdef USE_DEVICES - dump_field(out, "device_id", this->device_id); + dump_field(out, ESPHOME_PSTR("device_id"), this->device_id); #endif return out.c_str(); } const char *ValveStateResponse::dump_to(DumpBuffer &out) const { - MessageDumpHelper helper(out, "ValveStateResponse"); - dump_field(out, "key", this->key); - dump_field(out, "position", this->position); - dump_field(out, "current_operation", static_cast(this->current_operation)); + MessageDumpHelper helper(out, ESPHOME_PSTR("ValveStateResponse")); + dump_field(out, ESPHOME_PSTR("key"), this->key); + dump_field(out, ESPHOME_PSTR("position"), this->position); + dump_field(out, ESPHOME_PSTR("current_operation"), static_cast(this->current_operation)); #ifdef USE_DEVICES - dump_field(out, "device_id", this->device_id); + dump_field(out, ESPHOME_PSTR("device_id"), this->device_id); #endif return out.c_str(); } const char *ValveCommandRequest::dump_to(DumpBuffer &out) const { - MessageDumpHelper helper(out, "ValveCommandRequest"); - dump_field(out, "key", this->key); - dump_field(out, "has_position", this->has_position); - dump_field(out, "position", this->position); - dump_field(out, "stop", this->stop); + MessageDumpHelper helper(out, ESPHOME_PSTR("ValveCommandRequest")); + dump_field(out, ESPHOME_PSTR("key"), this->key); + dump_field(out, ESPHOME_PSTR("has_position"), this->has_position); + dump_field(out, ESPHOME_PSTR("position"), this->position); + dump_field(out, ESPHOME_PSTR("stop"), this->stop); #ifdef USE_DEVICES - dump_field(out, "device_id", this->device_id); + dump_field(out, ESPHOME_PSTR("device_id"), this->device_id); #endif return out.c_str(); } #endif #ifdef USE_DATETIME_DATETIME const char *ListEntitiesDateTimeResponse::dump_to(DumpBuffer &out) const { - MessageDumpHelper helper(out, "ListEntitiesDateTimeResponse"); - dump_field(out, "object_id", this->object_id); - dump_field(out, "key", this->key); - dump_field(out, "name", this->name); + MessageDumpHelper helper(out, ESPHOME_PSTR("ListEntitiesDateTimeResponse")); + dump_field(out, ESPHOME_PSTR("object_id"), this->object_id); + dump_field(out, ESPHOME_PSTR("key"), this->key); + dump_field(out, ESPHOME_PSTR("name"), this->name); #ifdef USE_ENTITY_ICON - dump_field(out, "icon", this->icon); + dump_field(out, ESPHOME_PSTR("icon"), this->icon); #endif - dump_field(out, "disabled_by_default", this->disabled_by_default); - dump_field(out, "entity_category", static_cast(this->entity_category)); + dump_field(out, ESPHOME_PSTR("disabled_by_default"), this->disabled_by_default); + dump_field(out, ESPHOME_PSTR("entity_category"), static_cast(this->entity_category)); #ifdef USE_DEVICES - dump_field(out, "device_id", this->device_id); + dump_field(out, ESPHOME_PSTR("device_id"), this->device_id); #endif return out.c_str(); } const char *DateTimeStateResponse::dump_to(DumpBuffer &out) const { - MessageDumpHelper helper(out, "DateTimeStateResponse"); - dump_field(out, "key", this->key); - dump_field(out, "missing_state", this->missing_state); - dump_field(out, "epoch_seconds", this->epoch_seconds); + MessageDumpHelper helper(out, ESPHOME_PSTR("DateTimeStateResponse")); + dump_field(out, ESPHOME_PSTR("key"), this->key); + dump_field(out, ESPHOME_PSTR("missing_state"), this->missing_state); + dump_field(out, ESPHOME_PSTR("epoch_seconds"), this->epoch_seconds); #ifdef USE_DEVICES - dump_field(out, "device_id", this->device_id); + dump_field(out, ESPHOME_PSTR("device_id"), this->device_id); #endif return out.c_str(); } const char *DateTimeCommandRequest::dump_to(DumpBuffer &out) const { - MessageDumpHelper helper(out, "DateTimeCommandRequest"); - dump_field(out, "key", this->key); - dump_field(out, "epoch_seconds", this->epoch_seconds); + MessageDumpHelper helper(out, ESPHOME_PSTR("DateTimeCommandRequest")); + dump_field(out, ESPHOME_PSTR("key"), this->key); + dump_field(out, ESPHOME_PSTR("epoch_seconds"), this->epoch_seconds); #ifdef USE_DEVICES - dump_field(out, "device_id", this->device_id); + dump_field(out, ESPHOME_PSTR("device_id"), this->device_id); #endif return out.c_str(); } #endif #ifdef USE_UPDATE const char *ListEntitiesUpdateResponse::dump_to(DumpBuffer &out) const { - MessageDumpHelper helper(out, "ListEntitiesUpdateResponse"); - dump_field(out, "object_id", this->object_id); - dump_field(out, "key", this->key); - dump_field(out, "name", this->name); + MessageDumpHelper helper(out, ESPHOME_PSTR("ListEntitiesUpdateResponse")); + dump_field(out, ESPHOME_PSTR("object_id"), this->object_id); + dump_field(out, ESPHOME_PSTR("key"), this->key); + dump_field(out, ESPHOME_PSTR("name"), this->name); #ifdef USE_ENTITY_ICON - dump_field(out, "icon", this->icon); + dump_field(out, ESPHOME_PSTR("icon"), this->icon); #endif - dump_field(out, "disabled_by_default", this->disabled_by_default); - dump_field(out, "entity_category", static_cast(this->entity_category)); - dump_field(out, "device_class", this->device_class); + dump_field(out, ESPHOME_PSTR("disabled_by_default"), this->disabled_by_default); + dump_field(out, ESPHOME_PSTR("entity_category"), static_cast(this->entity_category)); + dump_field(out, ESPHOME_PSTR("device_class"), this->device_class); #ifdef USE_DEVICES - dump_field(out, "device_id", this->device_id); + dump_field(out, ESPHOME_PSTR("device_id"), this->device_id); #endif return out.c_str(); } const char *UpdateStateResponse::dump_to(DumpBuffer &out) const { - MessageDumpHelper helper(out, "UpdateStateResponse"); - dump_field(out, "key", this->key); - dump_field(out, "missing_state", this->missing_state); - dump_field(out, "in_progress", this->in_progress); - dump_field(out, "has_progress", this->has_progress); - dump_field(out, "progress", this->progress); - dump_field(out, "current_version", this->current_version); - dump_field(out, "latest_version", this->latest_version); - dump_field(out, "title", this->title); - dump_field(out, "release_summary", this->release_summary); - dump_field(out, "release_url", this->release_url); + MessageDumpHelper helper(out, ESPHOME_PSTR("UpdateStateResponse")); + dump_field(out, ESPHOME_PSTR("key"), this->key); + dump_field(out, ESPHOME_PSTR("missing_state"), this->missing_state); + dump_field(out, ESPHOME_PSTR("in_progress"), this->in_progress); + dump_field(out, ESPHOME_PSTR("has_progress"), this->has_progress); + dump_field(out, ESPHOME_PSTR("progress"), this->progress); + dump_field(out, ESPHOME_PSTR("current_version"), this->current_version); + dump_field(out, ESPHOME_PSTR("latest_version"), this->latest_version); + dump_field(out, ESPHOME_PSTR("title"), this->title); + dump_field(out, ESPHOME_PSTR("release_summary"), this->release_summary); + dump_field(out, ESPHOME_PSTR("release_url"), this->release_url); #ifdef USE_DEVICES - dump_field(out, "device_id", this->device_id); + dump_field(out, ESPHOME_PSTR("device_id"), this->device_id); #endif return out.c_str(); } const char *UpdateCommandRequest::dump_to(DumpBuffer &out) const { - MessageDumpHelper helper(out, "UpdateCommandRequest"); - dump_field(out, "key", this->key); - dump_field(out, "command", static_cast(this->command)); + MessageDumpHelper helper(out, ESPHOME_PSTR("UpdateCommandRequest")); + dump_field(out, ESPHOME_PSTR("key"), this->key); + dump_field(out, ESPHOME_PSTR("command"), static_cast(this->command)); #ifdef USE_DEVICES - dump_field(out, "device_id", this->device_id); + dump_field(out, ESPHOME_PSTR("device_id"), this->device_id); #endif return out.c_str(); } #endif #ifdef USE_ZWAVE_PROXY const char *ZWaveProxyFrame::dump_to(DumpBuffer &out) const { - MessageDumpHelper helper(out, "ZWaveProxyFrame"); - dump_bytes_field(out, "data", this->data, this->data_len); + MessageDumpHelper helper(out, ESPHOME_PSTR("ZWaveProxyFrame")); + dump_bytes_field(out, ESPHOME_PSTR("data"), this->data, this->data_len); return out.c_str(); } const char *ZWaveProxyRequest::dump_to(DumpBuffer &out) const { - MessageDumpHelper helper(out, "ZWaveProxyRequest"); - dump_field(out, "type", static_cast(this->type)); - dump_bytes_field(out, "data", this->data, this->data_len); + MessageDumpHelper helper(out, ESPHOME_PSTR("ZWaveProxyRequest")); + dump_field(out, ESPHOME_PSTR("type"), static_cast(this->type)); + dump_bytes_field(out, ESPHOME_PSTR("data"), this->data, this->data_len); return out.c_str(); } #endif #ifdef USE_INFRARED const char *ListEntitiesInfraredResponse::dump_to(DumpBuffer &out) const { - MessageDumpHelper helper(out, "ListEntitiesInfraredResponse"); - dump_field(out, "object_id", this->object_id); - dump_field(out, "key", this->key); - dump_field(out, "name", this->name); + MessageDumpHelper helper(out, ESPHOME_PSTR("ListEntitiesInfraredResponse")); + dump_field(out, ESPHOME_PSTR("object_id"), this->object_id); + dump_field(out, ESPHOME_PSTR("key"), this->key); + dump_field(out, ESPHOME_PSTR("name"), this->name); #ifdef USE_ENTITY_ICON - dump_field(out, "icon", this->icon); + dump_field(out, ESPHOME_PSTR("icon"), this->icon); #endif - dump_field(out, "disabled_by_default", this->disabled_by_default); - dump_field(out, "entity_category", static_cast(this->entity_category)); + dump_field(out, ESPHOME_PSTR("disabled_by_default"), this->disabled_by_default); + dump_field(out, ESPHOME_PSTR("entity_category"), static_cast(this->entity_category)); #ifdef USE_DEVICES - dump_field(out, "device_id", this->device_id); + dump_field(out, ESPHOME_PSTR("device_id"), this->device_id); #endif - dump_field(out, "capabilities", this->capabilities); + dump_field(out, ESPHOME_PSTR("capabilities"), this->capabilities); + dump_field(out, ESPHOME_PSTR("receiver_frequency"), this->receiver_frequency); return out.c_str(); } #endif #ifdef USE_IR_RF const char *InfraredRFTransmitRawTimingsRequest::dump_to(DumpBuffer &out) const { - MessageDumpHelper helper(out, "InfraredRFTransmitRawTimingsRequest"); + MessageDumpHelper helper(out, ESPHOME_PSTR("InfraredRFTransmitRawTimingsRequest")); #ifdef USE_DEVICES - dump_field(out, "device_id", this->device_id); + dump_field(out, ESPHOME_PSTR("device_id"), this->device_id); #endif - dump_field(out, "key", this->key); - dump_field(out, "carrier_frequency", this->carrier_frequency); - dump_field(out, "repeat_count", this->repeat_count); - out.append(" timings: "); - out.append("packed buffer ["); + dump_field(out, ESPHOME_PSTR("key"), this->key); + dump_field(out, ESPHOME_PSTR("carrier_frequency"), this->carrier_frequency); + dump_field(out, ESPHOME_PSTR("repeat_count"), this->repeat_count); + out.append(2, ' ').append_p(ESPHOME_PSTR("timings")).append(": "); + out.append_p(ESPHOME_PSTR("packed buffer [")); append_uint(out, this->timings_count_); - out.append(" values, "); + out.append_p(ESPHOME_PSTR(" values, ")); append_uint(out, this->timings_length_); - out.append(" bytes]\n"); + out.append_p(ESPHOME_PSTR(" bytes]\n")); return out.c_str(); } const char *InfraredRFReceiveEvent::dump_to(DumpBuffer &out) const { - MessageDumpHelper helper(out, "InfraredRFReceiveEvent"); + MessageDumpHelper helper(out, ESPHOME_PSTR("InfraredRFReceiveEvent")); #ifdef USE_DEVICES - dump_field(out, "device_id", this->device_id); + dump_field(out, ESPHOME_PSTR("device_id"), this->device_id); #endif - dump_field(out, "key", this->key); + dump_field(out, ESPHOME_PSTR("key"), this->key); for (const auto &it : *this->timings) { - dump_field(out, "timings", it, 4); + dump_field(out, ESPHOME_PSTR("timings"), it, 4); } return out.c_str(); } #endif +#ifdef USE_SERIAL_PROXY +const char *SerialProxyConfigureRequest::dump_to(DumpBuffer &out) const { + MessageDumpHelper helper(out, ESPHOME_PSTR("SerialProxyConfigureRequest")); + dump_field(out, ESPHOME_PSTR("instance"), this->instance); + dump_field(out, ESPHOME_PSTR("baudrate"), this->baudrate); + dump_field(out, ESPHOME_PSTR("flow_control"), this->flow_control); + dump_field(out, ESPHOME_PSTR("parity"), static_cast(this->parity)); + dump_field(out, ESPHOME_PSTR("stop_bits"), this->stop_bits); + dump_field(out, ESPHOME_PSTR("data_size"), this->data_size); + return out.c_str(); +} +const char *SerialProxyDataReceived::dump_to(DumpBuffer &out) const { + MessageDumpHelper helper(out, ESPHOME_PSTR("SerialProxyDataReceived")); + dump_field(out, ESPHOME_PSTR("instance"), this->instance); + dump_bytes_field(out, ESPHOME_PSTR("data"), this->data_ptr_, this->data_len_); + return out.c_str(); +} +const char *SerialProxyWriteRequest::dump_to(DumpBuffer &out) const { + MessageDumpHelper helper(out, ESPHOME_PSTR("SerialProxyWriteRequest")); + dump_field(out, ESPHOME_PSTR("instance"), this->instance); + dump_bytes_field(out, ESPHOME_PSTR("data"), this->data, this->data_len); + return out.c_str(); +} +const char *SerialProxySetModemPinsRequest::dump_to(DumpBuffer &out) const { + MessageDumpHelper helper(out, ESPHOME_PSTR("SerialProxySetModemPinsRequest")); + dump_field(out, ESPHOME_PSTR("instance"), this->instance); + dump_field(out, ESPHOME_PSTR("line_states"), this->line_states); + return out.c_str(); +} +const char *SerialProxyGetModemPinsRequest::dump_to(DumpBuffer &out) const { + MessageDumpHelper helper(out, ESPHOME_PSTR("SerialProxyGetModemPinsRequest")); + dump_field(out, ESPHOME_PSTR("instance"), this->instance); + return out.c_str(); +} +const char *SerialProxyGetModemPinsResponse::dump_to(DumpBuffer &out) const { + MessageDumpHelper helper(out, ESPHOME_PSTR("SerialProxyGetModemPinsResponse")); + dump_field(out, ESPHOME_PSTR("instance"), this->instance); + dump_field(out, ESPHOME_PSTR("line_states"), this->line_states); + return out.c_str(); +} +const char *SerialProxyRequest::dump_to(DumpBuffer &out) const { + MessageDumpHelper helper(out, ESPHOME_PSTR("SerialProxyRequest")); + dump_field(out, ESPHOME_PSTR("instance"), this->instance); + dump_field(out, ESPHOME_PSTR("type"), static_cast(this->type)); + return out.c_str(); +} +const char *SerialProxyRequestResponse::dump_to(DumpBuffer &out) const { + MessageDumpHelper helper(out, ESPHOME_PSTR("SerialProxyRequestResponse")); + dump_field(out, ESPHOME_PSTR("instance"), this->instance); + dump_field(out, ESPHOME_PSTR("type"), static_cast(this->type)); + dump_field(out, ESPHOME_PSTR("status"), static_cast(this->status)); + dump_field(out, ESPHOME_PSTR("error_message"), this->error_message); + return out.c_str(); +} +#endif +#ifdef USE_BLUETOOTH_PROXY +const char *BluetoothSetConnectionParamsRequest::dump_to(DumpBuffer &out) const { + MessageDumpHelper helper(out, ESPHOME_PSTR("BluetoothSetConnectionParamsRequest")); + dump_field(out, ESPHOME_PSTR("address"), this->address); + dump_field(out, ESPHOME_PSTR("min_interval"), this->min_interval); + dump_field(out, ESPHOME_PSTR("max_interval"), this->max_interval); + dump_field(out, ESPHOME_PSTR("latency"), this->latency); + dump_field(out, ESPHOME_PSTR("timeout"), this->timeout); + return out.c_str(); +} +const char *BluetoothSetConnectionParamsResponse::dump_to(DumpBuffer &out) const { + MessageDumpHelper helper(out, ESPHOME_PSTR("BluetoothSetConnectionParamsResponse")); + dump_field(out, ESPHOME_PSTR("address"), this->address); + dump_field(out, ESPHOME_PSTR("error"), this->error); + return out.c_str(); +} +#endif } // namespace esphome::api diff --git a/esphome/components/api/api_pb2_service.cpp b/esphome/components/api/api_pb2_service.cpp index 4b7148e6c0..b41233eddd 100644 --- a/esphome/components/api/api_pb2_service.cpp +++ b/esphome/components/api/api_pb2_service.cpp @@ -1,6 +1,7 @@ // This file was automatically generated with a tool. // See script/api_protobuf/api_protobuf.py #include "api_pb2_service.h" +#include "api_connection.h" #include "esphome/core/log.h" namespace esphome::api { @@ -8,16 +9,36 @@ namespace esphome::api { static const char *const TAG = "api.service"; #ifdef HAS_PROTO_MESSAGE_DUMP -void APIServerConnectionBase::log_send_message_(const char *name, const char *dump) { - ESP_LOGVV(TAG, "send_message %s: %s", name, dump); +void APIServerConnectionBase::log_send_message_(const LogString *name, const char *dump) { + ESP_LOGVV(TAG, "send_message %s: %s", LOG_STR_ARG(name), dump); } void APIServerConnectionBase::log_receive_message_(const LogString *name, const ProtoMessage &msg) { DumpBuffer dump_buf; ESP_LOGVV(TAG, "%s: %s", LOG_STR_ARG(name), msg.dump_to(dump_buf)); } +void APIServerConnectionBase::log_receive_message_(const LogString *name) { + ESP_LOGVV(TAG, "%s: {}", LOG_STR_ARG(name)); +} #endif -void APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type, const uint8_t *msg_data) { +void APIConnection::read_message_(uint32_t msg_size, uint32_t msg_type, const uint8_t *msg_data) { + // Check authentication/connection requirements + switch (msg_type) { + case HelloRequest::MESSAGE_TYPE: // No setup required + case DisconnectRequest::MESSAGE_TYPE: // No setup required + case PingRequest::MESSAGE_TYPE: // No setup required + break; + case 9 /* DeviceInfoRequest is empty */: // Connection setup only + if (!this->check_connection_setup_()) { + return; + } + break; + default: + if (!this->check_authenticated_()) { + return; + } + break; + } switch (msg_type) { case HelloRequest::MESSAGE_TYPE: { HelloRequest msg; @@ -29,66 +50,52 @@ void APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type, break; } case DisconnectRequest::MESSAGE_TYPE: { - DisconnectRequest msg; - // Empty message: no decode needed #ifdef HAS_PROTO_MESSAGE_DUMP - this->log_receive_message_(LOG_STR("on_disconnect_request"), msg); + this->log_receive_message_(LOG_STR("on_disconnect_request")); #endif - this->on_disconnect_request(msg); + this->on_disconnect_request(); break; } case DisconnectResponse::MESSAGE_TYPE: { - DisconnectResponse msg; - // Empty message: no decode needed #ifdef HAS_PROTO_MESSAGE_DUMP - this->log_receive_message_(LOG_STR("on_disconnect_response"), msg); + this->log_receive_message_(LOG_STR("on_disconnect_response")); #endif - this->on_disconnect_response(msg); + this->on_disconnect_response(); break; } case PingRequest::MESSAGE_TYPE: { - PingRequest msg; - // Empty message: no decode needed #ifdef HAS_PROTO_MESSAGE_DUMP - this->log_receive_message_(LOG_STR("on_ping_request"), msg); + this->log_receive_message_(LOG_STR("on_ping_request")); #endif - this->on_ping_request(msg); + this->on_ping_request(); break; } case PingResponse::MESSAGE_TYPE: { - PingResponse msg; - // Empty message: no decode needed #ifdef HAS_PROTO_MESSAGE_DUMP - this->log_receive_message_(LOG_STR("on_ping_response"), msg); + this->log_receive_message_(LOG_STR("on_ping_response")); #endif - this->on_ping_response(msg); + this->on_ping_response(); break; } - case DeviceInfoRequest::MESSAGE_TYPE: { - DeviceInfoRequest msg; - // Empty message: no decode needed + case 9 /* DeviceInfoRequest is empty */: { #ifdef HAS_PROTO_MESSAGE_DUMP - this->log_receive_message_(LOG_STR("on_device_info_request"), msg); + this->log_receive_message_(LOG_STR("on_device_info_request")); #endif - this->on_device_info_request(msg); + this->on_device_info_request(); break; } - case ListEntitiesRequest::MESSAGE_TYPE: { - ListEntitiesRequest msg; - // Empty message: no decode needed + case 11 /* ListEntitiesRequest is empty */: { #ifdef HAS_PROTO_MESSAGE_DUMP - this->log_receive_message_(LOG_STR("on_list_entities_request"), msg); + this->log_receive_message_(LOG_STR("on_list_entities_request")); #endif - this->on_list_entities_request(msg); + this->on_list_entities_request(); break; } - case SubscribeStatesRequest::MESSAGE_TYPE: { - SubscribeStatesRequest msg; - // Empty message: no decode needed + case 20 /* SubscribeStatesRequest is empty */: { #ifdef HAS_PROTO_MESSAGE_DUMP - this->log_receive_message_(LOG_STR("on_subscribe_states_request"), msg); + this->log_receive_message_(LOG_STR("on_subscribe_states_request")); #endif - this->on_subscribe_states_request(msg); + this->on_subscribe_states_request(); break; } case SubscribeLogsRequest::MESSAGE_TYPE: { @@ -145,13 +152,11 @@ void APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type, } #endif #ifdef USE_API_HOMEASSISTANT_SERVICES - case SubscribeHomeassistantServicesRequest::MESSAGE_TYPE: { - SubscribeHomeassistantServicesRequest msg; - // Empty message: no decode needed + case 34 /* SubscribeHomeassistantServicesRequest is empty */: { #ifdef HAS_PROTO_MESSAGE_DUMP - this->log_receive_message_(LOG_STR("on_subscribe_homeassistant_services_request"), msg); + this->log_receive_message_(LOG_STR("on_subscribe_homeassistant_services_request")); #endif - this->on_subscribe_homeassistant_services_request(msg); + this->on_subscribe_homeassistant_services_request(); break; } #endif @@ -165,13 +170,11 @@ void APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type, break; } #ifdef USE_API_HOMEASSISTANT_STATES - case SubscribeHomeAssistantStatesRequest::MESSAGE_TYPE: { - SubscribeHomeAssistantStatesRequest msg; - // Empty message: no decode needed + case 38 /* SubscribeHomeAssistantStatesRequest is empty */: { #ifdef HAS_PROTO_MESSAGE_DUMP - this->log_receive_message_(LOG_STR("on_subscribe_home_assistant_states_request"), msg); + this->log_receive_message_(LOG_STR("on_subscribe_home_assistant_states_request")); #endif - this->on_subscribe_home_assistant_states_request(msg); + this->on_subscribe_home_assistant_states_request(); break; } #endif @@ -374,24 +377,20 @@ void APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type, } #endif #ifdef USE_BLUETOOTH_PROXY - case SubscribeBluetoothConnectionsFreeRequest::MESSAGE_TYPE: { - SubscribeBluetoothConnectionsFreeRequest msg; - // Empty message: no decode needed + case 80 /* SubscribeBluetoothConnectionsFreeRequest is empty */: { #ifdef HAS_PROTO_MESSAGE_DUMP - this->log_receive_message_(LOG_STR("on_subscribe_bluetooth_connections_free_request"), msg); + this->log_receive_message_(LOG_STR("on_subscribe_bluetooth_connections_free_request")); #endif - this->on_subscribe_bluetooth_connections_free_request(msg); + this->on_subscribe_bluetooth_connections_free_request(); break; } #endif #ifdef USE_BLUETOOTH_PROXY - case UnsubscribeBluetoothLEAdvertisementsRequest::MESSAGE_TYPE: { - UnsubscribeBluetoothLEAdvertisementsRequest msg; - // Empty message: no decode needed + case 87 /* UnsubscribeBluetoothLEAdvertisementsRequest is empty */: { #ifdef HAS_PROTO_MESSAGE_DUMP - this->log_receive_message_(LOG_STR("on_unsubscribe_bluetooth_le_advertisements_request"), msg); + this->log_receive_message_(LOG_STR("on_unsubscribe_bluetooth_le_advertisements_request")); #endif - this->on_unsubscribe_bluetooth_le_advertisements_request(msg); + this->on_unsubscribe_bluetooth_le_advertisements_request(); break; } #endif @@ -636,232 +635,76 @@ void APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type, this->on_infrared_rf_transmit_raw_timings_request(msg); break; } +#endif +#ifdef USE_SERIAL_PROXY + case SerialProxyConfigureRequest::MESSAGE_TYPE: { + SerialProxyConfigureRequest msg; + msg.decode(msg_data, msg_size); +#ifdef HAS_PROTO_MESSAGE_DUMP + this->log_receive_message_(LOG_STR("on_serial_proxy_configure_request"), msg); +#endif + this->on_serial_proxy_configure_request(msg); + break; + } +#endif +#ifdef USE_SERIAL_PROXY + case SerialProxyWriteRequest::MESSAGE_TYPE: { + SerialProxyWriteRequest msg; + msg.decode(msg_data, msg_size); +#ifdef HAS_PROTO_MESSAGE_DUMP + this->log_receive_message_(LOG_STR("on_serial_proxy_write_request"), msg); +#endif + this->on_serial_proxy_write_request(msg); + break; + } +#endif +#ifdef USE_SERIAL_PROXY + case SerialProxySetModemPinsRequest::MESSAGE_TYPE: { + SerialProxySetModemPinsRequest msg; + msg.decode(msg_data, msg_size); +#ifdef HAS_PROTO_MESSAGE_DUMP + this->log_receive_message_(LOG_STR("on_serial_proxy_set_modem_pins_request"), msg); +#endif + this->on_serial_proxy_set_modem_pins_request(msg); + break; + } +#endif +#ifdef USE_SERIAL_PROXY + case SerialProxyGetModemPinsRequest::MESSAGE_TYPE: { + SerialProxyGetModemPinsRequest msg; + msg.decode(msg_data, msg_size); +#ifdef HAS_PROTO_MESSAGE_DUMP + this->log_receive_message_(LOG_STR("on_serial_proxy_get_modem_pins_request"), msg); +#endif + this->on_serial_proxy_get_modem_pins_request(msg); + break; + } +#endif +#ifdef USE_SERIAL_PROXY + case SerialProxyRequest::MESSAGE_TYPE: { + SerialProxyRequest msg; + msg.decode(msg_data, msg_size); +#ifdef HAS_PROTO_MESSAGE_DUMP + this->log_receive_message_(LOG_STR("on_serial_proxy_request"), msg); +#endif + this->on_serial_proxy_request(msg); + break; + } +#endif +#ifdef USE_BLUETOOTH_PROXY + case BluetoothSetConnectionParamsRequest::MESSAGE_TYPE: { + BluetoothSetConnectionParamsRequest msg; + msg.decode(msg_data, msg_size); +#ifdef HAS_PROTO_MESSAGE_DUMP + this->log_receive_message_(LOG_STR("on_bluetooth_set_connection_params_request"), msg); +#endif + this->on_bluetooth_set_connection_params_request(msg); + break; + } #endif default: break; } } -void APIServerConnection::on_hello_request(const HelloRequest &msg) { - if (!this->send_hello_response(msg)) { - this->on_fatal_error(); - } -} -void APIServerConnection::on_disconnect_request(const DisconnectRequest &msg) { - if (!this->send_disconnect_response(msg)) { - this->on_fatal_error(); - } -} -void APIServerConnection::on_ping_request(const PingRequest &msg) { - if (!this->send_ping_response(msg)) { - this->on_fatal_error(); - } -} -void APIServerConnection::on_device_info_request(const DeviceInfoRequest &msg) { - if (!this->send_device_info_response(msg)) { - this->on_fatal_error(); - } -} -void APIServerConnection::on_list_entities_request(const ListEntitiesRequest &msg) { this->list_entities(msg); } -void APIServerConnection::on_subscribe_states_request(const SubscribeStatesRequest &msg) { - this->subscribe_states(msg); -} -void APIServerConnection::on_subscribe_logs_request(const SubscribeLogsRequest &msg) { this->subscribe_logs(msg); } -#ifdef USE_API_HOMEASSISTANT_SERVICES -void APIServerConnection::on_subscribe_homeassistant_services_request( - const SubscribeHomeassistantServicesRequest &msg) { - this->subscribe_homeassistant_services(msg); -} -#endif -#ifdef USE_API_HOMEASSISTANT_STATES -void APIServerConnection::on_subscribe_home_assistant_states_request(const SubscribeHomeAssistantStatesRequest &msg) { - this->subscribe_home_assistant_states(msg); -} -#endif -#ifdef USE_API_USER_DEFINED_ACTIONS -void APIServerConnection::on_execute_service_request(const ExecuteServiceRequest &msg) { this->execute_service(msg); } -#endif -#ifdef USE_API_NOISE -void APIServerConnection::on_noise_encryption_set_key_request(const NoiseEncryptionSetKeyRequest &msg) { - if (!this->send_noise_encryption_set_key_response(msg)) { - this->on_fatal_error(); - } -} -#endif -#ifdef USE_BUTTON -void APIServerConnection::on_button_command_request(const ButtonCommandRequest &msg) { this->button_command(msg); } -#endif -#ifdef USE_CAMERA -void APIServerConnection::on_camera_image_request(const CameraImageRequest &msg) { this->camera_image(msg); } -#endif -#ifdef USE_CLIMATE -void APIServerConnection::on_climate_command_request(const ClimateCommandRequest &msg) { this->climate_command(msg); } -#endif -#ifdef USE_COVER -void APIServerConnection::on_cover_command_request(const CoverCommandRequest &msg) { this->cover_command(msg); } -#endif -#ifdef USE_DATETIME_DATE -void APIServerConnection::on_date_command_request(const DateCommandRequest &msg) { this->date_command(msg); } -#endif -#ifdef USE_DATETIME_DATETIME -void APIServerConnection::on_date_time_command_request(const DateTimeCommandRequest &msg) { - this->datetime_command(msg); -} -#endif -#ifdef USE_FAN -void APIServerConnection::on_fan_command_request(const FanCommandRequest &msg) { this->fan_command(msg); } -#endif -#ifdef USE_LIGHT -void APIServerConnection::on_light_command_request(const LightCommandRequest &msg) { this->light_command(msg); } -#endif -#ifdef USE_LOCK -void APIServerConnection::on_lock_command_request(const LockCommandRequest &msg) { this->lock_command(msg); } -#endif -#ifdef USE_MEDIA_PLAYER -void APIServerConnection::on_media_player_command_request(const MediaPlayerCommandRequest &msg) { - this->media_player_command(msg); -} -#endif -#ifdef USE_NUMBER -void APIServerConnection::on_number_command_request(const NumberCommandRequest &msg) { this->number_command(msg); } -#endif -#ifdef USE_SELECT -void APIServerConnection::on_select_command_request(const SelectCommandRequest &msg) { this->select_command(msg); } -#endif -#ifdef USE_SIREN -void APIServerConnection::on_siren_command_request(const SirenCommandRequest &msg) { this->siren_command(msg); } -#endif -#ifdef USE_SWITCH -void APIServerConnection::on_switch_command_request(const SwitchCommandRequest &msg) { this->switch_command(msg); } -#endif -#ifdef USE_TEXT -void APIServerConnection::on_text_command_request(const TextCommandRequest &msg) { this->text_command(msg); } -#endif -#ifdef USE_DATETIME_TIME -void APIServerConnection::on_time_command_request(const TimeCommandRequest &msg) { this->time_command(msg); } -#endif -#ifdef USE_UPDATE -void APIServerConnection::on_update_command_request(const UpdateCommandRequest &msg) { this->update_command(msg); } -#endif -#ifdef USE_VALVE -void APIServerConnection::on_valve_command_request(const ValveCommandRequest &msg) { this->valve_command(msg); } -#endif -#ifdef USE_BLUETOOTH_PROXY -void APIServerConnection::on_subscribe_bluetooth_le_advertisements_request( - const SubscribeBluetoothLEAdvertisementsRequest &msg) { - this->subscribe_bluetooth_le_advertisements(msg); -} -#endif -#ifdef USE_BLUETOOTH_PROXY -void APIServerConnection::on_bluetooth_device_request(const BluetoothDeviceRequest &msg) { - this->bluetooth_device_request(msg); -} -#endif -#ifdef USE_BLUETOOTH_PROXY -void APIServerConnection::on_bluetooth_gatt_get_services_request(const BluetoothGATTGetServicesRequest &msg) { - this->bluetooth_gatt_get_services(msg); -} -#endif -#ifdef USE_BLUETOOTH_PROXY -void APIServerConnection::on_bluetooth_gatt_read_request(const BluetoothGATTReadRequest &msg) { - this->bluetooth_gatt_read(msg); -} -#endif -#ifdef USE_BLUETOOTH_PROXY -void APIServerConnection::on_bluetooth_gatt_write_request(const BluetoothGATTWriteRequest &msg) { - this->bluetooth_gatt_write(msg); -} -#endif -#ifdef USE_BLUETOOTH_PROXY -void APIServerConnection::on_bluetooth_gatt_read_descriptor_request(const BluetoothGATTReadDescriptorRequest &msg) { - this->bluetooth_gatt_read_descriptor(msg); -} -#endif -#ifdef USE_BLUETOOTH_PROXY -void APIServerConnection::on_bluetooth_gatt_write_descriptor_request(const BluetoothGATTWriteDescriptorRequest &msg) { - this->bluetooth_gatt_write_descriptor(msg); -} -#endif -#ifdef USE_BLUETOOTH_PROXY -void APIServerConnection::on_bluetooth_gatt_notify_request(const BluetoothGATTNotifyRequest &msg) { - this->bluetooth_gatt_notify(msg); -} -#endif -#ifdef USE_BLUETOOTH_PROXY -void APIServerConnection::on_subscribe_bluetooth_connections_free_request( - const SubscribeBluetoothConnectionsFreeRequest &msg) { - if (!this->send_subscribe_bluetooth_connections_free_response(msg)) { - this->on_fatal_error(); - } -} -#endif -#ifdef USE_BLUETOOTH_PROXY -void APIServerConnection::on_unsubscribe_bluetooth_le_advertisements_request( - const UnsubscribeBluetoothLEAdvertisementsRequest &msg) { - this->unsubscribe_bluetooth_le_advertisements(msg); -} -#endif -#ifdef USE_BLUETOOTH_PROXY -void APIServerConnection::on_bluetooth_scanner_set_mode_request(const BluetoothScannerSetModeRequest &msg) { - this->bluetooth_scanner_set_mode(msg); -} -#endif -#ifdef USE_VOICE_ASSISTANT -void APIServerConnection::on_subscribe_voice_assistant_request(const SubscribeVoiceAssistantRequest &msg) { - this->subscribe_voice_assistant(msg); -} -#endif -#ifdef USE_VOICE_ASSISTANT -void APIServerConnection::on_voice_assistant_configuration_request(const VoiceAssistantConfigurationRequest &msg) { - if (!this->send_voice_assistant_get_configuration_response(msg)) { - this->on_fatal_error(); - } -} -#endif -#ifdef USE_VOICE_ASSISTANT -void APIServerConnection::on_voice_assistant_set_configuration(const VoiceAssistantSetConfiguration &msg) { - this->voice_assistant_set_configuration(msg); -} -#endif -#ifdef USE_ALARM_CONTROL_PANEL -void APIServerConnection::on_alarm_control_panel_command_request(const AlarmControlPanelCommandRequest &msg) { - this->alarm_control_panel_command(msg); -} -#endif -#ifdef USE_ZWAVE_PROXY -void APIServerConnection::on_z_wave_proxy_frame(const ZWaveProxyFrame &msg) { this->zwave_proxy_frame(msg); } -#endif -#ifdef USE_ZWAVE_PROXY -void APIServerConnection::on_z_wave_proxy_request(const ZWaveProxyRequest &msg) { this->zwave_proxy_request(msg); } -#endif -#ifdef USE_IR_RF -void APIServerConnection::on_infrared_rf_transmit_raw_timings_request(const InfraredRFTransmitRawTimingsRequest &msg) { - this->infrared_rf_transmit_raw_timings(msg); -} -#endif - -void APIServerConnection::read_message(uint32_t msg_size, uint32_t msg_type, const uint8_t *msg_data) { - // Check authentication/connection requirements for messages - switch (msg_type) { - case HelloRequest::MESSAGE_TYPE: // No setup required - case DisconnectRequest::MESSAGE_TYPE: // No setup required - case PingRequest::MESSAGE_TYPE: // No setup required - break; // Skip all checks for these messages - case DeviceInfoRequest::MESSAGE_TYPE: // Connection setup only - if (!this->check_connection_setup_()) { - return; // Connection not setup - } - break; - default: - // All other messages require authentication (which includes connection check) - if (!this->check_authenticated_()) { - return; // Authentication failed - } - break; - } - - // Call base implementation to process the message - APIServerConnectionBase::read_message(msg_size, msg_type, msg_data); -} - } // namespace esphome::api diff --git a/esphome/components/api/api_pb2_service.h b/esphome/components/api/api_pb2_service.h index 200991c282..6ff988902f 100644 --- a/esphome/components/api/api_pb2_service.h +++ b/esphome/components/api/api_pb2_service.h @@ -8,486 +8,234 @@ namespace esphome::api { -class APIServerConnectionBase : public ProtoService { +class APIServerConnectionBase { public: #ifdef HAS_PROTO_MESSAGE_DUMP protected: - void log_send_message_(const char *name, const char *dump); + void log_send_message_(const LogString *name, const char *dump); void log_receive_message_(const LogString *name, const ProtoMessage &msg); + void log_receive_message_(const LogString *name); public: #endif - bool send_message(const ProtoMessage &msg, uint8_t message_type) { -#ifdef HAS_PROTO_MESSAGE_DUMP - DumpBuffer dump_buf; - this->log_send_message_(msg.message_name(), msg.dump_to(dump_buf)); -#endif - return this->send_message_(msg, message_type); - } + void on_hello_request(const HelloRequest &value){}; - virtual void on_hello_request(const HelloRequest &value){}; + void on_disconnect_request(){}; + void on_disconnect_response(){}; + void on_ping_request(){}; + void on_ping_response(){}; + void on_device_info_request(){}; - virtual void on_disconnect_request(const DisconnectRequest &value){}; - virtual void on_disconnect_response(const DisconnectResponse &value){}; - virtual void on_ping_request(const PingRequest &value){}; - virtual void on_ping_response(const PingResponse &value){}; - virtual void on_device_info_request(const DeviceInfoRequest &value){}; + void on_list_entities_request(){}; - virtual void on_list_entities_request(const ListEntitiesRequest &value){}; - - virtual void on_subscribe_states_request(const SubscribeStatesRequest &value){}; + void on_subscribe_states_request(){}; #ifdef USE_COVER - virtual void on_cover_command_request(const CoverCommandRequest &value){}; + void on_cover_command_request(const CoverCommandRequest &value){}; #endif #ifdef USE_FAN - virtual void on_fan_command_request(const FanCommandRequest &value){}; + void on_fan_command_request(const FanCommandRequest &value){}; #endif #ifdef USE_LIGHT - virtual void on_light_command_request(const LightCommandRequest &value){}; + void on_light_command_request(const LightCommandRequest &value){}; #endif #ifdef USE_SWITCH - virtual void on_switch_command_request(const SwitchCommandRequest &value){}; + void on_switch_command_request(const SwitchCommandRequest &value){}; #endif - virtual void on_subscribe_logs_request(const SubscribeLogsRequest &value){}; + void on_subscribe_logs_request(const SubscribeLogsRequest &value){}; #ifdef USE_API_NOISE - virtual void on_noise_encryption_set_key_request(const NoiseEncryptionSetKeyRequest &value){}; + void on_noise_encryption_set_key_request(const NoiseEncryptionSetKeyRequest &value){}; #endif #ifdef USE_API_HOMEASSISTANT_SERVICES - virtual void on_subscribe_homeassistant_services_request(const SubscribeHomeassistantServicesRequest &value){}; + void on_subscribe_homeassistant_services_request(){}; #endif #ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES - virtual void on_homeassistant_action_response(const HomeassistantActionResponse &value){}; + void on_homeassistant_action_response(const HomeassistantActionResponse &value){}; #endif #ifdef USE_API_HOMEASSISTANT_STATES - virtual void on_subscribe_home_assistant_states_request(const SubscribeHomeAssistantStatesRequest &value){}; + void on_subscribe_home_assistant_states_request(){}; #endif #ifdef USE_API_HOMEASSISTANT_STATES - virtual void on_home_assistant_state_response(const HomeAssistantStateResponse &value){}; + void on_home_assistant_state_response(const HomeAssistantStateResponse &value){}; #endif - virtual void on_get_time_response(const GetTimeResponse &value){}; + void on_get_time_response(const GetTimeResponse &value){}; #ifdef USE_API_USER_DEFINED_ACTIONS - virtual void on_execute_service_request(const ExecuteServiceRequest &value){}; + void on_execute_service_request(const ExecuteServiceRequest &value){}; #endif #ifdef USE_CAMERA - virtual void on_camera_image_request(const CameraImageRequest &value){}; + void on_camera_image_request(const CameraImageRequest &value){}; #endif #ifdef USE_CLIMATE - virtual void on_climate_command_request(const ClimateCommandRequest &value){}; + void on_climate_command_request(const ClimateCommandRequest &value){}; #endif #ifdef USE_WATER_HEATER - virtual void on_water_heater_command_request(const WaterHeaterCommandRequest &value){}; + void on_water_heater_command_request(const WaterHeaterCommandRequest &value){}; #endif #ifdef USE_NUMBER - virtual void on_number_command_request(const NumberCommandRequest &value){}; + void on_number_command_request(const NumberCommandRequest &value){}; #endif #ifdef USE_SELECT - virtual void on_select_command_request(const SelectCommandRequest &value){}; + void on_select_command_request(const SelectCommandRequest &value){}; #endif #ifdef USE_SIREN - virtual void on_siren_command_request(const SirenCommandRequest &value){}; + void on_siren_command_request(const SirenCommandRequest &value){}; #endif #ifdef USE_LOCK - virtual void on_lock_command_request(const LockCommandRequest &value){}; + void on_lock_command_request(const LockCommandRequest &value){}; #endif #ifdef USE_BUTTON - virtual void on_button_command_request(const ButtonCommandRequest &value){}; + void on_button_command_request(const ButtonCommandRequest &value){}; #endif #ifdef USE_MEDIA_PLAYER - virtual void on_media_player_command_request(const MediaPlayerCommandRequest &value){}; + void on_media_player_command_request(const MediaPlayerCommandRequest &value){}; #endif #ifdef USE_BLUETOOTH_PROXY - virtual void on_subscribe_bluetooth_le_advertisements_request( - const SubscribeBluetoothLEAdvertisementsRequest &value){}; + void on_subscribe_bluetooth_le_advertisements_request(const SubscribeBluetoothLEAdvertisementsRequest &value){}; #endif #ifdef USE_BLUETOOTH_PROXY - virtual void on_bluetooth_device_request(const BluetoothDeviceRequest &value){}; + void on_bluetooth_device_request(const BluetoothDeviceRequest &value){}; #endif #ifdef USE_BLUETOOTH_PROXY - virtual void on_bluetooth_gatt_get_services_request(const BluetoothGATTGetServicesRequest &value){}; + void on_bluetooth_gatt_get_services_request(const BluetoothGATTGetServicesRequest &value){}; #endif #ifdef USE_BLUETOOTH_PROXY - virtual void on_bluetooth_gatt_read_request(const BluetoothGATTReadRequest &value){}; + void on_bluetooth_gatt_read_request(const BluetoothGATTReadRequest &value){}; #endif #ifdef USE_BLUETOOTH_PROXY - virtual void on_bluetooth_gatt_write_request(const BluetoothGATTWriteRequest &value){}; + void on_bluetooth_gatt_write_request(const BluetoothGATTWriteRequest &value){}; #endif #ifdef USE_BLUETOOTH_PROXY - virtual void on_bluetooth_gatt_read_descriptor_request(const BluetoothGATTReadDescriptorRequest &value){}; + void on_bluetooth_gatt_read_descriptor_request(const BluetoothGATTReadDescriptorRequest &value){}; #endif #ifdef USE_BLUETOOTH_PROXY - virtual void on_bluetooth_gatt_write_descriptor_request(const BluetoothGATTWriteDescriptorRequest &value){}; + void on_bluetooth_gatt_write_descriptor_request(const BluetoothGATTWriteDescriptorRequest &value){}; #endif #ifdef USE_BLUETOOTH_PROXY - virtual void on_bluetooth_gatt_notify_request(const BluetoothGATTNotifyRequest &value){}; + void on_bluetooth_gatt_notify_request(const BluetoothGATTNotifyRequest &value){}; #endif #ifdef USE_BLUETOOTH_PROXY - virtual void on_subscribe_bluetooth_connections_free_request(const SubscribeBluetoothConnectionsFreeRequest &value){}; + void on_subscribe_bluetooth_connections_free_request(){}; #endif #ifdef USE_BLUETOOTH_PROXY - virtual void on_unsubscribe_bluetooth_le_advertisements_request( - const UnsubscribeBluetoothLEAdvertisementsRequest &value){}; + void on_unsubscribe_bluetooth_le_advertisements_request(){}; #endif #ifdef USE_BLUETOOTH_PROXY - virtual void on_bluetooth_scanner_set_mode_request(const BluetoothScannerSetModeRequest &value){}; + void on_bluetooth_scanner_set_mode_request(const BluetoothScannerSetModeRequest &value){}; #endif #ifdef USE_VOICE_ASSISTANT - virtual void on_subscribe_voice_assistant_request(const SubscribeVoiceAssistantRequest &value){}; + void on_subscribe_voice_assistant_request(const SubscribeVoiceAssistantRequest &value){}; #endif #ifdef USE_VOICE_ASSISTANT - virtual void on_voice_assistant_response(const VoiceAssistantResponse &value){}; + void on_voice_assistant_response(const VoiceAssistantResponse &value){}; #endif #ifdef USE_VOICE_ASSISTANT - virtual void on_voice_assistant_event_response(const VoiceAssistantEventResponse &value){}; + void on_voice_assistant_event_response(const VoiceAssistantEventResponse &value){}; #endif #ifdef USE_VOICE_ASSISTANT - virtual void on_voice_assistant_audio(const VoiceAssistantAudio &value){}; + void on_voice_assistant_audio(const VoiceAssistantAudio &value){}; #endif #ifdef USE_VOICE_ASSISTANT - virtual void on_voice_assistant_timer_event_response(const VoiceAssistantTimerEventResponse &value){}; + void on_voice_assistant_timer_event_response(const VoiceAssistantTimerEventResponse &value){}; #endif #ifdef USE_VOICE_ASSISTANT - virtual void on_voice_assistant_announce_request(const VoiceAssistantAnnounceRequest &value){}; + void on_voice_assistant_announce_request(const VoiceAssistantAnnounceRequest &value){}; #endif #ifdef USE_VOICE_ASSISTANT - virtual void on_voice_assistant_configuration_request(const VoiceAssistantConfigurationRequest &value){}; + void on_voice_assistant_configuration_request(const VoiceAssistantConfigurationRequest &value){}; #endif #ifdef USE_VOICE_ASSISTANT - virtual void on_voice_assistant_set_configuration(const VoiceAssistantSetConfiguration &value){}; + void on_voice_assistant_set_configuration(const VoiceAssistantSetConfiguration &value){}; #endif #ifdef USE_ALARM_CONTROL_PANEL - virtual void on_alarm_control_panel_command_request(const AlarmControlPanelCommandRequest &value){}; + void on_alarm_control_panel_command_request(const AlarmControlPanelCommandRequest &value){}; #endif #ifdef USE_TEXT - virtual void on_text_command_request(const TextCommandRequest &value){}; + void on_text_command_request(const TextCommandRequest &value){}; #endif #ifdef USE_DATETIME_DATE - virtual void on_date_command_request(const DateCommandRequest &value){}; + void on_date_command_request(const DateCommandRequest &value){}; #endif #ifdef USE_DATETIME_TIME - virtual void on_time_command_request(const TimeCommandRequest &value){}; + void on_time_command_request(const TimeCommandRequest &value){}; #endif #ifdef USE_VALVE - virtual void on_valve_command_request(const ValveCommandRequest &value){}; + void on_valve_command_request(const ValveCommandRequest &value){}; #endif #ifdef USE_DATETIME_DATETIME - virtual void on_date_time_command_request(const DateTimeCommandRequest &value){}; + void on_date_time_command_request(const DateTimeCommandRequest &value){}; #endif #ifdef USE_UPDATE - virtual void on_update_command_request(const UpdateCommandRequest &value){}; + void on_update_command_request(const UpdateCommandRequest &value){}; #endif #ifdef USE_ZWAVE_PROXY - virtual void on_z_wave_proxy_frame(const ZWaveProxyFrame &value){}; + void on_z_wave_proxy_frame(const ZWaveProxyFrame &value){}; #endif #ifdef USE_ZWAVE_PROXY - virtual void on_z_wave_proxy_request(const ZWaveProxyRequest &value){}; + void on_z_wave_proxy_request(const ZWaveProxyRequest &value){}; #endif #ifdef USE_IR_RF - virtual void on_infrared_rf_transmit_raw_timings_request(const InfraredRFTransmitRawTimingsRequest &value){}; + void on_infrared_rf_transmit_raw_timings_request(const InfraredRFTransmitRawTimingsRequest &value){}; #endif - protected: - void read_message(uint32_t msg_size, uint32_t msg_type, const uint8_t *msg_data) override; -}; +#ifdef USE_SERIAL_PROXY + void on_serial_proxy_configure_request(const SerialProxyConfigureRequest &value){}; +#endif -class APIServerConnection : public APIServerConnectionBase { - public: - virtual bool send_hello_response(const HelloRequest &msg) = 0; - virtual bool send_disconnect_response(const DisconnectRequest &msg) = 0; - virtual bool send_ping_response(const PingRequest &msg) = 0; - virtual bool send_device_info_response(const DeviceInfoRequest &msg) = 0; - virtual void list_entities(const ListEntitiesRequest &msg) = 0; - virtual void subscribe_states(const SubscribeStatesRequest &msg) = 0; - virtual void subscribe_logs(const SubscribeLogsRequest &msg) = 0; -#ifdef USE_API_HOMEASSISTANT_SERVICES - virtual void subscribe_homeassistant_services(const SubscribeHomeassistantServicesRequest &msg) = 0; +#ifdef USE_SERIAL_PROXY + void on_serial_proxy_write_request(const SerialProxyWriteRequest &value){}; #endif -#ifdef USE_API_HOMEASSISTANT_STATES - virtual void subscribe_home_assistant_states(const SubscribeHomeAssistantStatesRequest &msg) = 0; +#ifdef USE_SERIAL_PROXY + void on_serial_proxy_set_modem_pins_request(const SerialProxySetModemPinsRequest &value){}; #endif -#ifdef USE_API_USER_DEFINED_ACTIONS - virtual void execute_service(const ExecuteServiceRequest &msg) = 0; +#ifdef USE_SERIAL_PROXY + void on_serial_proxy_get_modem_pins_request(const SerialProxyGetModemPinsRequest &value){}; #endif -#ifdef USE_API_NOISE - virtual bool send_noise_encryption_set_key_response(const NoiseEncryptionSetKeyRequest &msg) = 0; -#endif -#ifdef USE_BUTTON - virtual void button_command(const ButtonCommandRequest &msg) = 0; -#endif -#ifdef USE_CAMERA - virtual void camera_image(const CameraImageRequest &msg) = 0; -#endif -#ifdef USE_CLIMATE - virtual void climate_command(const ClimateCommandRequest &msg) = 0; -#endif -#ifdef USE_COVER - virtual void cover_command(const CoverCommandRequest &msg) = 0; -#endif -#ifdef USE_DATETIME_DATE - virtual void date_command(const DateCommandRequest &msg) = 0; -#endif -#ifdef USE_DATETIME_DATETIME - virtual void datetime_command(const DateTimeCommandRequest &msg) = 0; -#endif -#ifdef USE_FAN - virtual void fan_command(const FanCommandRequest &msg) = 0; -#endif -#ifdef USE_LIGHT - virtual void light_command(const LightCommandRequest &msg) = 0; -#endif -#ifdef USE_LOCK - virtual void lock_command(const LockCommandRequest &msg) = 0; -#endif -#ifdef USE_MEDIA_PLAYER - virtual void media_player_command(const MediaPlayerCommandRequest &msg) = 0; -#endif -#ifdef USE_NUMBER - virtual void number_command(const NumberCommandRequest &msg) = 0; -#endif -#ifdef USE_SELECT - virtual void select_command(const SelectCommandRequest &msg) = 0; -#endif -#ifdef USE_SIREN - virtual void siren_command(const SirenCommandRequest &msg) = 0; -#endif -#ifdef USE_SWITCH - virtual void switch_command(const SwitchCommandRequest &msg) = 0; -#endif -#ifdef USE_TEXT - virtual void text_command(const TextCommandRequest &msg) = 0; -#endif -#ifdef USE_DATETIME_TIME - virtual void time_command(const TimeCommandRequest &msg) = 0; -#endif -#ifdef USE_UPDATE - virtual void update_command(const UpdateCommandRequest &msg) = 0; -#endif -#ifdef USE_VALVE - virtual void valve_command(const ValveCommandRequest &msg) = 0; + +#ifdef USE_SERIAL_PROXY + void on_serial_proxy_request(const SerialProxyRequest &value){}; #endif + #ifdef USE_BLUETOOTH_PROXY - virtual void subscribe_bluetooth_le_advertisements(const SubscribeBluetoothLEAdvertisementsRequest &msg) = 0; + void on_bluetooth_set_connection_params_request(const BluetoothSetConnectionParamsRequest &value){}; #endif -#ifdef USE_BLUETOOTH_PROXY - virtual void bluetooth_device_request(const BluetoothDeviceRequest &msg) = 0; -#endif -#ifdef USE_BLUETOOTH_PROXY - virtual void bluetooth_gatt_get_services(const BluetoothGATTGetServicesRequest &msg) = 0; -#endif -#ifdef USE_BLUETOOTH_PROXY - virtual void bluetooth_gatt_read(const BluetoothGATTReadRequest &msg) = 0; -#endif -#ifdef USE_BLUETOOTH_PROXY - virtual void bluetooth_gatt_write(const BluetoothGATTWriteRequest &msg) = 0; -#endif -#ifdef USE_BLUETOOTH_PROXY - virtual void bluetooth_gatt_read_descriptor(const BluetoothGATTReadDescriptorRequest &msg) = 0; -#endif -#ifdef USE_BLUETOOTH_PROXY - virtual void bluetooth_gatt_write_descriptor(const BluetoothGATTWriteDescriptorRequest &msg) = 0; -#endif -#ifdef USE_BLUETOOTH_PROXY - virtual void bluetooth_gatt_notify(const BluetoothGATTNotifyRequest &msg) = 0; -#endif -#ifdef USE_BLUETOOTH_PROXY - virtual bool send_subscribe_bluetooth_connections_free_response( - const SubscribeBluetoothConnectionsFreeRequest &msg) = 0; -#endif -#ifdef USE_BLUETOOTH_PROXY - virtual void unsubscribe_bluetooth_le_advertisements(const UnsubscribeBluetoothLEAdvertisementsRequest &msg) = 0; -#endif -#ifdef USE_BLUETOOTH_PROXY - virtual void bluetooth_scanner_set_mode(const BluetoothScannerSetModeRequest &msg) = 0; -#endif -#ifdef USE_VOICE_ASSISTANT - virtual void subscribe_voice_assistant(const SubscribeVoiceAssistantRequest &msg) = 0; -#endif -#ifdef USE_VOICE_ASSISTANT - virtual bool send_voice_assistant_get_configuration_response(const VoiceAssistantConfigurationRequest &msg) = 0; -#endif -#ifdef USE_VOICE_ASSISTANT - virtual void voice_assistant_set_configuration(const VoiceAssistantSetConfiguration &msg) = 0; -#endif -#ifdef USE_ALARM_CONTROL_PANEL - virtual void alarm_control_panel_command(const AlarmControlPanelCommandRequest &msg) = 0; -#endif -#ifdef USE_ZWAVE_PROXY - virtual void zwave_proxy_frame(const ZWaveProxyFrame &msg) = 0; -#endif -#ifdef USE_ZWAVE_PROXY - virtual void zwave_proxy_request(const ZWaveProxyRequest &msg) = 0; -#endif -#ifdef USE_IR_RF - virtual void infrared_rf_transmit_raw_timings(const InfraredRFTransmitRawTimingsRequest &msg) = 0; -#endif - protected: - void on_hello_request(const HelloRequest &msg) override; - void on_disconnect_request(const DisconnectRequest &msg) override; - void on_ping_request(const PingRequest &msg) override; - void on_device_info_request(const DeviceInfoRequest &msg) override; - void on_list_entities_request(const ListEntitiesRequest &msg) override; - void on_subscribe_states_request(const SubscribeStatesRequest &msg) override; - void on_subscribe_logs_request(const SubscribeLogsRequest &msg) override; -#ifdef USE_API_HOMEASSISTANT_SERVICES - void on_subscribe_homeassistant_services_request(const SubscribeHomeassistantServicesRequest &msg) override; -#endif -#ifdef USE_API_HOMEASSISTANT_STATES - void on_subscribe_home_assistant_states_request(const SubscribeHomeAssistantStatesRequest &msg) override; -#endif -#ifdef USE_API_USER_DEFINED_ACTIONS - void on_execute_service_request(const ExecuteServiceRequest &msg) override; -#endif -#ifdef USE_API_NOISE - void on_noise_encryption_set_key_request(const NoiseEncryptionSetKeyRequest &msg) override; -#endif -#ifdef USE_BUTTON - void on_button_command_request(const ButtonCommandRequest &msg) override; -#endif -#ifdef USE_CAMERA - void on_camera_image_request(const CameraImageRequest &msg) override; -#endif -#ifdef USE_CLIMATE - void on_climate_command_request(const ClimateCommandRequest &msg) override; -#endif -#ifdef USE_COVER - void on_cover_command_request(const CoverCommandRequest &msg) override; -#endif -#ifdef USE_DATETIME_DATE - void on_date_command_request(const DateCommandRequest &msg) override; -#endif -#ifdef USE_DATETIME_DATETIME - void on_date_time_command_request(const DateTimeCommandRequest &msg) override; -#endif -#ifdef USE_FAN - void on_fan_command_request(const FanCommandRequest &msg) override; -#endif -#ifdef USE_LIGHT - void on_light_command_request(const LightCommandRequest &msg) override; -#endif -#ifdef USE_LOCK - void on_lock_command_request(const LockCommandRequest &msg) override; -#endif -#ifdef USE_MEDIA_PLAYER - void on_media_player_command_request(const MediaPlayerCommandRequest &msg) override; -#endif -#ifdef USE_NUMBER - void on_number_command_request(const NumberCommandRequest &msg) override; -#endif -#ifdef USE_SELECT - void on_select_command_request(const SelectCommandRequest &msg) override; -#endif -#ifdef USE_SIREN - void on_siren_command_request(const SirenCommandRequest &msg) override; -#endif -#ifdef USE_SWITCH - void on_switch_command_request(const SwitchCommandRequest &msg) override; -#endif -#ifdef USE_TEXT - void on_text_command_request(const TextCommandRequest &msg) override; -#endif -#ifdef USE_DATETIME_TIME - void on_time_command_request(const TimeCommandRequest &msg) override; -#endif -#ifdef USE_UPDATE - void on_update_command_request(const UpdateCommandRequest &msg) override; -#endif -#ifdef USE_VALVE - void on_valve_command_request(const ValveCommandRequest &msg) override; -#endif -#ifdef USE_BLUETOOTH_PROXY - void on_subscribe_bluetooth_le_advertisements_request(const SubscribeBluetoothLEAdvertisementsRequest &msg) override; -#endif -#ifdef USE_BLUETOOTH_PROXY - void on_bluetooth_device_request(const BluetoothDeviceRequest &msg) override; -#endif -#ifdef USE_BLUETOOTH_PROXY - void on_bluetooth_gatt_get_services_request(const BluetoothGATTGetServicesRequest &msg) override; -#endif -#ifdef USE_BLUETOOTH_PROXY - void on_bluetooth_gatt_read_request(const BluetoothGATTReadRequest &msg) override; -#endif -#ifdef USE_BLUETOOTH_PROXY - void on_bluetooth_gatt_write_request(const BluetoothGATTWriteRequest &msg) override; -#endif -#ifdef USE_BLUETOOTH_PROXY - void on_bluetooth_gatt_read_descriptor_request(const BluetoothGATTReadDescriptorRequest &msg) override; -#endif -#ifdef USE_BLUETOOTH_PROXY - void on_bluetooth_gatt_write_descriptor_request(const BluetoothGATTWriteDescriptorRequest &msg) override; -#endif -#ifdef USE_BLUETOOTH_PROXY - void on_bluetooth_gatt_notify_request(const BluetoothGATTNotifyRequest &msg) override; -#endif -#ifdef USE_BLUETOOTH_PROXY - void on_subscribe_bluetooth_connections_free_request(const SubscribeBluetoothConnectionsFreeRequest &msg) override; -#endif -#ifdef USE_BLUETOOTH_PROXY - void on_unsubscribe_bluetooth_le_advertisements_request( - const UnsubscribeBluetoothLEAdvertisementsRequest &msg) override; -#endif -#ifdef USE_BLUETOOTH_PROXY - void on_bluetooth_scanner_set_mode_request(const BluetoothScannerSetModeRequest &msg) override; -#endif -#ifdef USE_VOICE_ASSISTANT - void on_subscribe_voice_assistant_request(const SubscribeVoiceAssistantRequest &msg) override; -#endif -#ifdef USE_VOICE_ASSISTANT - void on_voice_assistant_configuration_request(const VoiceAssistantConfigurationRequest &msg) override; -#endif -#ifdef USE_VOICE_ASSISTANT - void on_voice_assistant_set_configuration(const VoiceAssistantSetConfiguration &msg) override; -#endif -#ifdef USE_ALARM_CONTROL_PANEL - void on_alarm_control_panel_command_request(const AlarmControlPanelCommandRequest &msg) override; -#endif -#ifdef USE_ZWAVE_PROXY - void on_z_wave_proxy_frame(const ZWaveProxyFrame &msg) override; -#endif -#ifdef USE_ZWAVE_PROXY - void on_z_wave_proxy_request(const ZWaveProxyRequest &msg) override; -#endif -#ifdef USE_IR_RF - void on_infrared_rf_transmit_raw_timings_request(const InfraredRFTransmitRawTimingsRequest &msg) override; -#endif - void read_message(uint32_t msg_size, uint32_t msg_type, const uint8_t *msg_data) override; }; } // namespace esphome::api diff --git a/esphome/components/api/api_server.cpp b/esphome/components/api/api_server.cpp index ed97c3b9a2..d9c3cc6846 100644 --- a/esphome/components/api/api_server.cpp +++ b/esphome/components/api/api_server.cpp @@ -28,10 +28,12 @@ static const char *const TAG = "api"; // APIServer APIServer *global_api_server = nullptr; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) -APIServer::APIServer() { - global_api_server = this; - // Pre-allocate shared write buffer - shared_write_buffer_.reserve(64); +APIServer::APIServer() { global_api_server = this; } + +void APIServer::socket_failed_(const LogString *msg) { + ESP_LOGW(TAG, "Socket %s: errno %d", LOG_STR_ARG(msg), errno); + this->destroy_socket_(); + this->mark_failed(); } void APIServer::setup() { @@ -44,30 +46,26 @@ void APIServer::setup() { #ifndef USE_API_NOISE_PSK_FROM_YAML // Only load saved PSK if not set from YAML - SavedNoisePsk noise_pref_saved{}; - if (this->noise_pref_.load(&noise_pref_saved)) { + if (this->load_and_apply_noise_psk_()) { ESP_LOGD(TAG, "Loaded saved Noise PSK"); - this->set_noise_psk(noise_pref_saved.psk); } #endif #endif - this->socket_ = socket::socket_ip_loop_monitored(SOCK_STREAM, 0); // monitored for incoming connections + this->socket_ = socket::socket_ip_loop_monitored(SOCK_STREAM, 0).release(); // monitored for incoming connections if (this->socket_ == nullptr) { - ESP_LOGW(TAG, "Could not create socket"); - this->mark_failed(); + this->socket_failed_(LOG_STR("creation")); return; } int enable = 1; int err = this->socket_->setsockopt(SOL_SOCKET, SO_REUSEADDR, &enable, sizeof(int)); if (err != 0) { - ESP_LOGW(TAG, "Socket unable to set reuseaddr: errno %d", err); + ESP_LOGW(TAG, "Socket reuseaddr: errno %d", errno); // we can still continue } err = this->socket_->setblocking(false); if (err != 0) { - ESP_LOGW(TAG, "Socket unable to set nonblocking mode: errno %d", err); - this->mark_failed(); + this->socket_failed_(LOG_STR("nonblocking")); return; } @@ -75,28 +73,28 @@ void APIServer::setup() { socklen_t sl = socket::set_sockaddr_any((struct sockaddr *) &server, sizeof(server), this->port_); if (sl == 0) { - ESP_LOGW(TAG, "Socket unable to set sockaddr: errno %d", errno); - this->mark_failed(); + this->socket_failed_(LOG_STR("set sockaddr")); return; } err = this->socket_->bind((struct sockaddr *) &server, sl); if (err != 0) { - ESP_LOGW(TAG, "Socket unable to bind: errno %d", errno); - this->mark_failed(); + this->socket_failed_(LOG_STR("bind")); return; } err = this->socket_->listen(this->listen_backlog_); if (err != 0) { - ESP_LOGW(TAG, "Socket unable to listen: errno %d", errno); - this->mark_failed(); + this->socket_failed_(LOG_STR("listen")); return; } #ifdef USE_LOGGER if (logger::global_logger != nullptr) { - logger::global_logger->add_log_listener(this); + logger::global_logger->add_log_callback( + this, [](void *self, uint8_t level, const char *tag, const char *message, size_t message_len) { + static_cast(self)->on_log(level, tag, message, message_len); + }); } #endif @@ -110,44 +108,14 @@ void APIServer::setup() { this->last_connected_ = App.get_loop_component_start_time(); // Set warning status if reboot timeout is enabled if (this->reboot_timeout_ != 0) { - this->status_set_warning(); + this->status_set_warning(LOG_STR("waiting for client connection")); } } void APIServer::loop() { // Accept new clients only if the socket exists and has incoming connections if (this->socket_ && this->socket_->ready()) { - while (true) { - struct sockaddr_storage source_addr; - socklen_t addr_len = sizeof(source_addr); - - auto sock = this->socket_->accept_loop_monitored((struct sockaddr *) &source_addr, &addr_len); - if (!sock) - break; - - char peername[socket::SOCKADDR_STR_LEN]; - sock->getpeername_to(peername); - - // Check if we're at the connection limit - if (this->clients_.size() >= this->max_connections_) { - ESP_LOGW(TAG, "Max connections (%d), rejecting %s", this->max_connections_, peername); - // Immediately close - socket destructor will handle cleanup - sock.reset(); - continue; - } - - ESP_LOGD(TAG, "Accept %s", peername); - - auto *conn = new APIConnection(std::move(sock), this); - this->clients_.emplace_back(conn); - conn->start(); - - // First client connected - clear warning and update timestamp - if (this->clients_.size() == 1 && this->reboot_timeout_ != 0) { - this->status_clear_warning(); - this->last_connected_ = App.get_loop_component_start_time(); - } - } + this->accept_new_connections_(); } if (this->clients_.empty()) { @@ -178,42 +146,88 @@ void APIServer::loop() { while (client_index < this->clients_.size()) { auto &client = this->clients_[client_index]; + // Common case: process active client if (!client->flags_.remove) { - // Common case: process active client client->loop(); + } + // Handle disconnection promptly - close socket to free LWIP PCB + // resources and prevent retransmit crashes on ESP8266. + if (client->flags_.remove) { + // Rare case: handle disconnection (don't increment - swapped element needs processing) + this->remove_client_(client_index); + } else { client_index++; + } + } +} + +void APIServer::remove_client_(size_t client_index) { + auto &client = this->clients_[client_index]; + +#ifdef USE_API_USER_DEFINED_ACTION_RESPONSES + this->unregister_active_action_calls_for_connection(client.get()); +#endif + ESP_LOGV(TAG, "Remove connection %s", client->get_name()); + +#ifdef USE_API_CLIENT_DISCONNECTED_TRIGGER + // Save client info before closing socket and removal for the trigger + char peername_buf[socket::SOCKADDR_STR_LEN]; + std::string client_name(client->get_name()); + std::string client_peername(client->get_peername_to(peername_buf)); +#endif + + // Close socket now (was deferred from on_fatal_error to allow getpeername) + client->helper_->close(); + + // Swap with the last element and pop (avoids expensive vector shifts) + if (client_index < this->clients_.size() - 1) { + std::swap(this->clients_[client_index], this->clients_.back()); + } + this->clients_.pop_back(); + + // Last client disconnected - set warning and start tracking for reboot timeout + if (this->clients_.empty() && this->reboot_timeout_ != 0) { + this->status_set_warning(LOG_STR("waiting for client connection")); + this->last_connected_ = App.get_loop_component_start_time(); + } + +#ifdef USE_API_CLIENT_DISCONNECTED_TRIGGER + // Fire trigger after client is removed so api.connected reflects the true state + this->client_disconnected_trigger_.trigger(client_name, client_peername); +#endif +} + +void __attribute__((flatten)) APIServer::accept_new_connections_() { + while (true) { + struct sockaddr_storage source_addr; + socklen_t addr_len = sizeof(source_addr); + + auto sock = this->socket_->accept_loop_monitored((struct sockaddr *) &source_addr, &addr_len); + if (!sock) + break; + + char peername[socket::SOCKADDR_STR_LEN]; + sock->getpeername_to(peername); + + // Check if we're at the connection limit + if (this->clients_.size() >= this->max_connections_) { + ESP_LOGW(TAG, "Max connections (%d), rejecting %s", this->max_connections_, peername); + // Immediately close - socket destructor will handle cleanup + sock.reset(); continue; } - // Rare case: handle disconnection -#ifdef USE_API_USER_DEFINED_ACTION_RESPONSES - this->unregister_active_action_calls_for_connection(client.get()); -#endif - ESP_LOGV(TAG, "Remove connection %s", client->get_name()); + ESP_LOGD(TAG, "Accept %s", peername); -#ifdef USE_API_CLIENT_DISCONNECTED_TRIGGER - // Save client info before removal for the trigger - std::string client_name(client->get_name()); - std::string client_peername(client->get_peername()); -#endif + auto *conn = new APIConnection(std::move(sock), this); + this->clients_.emplace_back(conn); + conn->start(); - // Swap with the last element and pop (avoids expensive vector shifts) - if (client_index < this->clients_.size() - 1) { - std::swap(this->clients_[client_index], this->clients_.back()); - } - this->clients_.pop_back(); - - // Last client disconnected - set warning and start tracking for reboot timeout - if (this->clients_.empty() && this->reboot_timeout_ != 0) { - this->status_set_warning(); + // First client connected - clear warning and update timestamp + if (this->clients_.size() == 1 && this->reboot_timeout_ != 0) { + this->status_clear_warning(); this->last_connected_ = App.get_loop_component_start_time(); } - -#ifdef USE_API_CLIENT_DISCONNECTED_TRIGGER - // Fire trigger after client is removed so api.connected reflects the true state - this->client_disconnected_trigger_->trigger(client_name, client_peername); -#endif - // Don't increment client_index since we need to process the swapped element } } @@ -343,11 +357,11 @@ void APIServer::on_update(update::UpdateEntity *obj) { #endif #ifdef USE_ZWAVE_PROXY -void APIServer::on_zwave_proxy_request(const esphome::api::ProtoMessage &msg) { +void APIServer::on_zwave_proxy_request(const ZWaveProxyRequest &msg) { // We could add code to manage a second subscription type, but, since this message type is // very infrequent and small, we simply send it to all clients for (auto &c : this->clients_) - c->send_message(msg, api::ZWaveProxyRequest::MESSAGE_TYPE); + c->send_message(msg); } #endif @@ -417,8 +431,8 @@ void APIServer::handle_action_response(uint32_t call_id, bool success, StringRef #ifdef USE_API_HOMEASSISTANT_STATES // Helper to add subscription (reduces duplication) -void APIServer::add_state_subscription_(const char *entity_id, const char *attribute, std::function f, - bool once) { +void APIServer::add_state_subscription_(const char *entity_id, const char *attribute, + std::function &&f, bool once) { this->state_subs_.push_back(HomeAssistantStateSubscription{ .entity_id = entity_id, .attribute = attribute, .callback = std::move(f), .once = once, // entity_id_dynamic_storage and attribute_dynamic_storage remain nullptr (no heap allocation) @@ -427,7 +441,7 @@ void APIServer::add_state_subscription_(const char *entity_id, const char *attri // Helper to add subscription with heap-allocated strings (reduces duplication) void APIServer::add_state_subscription_(std::string entity_id, optional attribute, - std::function f, bool once) { + std::function &&f, bool once) { HomeAssistantStateSubscription sub; // Allocate heap storage for the strings sub.entity_id_dynamic_storage = std::make_unique(std::move(entity_id)); @@ -447,29 +461,29 @@ void APIServer::add_state_subscription_(std::string entity_id, optional f) { + std::function &&f) { this->add_state_subscription_(entity_id, attribute, std::move(f), false); } void APIServer::get_home_assistant_state(const char *entity_id, const char *attribute, - std::function f) { + std::function &&f) { this->add_state_subscription_(entity_id, attribute, std::move(f), true); } // std::string overload with StringRef callback (zero-allocation callback) void APIServer::subscribe_home_assistant_state(std::string entity_id, optional attribute, - std::function f) { + std::function &&f) { this->add_state_subscription_(std::move(entity_id), std::move(attribute), std::move(f), false); } void APIServer::get_home_assistant_state(std::string entity_id, optional attribute, - std::function f) { + std::function &&f) { this->add_state_subscription_(std::move(entity_id), std::move(attribute), std::move(f), true); } // Legacy helper: wraps std::string callback and delegates to StringRef version void APIServer::add_state_subscription_(std::string entity_id, optional attribute, - std::function f, bool once) { + std::function &&f, bool once) { // Wrap callback to convert StringRef -> std::string, then delegate this->add_state_subscription_(std::move(entity_id), std::move(attribute), std::function([f = std::move(f)](StringRef state) { f(state.str()); }), @@ -478,12 +492,12 @@ void APIServer::add_state_subscription_(std::string entity_id, optional attribute, - std::function f) { + std::function &&f) { this->add_state_subscription_(std::move(entity_id), std::move(attribute), std::move(f), false); } void APIServer::get_home_assistant_state(std::string entity_id, optional attribute, - std::function f) { + std::function &&f) { this->add_state_subscription_(std::move(entity_id), std::move(attribute), std::move(f), true); } @@ -498,7 +512,7 @@ void APIServer::set_reboot_timeout(uint32_t reboot_timeout) { this->reboot_timeo #ifdef USE_API_NOISE bool APIServer::update_noise_psk_(const SavedNoisePsk &new_psk, const LogString *save_log_msg, - const LogString *fail_log_msg, const psk_t &active_psk, bool make_active) { + const LogString *fail_log_msg, bool make_active) { if (!this->noise_pref_.save(&new_psk)) { ESP_LOGW(TAG, "%s", LOG_STR_ARG(fail_log_msg)); return false; @@ -510,18 +524,31 @@ bool APIServer::update_noise_psk_(const SavedNoisePsk &new_psk, const LogString } ESP_LOGD(TAG, "%s", LOG_STR_ARG(save_log_msg)); if (make_active) { - this->set_timeout(100, [this, active_psk]() { + this->set_timeout(100, [this]() { + // Re-read the PSK from preferences rather than capturing the 32-byte array + // in the lambda (which would exceed std::function SBO and heap-allocate). + if (!this->load_and_apply_noise_psk_()) { + ESP_LOGW(TAG, "Failed to load saved PSK for activation"); + return; + } ESP_LOGW(TAG, "Disconnecting all clients to reset PSK"); - this->set_noise_psk(active_psk); for (auto &c : this->clients_) { DisconnectRequest req; - c->send_message(req, DisconnectRequest::MESSAGE_TYPE); + c->send_message(req); } }); } return true; } +bool APIServer::load_and_apply_noise_psk_() { + SavedNoisePsk saved{}; + if (!this->noise_pref_.load(&saved)) + return false; + this->set_noise_psk(saved.psk); + return true; +} + bool APIServer::save_noise_psk(psk_t psk, bool make_active) { #ifdef USE_API_NOISE_PSK_FROM_YAML // When PSK is set from YAML, this function should never be called @@ -536,7 +563,7 @@ bool APIServer::save_noise_psk(psk_t psk, bool make_active) { } SavedNoisePsk new_saved_psk{psk}; - return this->update_noise_psk_(new_saved_psk, LOG_STR("Noise PSK saved"), LOG_STR("Failed to save Noise PSK"), psk, + return this->update_noise_psk_(new_saved_psk, LOG_STR("Noise PSK saved"), LOG_STR("Failed to save Noise PSK"), make_active); #endif } @@ -548,8 +575,7 @@ bool APIServer::clear_noise_psk(bool make_active) { return false; #else SavedNoisePsk empty_psk{}; - psk_t empty{}; - return this->update_noise_psk_(empty_psk, LOG_STR("Noise PSK cleared"), LOG_STR("Failed to clear Noise PSK"), empty, + return this->update_noise_psk_(empty_psk, LOG_STR("Noise PSK cleared"), LOG_STR("Failed to clear Noise PSK"), make_active); #endif } @@ -566,11 +592,7 @@ void APIServer::request_time() { } #endif -bool APIServer::is_connected(bool state_subscription_only) const { - if (!state_subscription_only) { - return !this->clients_.empty(); - } - +bool APIServer::is_connected_with_state_subscription() const { for (const auto &client : this->clients_) { if (client->flags_.state_subscription) { return true; @@ -607,10 +629,7 @@ void APIServer::on_shutdown() { this->shutting_down_ = true; // Close the listening socket to prevent new connections - if (this->socket_) { - this->socket_->close(); - this->socket_ = nullptr; - } + this->destroy_socket_(); // Change batch delay to 5ms for quick flushing during shutdown this->batch_delay_ = 5; @@ -618,7 +637,7 @@ void APIServer::on_shutdown() { // Send disconnect requests to all connected clients for (auto &c : this->clients_) { DisconnectRequest req; - if (!c->send_message(req, DisconnectRequest::MESSAGE_TYPE)) { + if (!c->send_message(req)) { // If we can't send the disconnect request directly (tx_buffer full), // schedule it at the front of the batch so it will be sent with priority c->schedule_message_front_(nullptr, DisconnectRequest::MESSAGE_TYPE, DisconnectRequest::ESTIMATED_SIZE); diff --git a/esphome/components/api/api_server.h b/esphome/components/api/api_server.h index 93421ef801..65076879a2 100644 --- a/esphome/components/api/api_server.h +++ b/esphome/components/api/api_server.h @@ -2,6 +2,7 @@ #include "esphome/core/defines.h" #ifdef USE_API +#include "api_buffer.h" #include "api_noise_context.h" #include "api_pb2.h" #include "api_pb2_service.h" @@ -35,15 +36,11 @@ struct SavedNoisePsk { } PACKED; // NOLINT #endif -class APIServer : public Component, - public Controller -#ifdef USE_LOGGER - , - public logger::LogListener -#endif +class APIServer final : public Component, + public Controller #ifdef USE_CAMERA , - public camera::CameraListener + public camera::CameraListener #endif { public: @@ -56,7 +53,7 @@ class APIServer : public Component, void on_shutdown() override; bool teardown() override; #ifdef USE_LOGGER - void on_log(uint8_t level, const char *tag, const char *message, size_t message_len) override; + void on_log(uint8_t level, const char *tag, const char *message, size_t message_len); #endif #ifdef USE_CAMERA void on_camera_image(const std::shared_ptr &image) override; @@ -69,7 +66,7 @@ class APIServer : public Component, void set_max_connections(uint8_t max_connections) { this->max_connections_ = max_connections; } // Get reference to shared buffer for API connections - std::vector &get_shared_buffer_ref() { return shared_write_buffer_; } + APIBuffer &get_shared_buffer_ref() { return shared_write_buffer_; } #ifdef USE_API_NOISE bool save_noise_psk(psk_t psk, bool make_active = true); @@ -183,13 +180,14 @@ class APIServer : public Component, void on_update(update::UpdateEntity *obj) override; #endif #ifdef USE_ZWAVE_PROXY - void on_zwave_proxy_request(const esphome::api::ProtoMessage &msg); + void on_zwave_proxy_request(const ZWaveProxyRequest &msg); #endif #ifdef USE_IR_RF void send_infrared_rf_receive_event(uint32_t device_id, uint32_t key, const std::vector *timings); #endif - bool is_connected(bool state_subscription_only = false) const; + bool is_connected() const { return !this->clients_.empty(); } + bool is_connected_with_state_subscription() const; #ifdef USE_API_HOMEASSISTANT_STATES struct HomeAssistantStateSubscription { @@ -205,20 +203,20 @@ class APIServer : public Component, }; // New const char* overload (for internal components - zero allocation) - void subscribe_home_assistant_state(const char *entity_id, const char *attribute, std::function f); - void get_home_assistant_state(const char *entity_id, const char *attribute, std::function f); + void subscribe_home_assistant_state(const char *entity_id, const char *attribute, std::function &&f); + void get_home_assistant_state(const char *entity_id, const char *attribute, std::function &&f); // std::string overload with StringRef callback (for custom_api_device.h with zero-allocation callback) void subscribe_home_assistant_state(std::string entity_id, optional attribute, - std::function f); + std::function &&f); void get_home_assistant_state(std::string entity_id, optional attribute, - std::function f); + std::function &&f); // Legacy std::string overload (for custom_api_device.h - converts StringRef to std::string for callback) void subscribe_home_assistant_state(std::string entity_id, optional attribute, - std::function f); + std::function &&f); void get_home_assistant_state(std::string entity_id, optional attribute, - std::function f); + std::function &&f); const std::vector &get_state_subs() const; #endif @@ -227,36 +225,48 @@ class APIServer : public Component, #endif #ifdef USE_API_CLIENT_CONNECTED_TRIGGER - Trigger *get_client_connected_trigger() const { return this->client_connected_trigger_; } + Trigger *get_client_connected_trigger() { return &this->client_connected_trigger_; } #endif #ifdef USE_API_CLIENT_DISCONNECTED_TRIGGER - Trigger *get_client_disconnected_trigger() const { - return this->client_disconnected_trigger_; - } + Trigger *get_client_disconnected_trigger() { return &this->client_disconnected_trigger_; } #endif protected: + // Accept incoming socket connections. Only called when socket has pending connections. + void __attribute__((noinline)) accept_new_connections_(); + // Remove a disconnected client by index. Swaps with last element and pops. + void __attribute__((noinline)) remove_client_(size_t client_index); + #ifdef USE_API_NOISE bool update_noise_psk_(const SavedNoisePsk &new_psk, const LogString *save_log_msg, const LogString *fail_log_msg, - const psk_t &active_psk, bool make_active); + bool make_active); + // Load saved PSK from preferences and apply it. Returns true on success. + bool load_and_apply_noise_psk_(); #endif // USE_API_NOISE #ifdef USE_API_HOMEASSISTANT_STATES // Helper methods to reduce code duplication - void add_state_subscription_(const char *entity_id, const char *attribute, std::function f, - bool once); - void add_state_subscription_(std::string entity_id, optional attribute, std::function f, + void add_state_subscription_(const char *entity_id, const char *attribute, std::function &&f, bool once); + void add_state_subscription_(std::string entity_id, optional attribute, + std::function &&f, bool once); // Legacy helper: wraps std::string callback and delegates to StringRef version void add_state_subscription_(std::string entity_id, optional attribute, - std::function f, bool once); + std::function &&f, bool once); #endif // USE_API_HOMEASSISTANT_STATES + // No explicit close() needed — listen sockets have no active connections on + // failure/shutdown. Destructor handles fd cleanup (close or abort per platform). + inline void destroy_socket_() { + delete this->socket_; + this->socket_ = nullptr; + } + void socket_failed_(const LogString *msg); // Pointers and pointer-like types first (4 bytes each) - std::unique_ptr socket_ = nullptr; + socket::ListenSocket *socket_{nullptr}; #ifdef USE_API_CLIENT_CONNECTED_TRIGGER - Trigger *client_connected_trigger_ = new Trigger(); + Trigger client_connected_trigger_; #endif #ifdef USE_API_CLIENT_DISCONNECTED_TRIGGER - Trigger *client_disconnected_trigger_ = new Trigger(); + Trigger client_disconnected_trigger_; #endif // 4-byte aligned types @@ -265,7 +275,11 @@ class APIServer : public Component, // Vectors and strings (12 bytes each on 32-bit) std::vector> clients_; - std::vector shared_write_buffer_; // Shared proto write buffer for all connections + // Shared proto write buffer for all connections. + // Not pre-allocated: all send paths call prepare_first_message_buffer() which + // reserves the exact needed size. Pre-allocating here would cause heap fragmentation + // since the buffer would almost always reallocate on first use. + APIBuffer shared_write_buffer_; #ifdef USE_API_HOMEASSISTANT_STATES std::vector state_subs_; #endif @@ -313,7 +327,10 @@ template class APIConnectedCondition : public Condition { TEMPLATABLE_VALUE(bool, state_subscription_only) public: bool check(const Ts &...x) override { - return global_api_server->is_connected(this->state_subscription_only_.value(x...)); + if (this->state_subscription_only_.value(x...)) { + return global_api_server->is_connected_with_state_subscription(); + } + return global_api_server->is_connected(); } }; diff --git a/esphome/components/api/client.py b/esphome/components/api/client.py index 200d0938bd..0c6c569c7d 100644 --- a/esphome/components/api/client.py +++ b/esphome/components/api/client.py @@ -2,6 +2,7 @@ from __future__ import annotations import asyncio from datetime import datetime +import importlib import logging from typing import TYPE_CHECKING, Any import warnings @@ -18,6 +19,7 @@ import contextlib from esphome.const import CONF_KEY, CONF_PORT, __version__ from esphome.core import CORE +from esphome.platformio_api import process_stacktrace from . import CONF_ENCRYPTION @@ -30,7 +32,11 @@ if TYPE_CHECKING: _LOGGER = logging.getLogger(__name__) -async def async_run_logs(config: dict[str, Any], addresses: list[str]) -> None: +async def async_run_logs( + config: dict[str, Any], + addresses: list[str], + subscribe_states: bool = True, +) -> None: """Run the logs command in the event loop.""" conf = config["api"] name = config["esphome"]["name"] @@ -55,9 +61,19 @@ async def async_run_logs(config: dict[str, Any], addresses: list[str]) -> None: addresses=addresses, # Pass all addresses for automatic retry ) dashboard = CORE.dashboard + backtrace_state = False + + # Try platform-specific stacktrace handler first, fall back to generic + platform_process_stacktrace = None + try: + module = importlib.import_module("esphome.components." + CORE.target_platform) + platform_process_stacktrace = getattr(module, "process_stacktrace") + except (AttributeError, ImportError): + pass def on_log(msg: SubscribeLogsResponse) -> None: """Handle a new log message.""" + nonlocal backtrace_state time_ = datetime.now() message: bytes = msg.message text = message.decode("utf8", "backslashreplace") @@ -67,15 +83,30 @@ async def async_run_logs(config: dict[str, Any], addresses: list[str]) -> None: ) for parsed_msg in parse_log_message(text, timestamp): print(parsed_msg.replace("\033", "\\033") if dashboard else parsed_msg) + for raw_line in text.splitlines(): + if platform_process_stacktrace: + backtrace_state = platform_process_stacktrace( + config, raw_line, backtrace_state + ) + else: + backtrace_state = process_stacktrace( + config, raw_line, backtrace_state=backtrace_state + ) - stop = await async_run(cli, on_log, name=name) + stop = await async_run(cli, on_log, name=name, subscribe_states=subscribe_states) try: await asyncio.Event().wait() finally: await stop() -def run_logs(config: dict[str, Any], addresses: list[str]) -> None: +def run_logs( + config: dict[str, Any], + addresses: list[str], + subscribe_states: bool = True, +) -> None: """Run the logs command.""" with contextlib.suppress(KeyboardInterrupt): - asyncio.run(async_run_logs(config, addresses)) + asyncio.run( + async_run_logs(config, addresses, subscribe_states=subscribe_states) + ) diff --git a/esphome/components/api/custom_api_device.h b/esphome/components/api/custom_api_device.h index 2fd9cb0dd2..c2b0f14bcc 100644 --- a/esphome/components/api/custom_api_device.h +++ b/esphome/components/api/custom_api_device.h @@ -136,8 +136,9 @@ class CustomAPIDevice { template void subscribe_homeassistant_state(void (T::*callback)(StringRef), const std::string &entity_id, const std::string &attribute = "") { - auto f = std::bind(callback, (T *) this, std::placeholders::_1); - global_api_server->subscribe_home_assistant_state(entity_id, optional(attribute), std::move(f)); + auto *obj = static_cast(this); + global_api_server->subscribe_home_assistant_state(entity_id, optional(attribute), + [obj, callback](StringRef state) { (obj->*callback)(state); }); } /** Subscribe to the state (or attribute state) of an entity from Home Assistant (legacy std::string version). @@ -148,10 +149,12 @@ class CustomAPIDevice { ESPDEPRECATED("Use void callback(StringRef) instead. Will be removed in 2027.1.0.", "2026.1.0") void subscribe_homeassistant_state(void (T::*callback)(std::string), const std::string &entity_id, const std::string &attribute = "") { - auto f = std::bind(callback, (T *) this, std::placeholders::_1); + auto *obj = static_cast(this); // Explicit type to disambiguate overload resolution - global_api_server->subscribe_home_assistant_state(entity_id, optional(attribute), - std::function(f)); + global_api_server->subscribe_home_assistant_state( + entity_id, optional(attribute), + std::function( + [obj, callback](const std::string &state) { (obj->*callback)(state); })); } /** Subscribe to the state (or attribute state) of an entity from Home Assistant. @@ -176,8 +179,10 @@ class CustomAPIDevice { template void subscribe_homeassistant_state(void (T::*callback)(const std::string &, StringRef), const std::string &entity_id, const std::string &attribute = "") { - auto f = std::bind(callback, (T *) this, entity_id, std::placeholders::_1); - global_api_server->subscribe_home_assistant_state(entity_id, optional(attribute), std::move(f)); + auto *obj = static_cast(this); + global_api_server->subscribe_home_assistant_state( + entity_id, optional(attribute), + [obj, callback, entity_id](StringRef state) { (obj->*callback)(entity_id, state); }); } /** Subscribe to the state (or attribute state) of an entity from Home Assistant (legacy std::string version). @@ -188,10 +193,12 @@ class CustomAPIDevice { ESPDEPRECATED("Use void callback(const std::string &, StringRef) instead. Will be removed in 2027.1.0.", "2026.1.0") void subscribe_homeassistant_state(void (T::*callback)(std::string, std::string), const std::string &entity_id, const std::string &attribute = "") { - auto f = std::bind(callback, (T *) this, entity_id, std::placeholders::_1); + auto *obj = static_cast(this); // Explicit type to disambiguate overload resolution - global_api_server->subscribe_home_assistant_state(entity_id, optional(attribute), - std::function(f)); + global_api_server->subscribe_home_assistant_state( + entity_id, optional(attribute), + std::function( + [obj, callback, entity_id](const std::string &state) { (obj->*callback)(entity_id, state); })); } #else template diff --git a/esphome/components/api/homeassistant_service.h b/esphome/components/api/homeassistant_service.h index 9bffe18764..9d14061d07 100644 --- a/esphome/components/api/homeassistant_service.h +++ b/esphome/components/api/homeassistant_service.h @@ -25,7 +25,9 @@ template class TemplatableStringValue : public TemplatableValue static std::string value_to_string(T &&val) { return to_string(std::forward(val)); } + template static std::string value_to_string(T &&val) { + return to_string(std::forward(val)); // NOLINT + } // Overloads for string types - needed because std::to_string doesn't support them static std::string value_to_string(char *val) { @@ -34,6 +36,8 @@ template class TemplatableStringValue : public TemplatableValue() {} @@ -126,6 +130,20 @@ template class HomeAssistantServiceCallAction : public Actionadd_kv_(this->variables_, key, std::forward(value)); } +#ifdef USE_ESP8266 + // On ESP8266, ESPHOME_F() returns __FlashStringHelper* (PROGMEM pointer). + // Store as const char* — populate_service_map copies from PROGMEM at play() time. + template void add_data(const __FlashStringHelper *key, V &&value) { + this->add_kv_(this->data_, reinterpret_cast(key), std::forward(value)); + } + template void add_data_template(const __FlashStringHelper *key, V &&value) { + this->add_kv_(this->data_template_, reinterpret_cast(key), std::forward(value)); + } + template void add_variable(const __FlashStringHelper *key, V &&value) { + this->add_kv_(this->variables_, reinterpret_cast(key), std::forward(value)); + } +#endif + #ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES template void set_response_template(T response_template) { this->response_template_ = response_template; @@ -136,12 +154,10 @@ template class HomeAssistantServiceCallAction : public Actionflags_.wants_response = true; } #ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES_JSON - Trigger *get_success_trigger_with_response() const { - return this->success_trigger_with_response_; - } + Trigger *get_success_trigger_with_response() { return &this->success_trigger_with_response_; } #endif - Trigger *get_success_trigger() const { return this->success_trigger_; } - Trigger *get_error_trigger() const { return this->error_trigger_; } + Trigger *get_success_trigger() { return &this->success_trigger_; } + Trigger *get_error_trigger() { return &this->error_trigger_; } #endif // USE_API_HOMEASSISTANT_ACTION_RESPONSES void play(const Ts &...x) override { @@ -187,14 +203,14 @@ template class HomeAssistantServiceCallAction : public Actionflags_.wants_response) { - this->success_trigger_with_response_->trigger(response.get_json(), args...); + this->success_trigger_with_response_.trigger(response.get_json(), args...); } else #endif { - this->success_trigger_->trigger(args...); + this->success_trigger_.trigger(args...); } } else { - this->error_trigger_->trigger(response.get_error_message(), args...); + this->error_trigger_.trigger(response.get_error_message(), args...); } }, captured_args); @@ -219,7 +235,32 @@ template class HomeAssistantServiceCallAction : public Action class HomeAssistantServiceCallAction : public Action class HomeAssistantServiceCallAction : public Action response_template_{""}; - Trigger *success_trigger_with_response_ = new Trigger(); + Trigger success_trigger_with_response_; #endif // USE_API_HOMEASSISTANT_ACTION_RESPONSES_JSON - Trigger *success_trigger_ = new Trigger(); - Trigger *error_trigger_ = new Trigger(); + Trigger success_trigger_; + Trigger error_trigger_; #endif // USE_API_HOMEASSISTANT_ACTION_RESPONSES struct Flags { diff --git a/esphome/components/api/list_entities.cpp b/esphome/components/api/list_entities.cpp index fe43a47c3b..0a94c1699b 100644 --- a/esphome/components/api/list_entities.cpp +++ b/esphome/components/api/list_entities.cpp @@ -94,7 +94,7 @@ ListEntitiesIterator::ListEntitiesIterator(APIConnection *client) : client_(clie #ifdef USE_API_USER_DEFINED_ACTIONS bool ListEntitiesIterator::on_service(UserServiceDescriptor *service) { auto resp = service->encode_list_service_response(); - return this->client_->send_message(resp, ListEntitiesServicesResponse::MESSAGE_TYPE); + return this->client_->send_message(resp); } #endif diff --git a/esphome/components/api/list_entities.h b/esphome/components/api/list_entities.h index bef36dd015..7d0eb5bb13 100644 --- a/esphome/components/api/list_entities.h +++ b/esphome/components/api/list_entities.h @@ -15,7 +15,7 @@ class APIConnection; return this->client_->schedule_message_(entity, ResponseType::MESSAGE_TYPE, ResponseType::ESTIMATED_SIZE); \ } -class ListEntitiesIterator : public ComponentIterator { +class ListEntitiesIterator final : public ComponentIterator { public: ListEntitiesIterator(APIConnection *client); #ifdef USE_BINARY_SENSOR @@ -94,7 +94,6 @@ class ListEntitiesIterator : public ComponentIterator { bool on_update(update::UpdateEntity *entity) override; #endif bool on_end() override; - bool completed() { return this->state_ == IteratorState::NONE; } protected: APIConnection *client_; diff --git a/esphome/components/api/proto.cpp b/esphome/components/api/proto.cpp index 2a0ddf91db..d9fe0fe461 100644 --- a/esphome/components/api/proto.cpp +++ b/esphome/components/api/proto.cpp @@ -1,5 +1,6 @@ #include "proto.h" #include +#include #include "esphome/core/helpers.h" #include "esphome/core/log.h" @@ -7,24 +8,71 @@ namespace esphome::api { static const char *const TAG = "api.proto"; +uint32_t ProtoSize::varint_slow(uint32_t value) { return varint_wide(value); } + +void ProtoWriteBuffer::encode_varint_raw_slow_(uint32_t value) { + do { + this->debug_check_bounds_(1); + *this->pos_++ = static_cast(value | 0x80); + value >>= 7; + } while (value > 0x7F); + this->debug_check_bounds_(1); + *this->pos_++ = static_cast(value); +} + +ProtoVarIntResult ProtoVarInt::parse_slow(const uint8_t *buffer, uint32_t len) { + // Multi-byte varint: first byte already checked to have high bit set + uint32_t result32 = buffer[0] & 0x7F; +#ifdef USE_API_VARINT64 + uint32_t limit = std::min(len, uint32_t(4)); +#else + uint32_t limit = std::min(len, uint32_t(5)); +#endif + for (uint32_t i = 1; i < limit; i++) { + uint8_t val = buffer[i]; + result32 |= uint32_t(val & 0x7F) << (i * 7); + if ((val & 0x80) == 0) { + return {result32, i + 1}; + } + } +#ifdef USE_API_VARINT64 + return parse_wide(buffer, len, result32); +#else + return {0, PROTO_VARINT_PARSE_FAILED}; +#endif +} + +#ifdef USE_API_VARINT64 +ProtoVarIntResult ProtoVarInt::parse_wide(const uint8_t *buffer, uint32_t len, uint32_t result32) { + uint64_t result64 = result32; + uint32_t limit = std::min(len, uint32_t(10)); + for (uint32_t i = 4; i < limit; i++) { + uint8_t val = buffer[i]; + result64 |= uint64_t(val & 0x7F) << (i * 7); + if ((val & 0x80) == 0) { + return {result64, i + 1}; + } + } + return {0, PROTO_VARINT_PARSE_FAILED}; +} +#endif + uint32_t ProtoDecodableMessage::count_repeated_field(const uint8_t *buffer, size_t length, uint32_t target_field_id) { uint32_t count = 0; const uint8_t *ptr = buffer; const uint8_t *end = buffer + length; while (ptr < end) { - uint32_t consumed; - - // Parse field header (tag) - auto res = ProtoVarInt::parse(ptr, end - ptr, &consumed); + // Parse field header (tag) - ptr < end guarantees len >= 1 + auto res = ProtoVarInt::parse_non_empty(ptr, end - ptr); if (!res.has_value()) { break; // Invalid data, stop counting } - uint32_t tag = res->as_uint32(); + uint32_t tag = static_cast(res.value); uint32_t field_type = tag & WIRE_TYPE_MASK; uint32_t field_id = tag >> 3; - ptr += consumed; + ptr += res.consumed; // Count if this is the target field if (field_id == target_field_id) { @@ -34,20 +82,20 @@ uint32_t ProtoDecodableMessage::count_repeated_field(const uint8_t *buffer, size // Skip field data based on wire type switch (field_type) { case WIRE_TYPE_VARINT: { // VarInt - parse and skip - res = ProtoVarInt::parse(ptr, end - ptr, &consumed); + res = ProtoVarInt::parse(ptr, end - ptr); if (!res.has_value()) { return count; // Invalid data, return what we have } - ptr += consumed; + ptr += res.consumed; break; } case WIRE_TYPE_LENGTH_DELIMITED: { // Length-delimited - parse length and skip data - res = ProtoVarInt::parse(ptr, end - ptr, &consumed); + res = ProtoVarInt::parse(ptr, end - ptr); if (!res.has_value()) { return count; } - uint32_t field_length = res->as_uint32(); - ptr += consumed; + uint32_t field_length = static_cast(res.value); + ptr += res.consumed; if (field_length > static_cast(end - ptr)) { return count; // Out of bounds } @@ -70,46 +118,130 @@ uint32_t ProtoDecodableMessage::count_repeated_field(const uint8_t *buffer, size return count; } +// Single-pass encode for repeated submessage elements (non-template core). +// Writes field tag, reserves 1 byte for length varint, encodes the submessage body, +// then backpatches the actual length. For the common case (body < 128 bytes), this is +// just a single byte write with no memmove — all current repeated submessage types +// (BLE advertisements at ~47B, GATT descriptors at ~24B, service args, etc.) take +// this fast path. +// +// The memmove fallback for body >= 128 bytes exists only for correctness (e.g., a GATT +// characteristic with many descriptors). It is safe because calculate_size() already +// reserved space for the full multi-byte varint — the shift fills that reserved space: +// +// calculate_size() allocates per element: tag + varint_size(body) + body_size +// +// After encode, before memmove (1 byte reserved, body written): +// [tag][__][body ..... body][??] +// ^ ^-- unused byte (v2 space from calculate_size) +// len_pos +// +// After memmove(body_start+1, body_start, body_size): +// [tag][__][__][body ..... body] +// ^ ^-- body shifted forward, fills v2 space exactly +// len_pos +// +// After writing 2-byte varint at len_pos: +// [tag][v1][v2][body ..... body] +// ^-- pos_ = element end, within buffer +void ProtoWriteBuffer::encode_sub_message(uint32_t field_id, const void *value, + void (*encode_fn)(const void *, ProtoWriteBuffer &)) { + this->encode_field_raw(field_id, 2); + // Reserve 1 byte for length varint (optimistic: submessage < 128 bytes) + uint8_t *len_pos = this->pos_; + this->debug_check_bounds_(1); + this->pos_++; + uint8_t *body_start = this->pos_; + encode_fn(value, *this); + uint32_t body_size = static_cast(this->pos_ - body_start); + if (body_size < 128) [[likely]] { + // Common case: 1-byte varint, just backpatch + *len_pos = static_cast(body_size); + return; + } + // Compute extra bytes needed for varint beyond the 1 already reserved + uint8_t extra = ProtoSize::varint(body_size) - 1; + // Shift body forward to make room for the extra varint bytes + this->debug_check_bounds_(extra); + std::memmove(body_start + extra, body_start, body_size); + uint8_t *end = this->pos_ + extra; + // Write the full varint at len_pos + this->pos_ = len_pos; + this->encode_varint_raw(body_size); + this->pos_ = end; +} + +// Non-template core for encode_optional_sub_message. +void ProtoWriteBuffer::encode_optional_sub_message(uint32_t field_id, uint32_t nested_size, const void *value, + void (*encode_fn)(const void *, ProtoWriteBuffer &)) { + if (nested_size == 0) + return; + this->encode_field_raw(field_id, 2); + this->encode_varint_raw(nested_size); +#ifdef ESPHOME_DEBUG_API + uint8_t *start = this->pos_; + encode_fn(value, *this); + if (static_cast(this->pos_ - start) != nested_size) + this->debug_check_encode_size_(field_id, nested_size, this->pos_ - start); +#else + encode_fn(value, *this); +#endif +} + +#ifdef ESPHOME_DEBUG_API +void ProtoWriteBuffer::debug_check_bounds_(size_t bytes, const char *caller) { + if (this->pos_ + bytes > this->buffer_->data() + this->buffer_->size()) { + ESP_LOGE(TAG, "ProtoWriteBuffer bounds check failed in %s: bytes=%zu offset=%td buf_size=%zu", caller, bytes, + this->pos_ - this->buffer_->data(), this->buffer_->size()); + abort(); + } +} +void ProtoWriteBuffer::debug_check_encode_size_(uint32_t field_id, uint32_t expected, ptrdiff_t actual) { + ESP_LOGE(TAG, "encode_message: size mismatch for field %" PRIu32 ": calculated=%" PRIu32 " actual=%td", field_id, + expected, actual); + abort(); +} +#endif + void ProtoDecodableMessage::decode(const uint8_t *buffer, size_t length) { const uint8_t *ptr = buffer; const uint8_t *end = buffer + length; while (ptr < end) { - uint32_t consumed; - - // Parse field header - auto res = ProtoVarInt::parse(ptr, end - ptr, &consumed); + // Parse field header - ptr < end guarantees len >= 1 + auto res = ProtoVarInt::parse_non_empty(ptr, end - ptr); if (!res.has_value()) { ESP_LOGV(TAG, "Invalid field start at offset %ld", (long) (ptr - buffer)); return; } - uint32_t tag = res->as_uint32(); + uint32_t tag = static_cast(res.value); uint32_t field_type = tag & WIRE_TYPE_MASK; uint32_t field_id = tag >> 3; - ptr += consumed; + ptr += res.consumed; switch (field_type) { case WIRE_TYPE_VARINT: { // VarInt - res = ProtoVarInt::parse(ptr, end - ptr, &consumed); + res = ProtoVarInt::parse(ptr, end - ptr); if (!res.has_value()) { ESP_LOGV(TAG, "Invalid VarInt at offset %ld", (long) (ptr - buffer)); return; } - if (!this->decode_varint(field_id, *res)) { - ESP_LOGV(TAG, "Cannot decode VarInt field %" PRIu32 " with value %" PRIu32 "!", field_id, res->as_uint32()); + if (!this->decode_varint(field_id, res.value)) { + ESP_LOGV(TAG, "Cannot decode VarInt field %" PRIu32 " with value %" PRIu64 "!", field_id, + static_cast(res.value)); } - ptr += consumed; + ptr += res.consumed; break; } case WIRE_TYPE_LENGTH_DELIMITED: { // Length-delimited - res = ProtoVarInt::parse(ptr, end - ptr, &consumed); + res = ProtoVarInt::parse(ptr, end - ptr); if (!res.has_value()) { ESP_LOGV(TAG, "Invalid Length Delimited at offset %ld", (long) (ptr - buffer)); return; } - uint32_t field_length = res->as_uint32(); - ptr += consumed; + uint32_t field_length = static_cast(res.value); + ptr += res.consumed; if (field_length > static_cast(end - ptr)) { ESP_LOGV(TAG, "Out-of-bounds Length Delimited at offset %ld", (long) (ptr - buffer)); return; @@ -125,7 +257,13 @@ void ProtoDecodableMessage::decode(const uint8_t *buffer, size_t length) { ESP_LOGV(TAG, "Out-of-bounds Fixed32-bit at offset %ld", (long) (ptr - buffer)); return; } - uint32_t val = encode_uint32(ptr[3], ptr[2], ptr[1], ptr[0]); + uint32_t val; +#if __BYTE_ORDER__ == __ORDER_LITTLE_ENDIAN__ + // Protobuf fixed32 is little-endian — direct load on LE platforms + memcpy(&val, ptr, 4); +#else + val = encode_uint32(ptr[3], ptr[2], ptr[1], ptr[0]); +#endif if (!this->decode_32bit(field_id, Proto32Bit(val))) { ESP_LOGV(TAG, "Cannot decode 32-bit field %" PRIu32 " with value %" PRIu32 "!", field_id, val); } @@ -133,7 +271,7 @@ void ProtoDecodableMessage::decode(const uint8_t *buffer, size_t length) { break; } default: - ESP_LOGV(TAG, "Invalid field type %u at offset %ld", field_type, (long) (ptr - buffer)); + ESP_LOGV(TAG, "Invalid field type %" PRIu32 " at offset %ld", field_type, (long) (ptr - buffer)); return; } } diff --git a/esphome/components/api/proto.h b/esphome/components/api/proto.h index 2e0df297c3..b629018a91 100644 --- a/esphome/components/api/proto.h +++ b/esphome/components/api/proto.h @@ -1,8 +1,11 @@ #pragma once +#include "api_pb2_defines.h" +#include "api_buffer.h" #include "esphome/core/component.h" #include "esphome/core/helpers.h" #include "esphome/core/log.h" +#include "esphome/core/progmem.h" #include "esphome/core/string_ref.h" #include @@ -57,6 +60,16 @@ inline uint16_t count_packed_varints(const uint8_t *data, size_t len) { return count; } +/// Encode a varint directly into a pre-allocated buffer. +/// Caller must ensure buffer has space (use ProtoSize::varint() to calculate). +inline void encode_varint_to_buffer(uint32_t val, uint8_t *buffer) { + while (val > 0x7F) { + *buffer++ = static_cast(val | 0x80); + val >>= 7; + } + *buffer = static_cast(val); +} + /* * StringRef Ownership Model for API Protocol Messages * =================================================== @@ -87,119 +100,60 @@ inline uint16_t count_packed_varints(const uint8_t *data, size_t len) { * within the same function scope where temporaries are created. */ -/// Representation of a VarInt - in ProtoBuf should be 64bit but we only use 32bit +/// Type used for decoded varint values - uint64_t when BLE needs 64-bit addresses, uint32_t otherwise +#ifdef USE_API_VARINT64 +using proto_varint_value_t = uint64_t; +#else +using proto_varint_value_t = uint32_t; +#endif + +/// Sentinel value for consumed field indicating parse failure +inline constexpr uint32_t PROTO_VARINT_PARSE_FAILED = 0; + +/// Result of parsing a varint: value + number of bytes consumed. +/// consumed == PROTO_VARINT_PARSE_FAILED indicates parse failure (not enough data or invalid). +struct ProtoVarIntResult { + proto_varint_value_t value; + uint32_t consumed; // PROTO_VARINT_PARSE_FAILED = parse failed + + constexpr bool has_value() const { return this->consumed != PROTO_VARINT_PARSE_FAILED; } +}; + +/// Static varint parsing methods for the protobuf wire format. class ProtoVarInt { public: - ProtoVarInt() : value_(0) {} - explicit ProtoVarInt(uint64_t value) : value_(value) {} - - static optional parse(const uint8_t *buffer, uint32_t len, uint32_t *consumed) { - if (len == 0) { - if (consumed != nullptr) - *consumed = 0; - return {}; - } - - // Most common case: single-byte varint (values 0-127) - if ((buffer[0] & 0x80) == 0) { - if (consumed != nullptr) - *consumed = 1; - return ProtoVarInt(buffer[0]); - } - - // General case for multi-byte varints - // Since we know buffer[0]'s high bit is set, initialize with its value - uint64_t result = buffer[0] & 0x7F; - uint8_t bitpos = 7; - - // Start from the second byte since we've already processed the first - for (uint32_t i = 1; i < len; i++) { - uint8_t val = buffer[i]; - result |= uint64_t(val & 0x7F) << uint64_t(bitpos); - bitpos += 7; - if ((val & 0x80) == 0) { - if (consumed != nullptr) - *consumed = i + 1; - return ProtoVarInt(result); - } - } - - if (consumed != nullptr) - *consumed = 0; - return {}; // Incomplete or invalid varint + /// Parse a varint from buffer. Caller must ensure len >= 1. + /// Returns result with consumed=0 on failure (truncated multi-byte varint). + static inline ProtoVarIntResult ESPHOME_ALWAYS_INLINE parse_non_empty(const uint8_t *buffer, uint32_t len) { +#ifdef ESPHOME_DEBUG_API + assert(len > 0); +#endif + // Fast path: single-byte varints (0-127) are the most common case + // (booleans, small enums, field tags, small message sizes/types). + if ((buffer[0] & 0x80) == 0) [[likely]] + return {buffer[0], 1}; + return parse_slow(buffer, len); } - constexpr uint16_t as_uint16() const { return this->value_; } - constexpr uint32_t as_uint32() const { return this->value_; } - constexpr uint64_t as_uint64() const { return this->value_; } - constexpr bool as_bool() const { return this->value_; } - constexpr int32_t as_int32() const { - // Not ZigZag encoded - return static_cast(this->as_int64()); - } - constexpr int64_t as_int64() const { - // Not ZigZag encoded - return static_cast(this->value_); - } - constexpr int32_t as_sint32() const { - // with ZigZag encoding - return decode_zigzag32(static_cast(this->value_)); - } - constexpr int64_t as_sint64() const { - // with ZigZag encoding - return decode_zigzag64(this->value_); - } - /** - * Encode the varint value to a pre-allocated buffer without bounds checking. - * - * @param buffer The pre-allocated buffer to write the encoded varint to - * @param len The size of the buffer in bytes - * - * @note The caller is responsible for ensuring the buffer is large enough - * to hold the encoded value. Use ProtoSize::varint() to calculate - * the exact size needed before calling this method. - * @note No bounds checking is performed for performance reasons. - */ - void encode_to_buffer_unchecked(uint8_t *buffer, size_t len) { - uint64_t val = this->value_; - if (val <= 0x7F) { - buffer[0] = val; - return; - } - size_t i = 0; - while (val && i < len) { - uint8_t temp = val & 0x7F; - val >>= 7; - if (val) { - buffer[i++] = temp | 0x80; - } else { - buffer[i++] = temp; - } - } - } - void encode(std::vector &out) { - uint64_t val = this->value_; - if (val <= 0x7F) { - out.push_back(val); - return; - } - while (val) { - uint8_t temp = val & 0x7F; - val >>= 7; - if (val) { - out.push_back(temp | 0x80); - } else { - out.push_back(temp); - } - } + /// Parse a varint from buffer (safe for empty buffers). + /// Returns result with consumed=0 on failure (empty buffer or truncated varint). + static inline ProtoVarIntResult ESPHOME_ALWAYS_INLINE parse(const uint8_t *buffer, uint32_t len) { + if (len == 0) + return {0, PROTO_VARINT_PARSE_FAILED}; + return parse_non_empty(buffer, len); } protected: - uint64_t value_; + // Slow path for multi-byte varints (>= 128), outlined to keep fast path small + static ProtoVarIntResult parse_slow(const uint8_t *buffer, uint32_t len) __attribute__((noinline)); + +#ifdef USE_API_VARINT64 + /// Continue parsing varint bytes 4-9 with 64-bit arithmetic. + static ProtoVarIntResult parse_wide(const uint8_t *buffer, uint32_t len, uint32_t result32) __attribute__((noinline)); +#endif }; -// Forward declarations for decode_to_message, encode_message and encode_packed_sint32 -class ProtoDecodableMessage; +// Forward declarations for encoding helpers class ProtoMessage; class ProtoSize; @@ -212,16 +166,9 @@ class ProtoLengthDelimited { const uint8_t *data() const { return this->value_; } size_t size() const { return this->length_; } - /** - * Decode the length-delimited data into an existing ProtoDecodableMessage instance. - * - * This method allows decoding without templates, enabling use in contexts - * where the message type is not known at compile time. The ProtoDecodableMessage's - * decode() method will be called with the raw data and length. - * - * @param msg The ProtoDecodableMessage instance to decode into - */ - void decode_to_message(ProtoDecodableMessage &msg) const; + /// Decode the length-delimited data into a message instance. + /// Template preserves concrete type so decode() resolves statically. + template void decode_to_message(T &msg) const; protected: const uint8_t *const value_; @@ -250,10 +197,25 @@ class Proto32Bit { class ProtoWriteBuffer { public: - ProtoWriteBuffer(std::vector *buffer) : buffer_(buffer) {} - void write(uint8_t value) { this->buffer_->push_back(value); } - void encode_varint_raw(ProtoVarInt value) { value.encode(*this->buffer_); } - void encode_varint_raw(uint32_t value) { this->encode_varint_raw(ProtoVarInt(value)); } + ProtoWriteBuffer(APIBuffer *buffer) : buffer_(buffer), pos_(buffer->data() + buffer->size()) {} + ProtoWriteBuffer(APIBuffer *buffer, size_t write_pos) : buffer_(buffer), pos_(buffer->data() + write_pos) {} + inline void ESPHOME_ALWAYS_INLINE encode_varint_raw(uint32_t value) { + if (value < 128) [[likely]] { + this->debug_check_bounds_(1); + *this->pos_++ = static_cast(value); + return; + } + this->encode_varint_raw_slow_(value); + } + void encode_varint_raw_64(uint64_t value) { + while (value > 0x7F) { + this->debug_check_bounds_(1); + *this->pos_++ = static_cast(value | 0x80); + value >>= 7; + } + this->debug_check_bounds_(1); + *this->pos_++ = static_cast(value); + } /** * Encode a field key (tag/wire type combination). * @@ -266,9 +228,32 @@ class ProtoWriteBuffer { * * Following https://protobuf.dev/programming-guides/encoding/#structure */ - void encode_field_raw(uint32_t field_id, uint32_t type) { - uint32_t val = (field_id << 3) | (type & WIRE_TYPE_MASK); - this->encode_varint_raw(val); + void encode_field_raw(uint32_t field_id, uint32_t type) { this->encode_varint_raw((field_id << 3) | type); } + /// Write a single precomputed tag byte. Tag must be < 128. + inline void write_raw_byte(uint8_t b) ESPHOME_ALWAYS_INLINE { + this->debug_check_bounds_(1); + *this->pos_++ = b; + } + /// Write raw bytes to the buffer (no tag, no length prefix). + inline void encode_raw(const void *data, size_t len) ESPHOME_ALWAYS_INLINE { + this->debug_check_bounds_(len); + std::memcpy(this->pos_, data, len); + this->pos_ += len; + } + /// Write a precomputed tag byte + 32-bit value in one operation. + /// Tag must be a single-byte varint (< 128). No zero check. + inline void write_tag_and_fixed32(uint8_t tag, uint32_t value) ESPHOME_ALWAYS_INLINE { + this->debug_check_bounds_(5); + this->pos_[0] = tag; +#if __BYTE_ORDER__ == __ORDER_LITTLE_ENDIAN__ + std::memcpy(this->pos_ + 1, &value, 4); +#else + this->pos_[1] = static_cast(value & 0xFF); + this->pos_[2] = static_cast((value >> 8) & 0xFF); + this->pos_[3] = static_cast((value >> 16) & 0xFF); + this->pos_[4] = static_cast((value >> 24) & 0xFF); +#endif + this->pos_ += 5; } void encode_string(uint32_t field_id, const char *string, size_t len, bool force = false) { if (len == 0 && !force) @@ -276,13 +261,11 @@ class ProtoWriteBuffer { this->encode_field_raw(field_id, 2); // type 2: Length-delimited string this->encode_varint_raw(len); - - // Using resize + memcpy instead of insert provides significant performance improvement: - // ~10-11x faster for 16-32 byte strings, ~3x faster for 64-byte strings - // as it avoids iterator checks and potential element moves that insert performs - size_t old_size = this->buffer_->size(); - this->buffer_->resize(old_size + len); - std::memcpy(this->buffer_->data() + old_size, string, len); + // Direct memcpy into pre-sized buffer — avoids push_back() per-byte capacity checks + // and vector::insert() iterator overhead. ~10-11x faster for 16-32 byte strings. + this->debug_check_bounds_(len); + std::memcpy(this->pos_, string, len); + this->pos_ += len; } void encode_string(uint32_t field_id, const std::string &value, bool force = false) { this->encode_string(field_id, value.data(), value.size(), force); @@ -303,23 +286,31 @@ class ProtoWriteBuffer { if (value == 0 && !force) return; this->encode_field_raw(field_id, 0); // type 0: Varint - uint64 - this->encode_varint_raw(ProtoVarInt(value)); + this->encode_varint_raw_64(value); } void encode_bool(uint32_t field_id, bool value, bool force = false) { if (!value && !force) return; this->encode_field_raw(field_id, 0); // type 0: Varint - bool - this->write(0x01); + this->debug_check_bounds_(1); + *this->pos_++ = value ? 0x01 : 0x00; } void encode_fixed32(uint32_t field_id, uint32_t value, bool force = false) { if (value == 0 && !force) return; this->encode_field_raw(field_id, 5); // type 5: 32-bit fixed32 - this->write((value >> 0) & 0xFF); - this->write((value >> 8) & 0xFF); - this->write((value >> 16) & 0xFF); - this->write((value >> 24) & 0xFF); + this->debug_check_bounds_(4); +#if __BYTE_ORDER__ == __ORDER_LITTLE_ENDIAN__ + // Protobuf fixed32 is little-endian, so direct copy works + std::memcpy(this->pos_, &value, 4); + this->pos_ += 4; +#else + *this->pos_++ = (value >> 0) & 0xFF; + *this->pos_++ = (value >> 8) & 0xFF; + *this->pos_++ = (value >> 16) & 0xFF; + *this->pos_++ = (value >> 24) & 0xFF; +#endif } // NOTE: Wire type 1 (64-bit fixed: double, fixed64, sfixed64) is intentionally // not supported to reduce overhead on embedded systems. All ESPHome devices are @@ -355,11 +346,33 @@ class ProtoWriteBuffer { } /// Encode a packed repeated sint32 field (zero-copy from vector) void encode_packed_sint32(uint32_t field_id, const std::vector &values); - void encode_message(uint32_t field_id, const ProtoMessage &value); - std::vector *get_buffer() const { return buffer_; } + /// Single-pass encode for repeated submessage elements. + /// Thin template wrapper; all buffer work is in the non-template core. + template void encode_sub_message(uint32_t field_id, const T &value); + /// Encode an optional singular submessage field — skips if empty. + /// Thin template wrapper; all buffer work is in the non-template core. + template void encode_optional_sub_message(uint32_t field_id, const T &value); + + // Non-template core for encode_sub_message — backpatch approach. + void encode_sub_message(uint32_t field_id, const void *value, void (*encode_fn)(const void *, ProtoWriteBuffer &)); + // Non-template core for encode_optional_sub_message. + void encode_optional_sub_message(uint32_t field_id, uint32_t nested_size, const void *value, + void (*encode_fn)(const void *, ProtoWriteBuffer &)); + APIBuffer *get_buffer() const { return buffer_; } protected: - std::vector *buffer_; + // Slow path for encode_varint_raw values >= 128, outlined to keep fast path small + void encode_varint_raw_slow_(uint32_t value) __attribute__((noinline)); + +#ifdef ESPHOME_DEBUG_API + void debug_check_bounds_(size_t bytes, const char *caller = __builtin_FUNCTION()); + void debug_check_encode_size_(uint32_t field_id, uint32_t expected, ptrdiff_t actual); +#else + void debug_check_bounds_([[maybe_unused]] size_t bytes) {} +#endif + + APIBuffer *buffer_; + uint8_t *pos_; }; #ifdef HAS_PROTO_MESSAGE_DUMP @@ -399,9 +412,40 @@ class DumpBuffer { return *this; } + /// Append a PROGMEM string (flash-safe on ESP8266, regular append on other platforms) + DumpBuffer &append_p(const char *str) { + if (str) { +#ifdef USE_ESP8266 + append_p_esp8266(str); +#else + append_impl_(str, strlen(str)); +#endif + } + return *this; + } + +#ifdef USE_ESP8266 + /// Out-of-line ESP8266 PROGMEM append to avoid inlining strlen_P/memcpy_P at every call site + void append_p_esp8266(const char *str); +#endif + const char *c_str() const { return buf_; } size_t size() const { return pos_; } + /// Get writable buffer pointer for use with buf_append_printf + char *data() { return buf_; } + /// Get current position for use with buf_append_printf + size_t pos() const { return pos_; } + /// Update position after buf_append_printf call + void set_pos(size_t pos) { + if (pos >= CAPACITY) { + pos_ = CAPACITY - 1; + } else { + pos_ = pos; + } + buf_[pos_] = '\0'; + } + private: void append_impl_(const char *str, size_t len) { size_t space = CAPACITY - 1 - pos_; @@ -421,21 +465,31 @@ class DumpBuffer { class ProtoMessage { public: - virtual ~ProtoMessage() = default; - // Default implementation for messages with no fields - virtual void encode(ProtoWriteBuffer buffer) const {} - // Default implementation for messages with no fields - virtual void calculate_size(ProtoSize &size) const {} + // Non-virtual defaults for messages with no fields. + // Concrete message classes hide these with their own implementations. + // All call sites use templates to preserve the concrete type, so virtual + // dispatch is not needed. This eliminates per-message vtable entries for + // encode/calculate_size, saving ~1.3 KB of flash across all message types. + void encode(ProtoWriteBuffer &buffer) const {} + uint32_t calculate_size() const { return 0; } #ifdef HAS_PROTO_MESSAGE_DUMP virtual const char *dump_to(DumpBuffer &out) const = 0; - virtual const char *message_name() const { return "unknown"; } + virtual const LogString *message_name() const { return LOG_STR("unknown"); } #endif + +#ifndef USE_HOST + protected: +#endif + // Non-virtual destructor is protected to prevent polymorphic deletion. + // On host platform, made public to allow value-initialization of std::array + // members (e.g. DeviceInfoResponse::devices) without clang errors. + ~ProtoMessage() = default; }; // Base class for messages that support decoding class ProtoDecodableMessage : public ProtoMessage { public: - virtual void decode(const uint8_t *buffer, size_t length); + void decode(const uint8_t *buffer, size_t length); /** * Count occurrences of a repeated field in a protobuf buffer. @@ -449,38 +503,20 @@ class ProtoDecodableMessage : public ProtoMessage { static uint32_t count_repeated_field(const uint8_t *buffer, size_t length, uint32_t target_field_id); protected: - virtual bool decode_varint(uint32_t field_id, ProtoVarInt value) { return false; } + ~ProtoDecodableMessage() = default; + virtual bool decode_varint(uint32_t field_id, proto_varint_value_t value) { return false; } virtual bool decode_length(uint32_t field_id, ProtoLengthDelimited value) { return false; } virtual bool decode_32bit(uint32_t field_id, Proto32Bit value) { return false; } // NOTE: decode_64bit removed - wire type 1 not supported }; class ProtoSize { - private: - uint32_t total_size_ = 0; - public: - /** - * @brief ProtoSize class for Protocol Buffer serialization size calculation - * - * This class provides methods to calculate the exact byte counts needed - * for encoding various Protocol Buffer field types. The class now uses an - * object-based approach to reduce parameter passing overhead while keeping - * varint calculation methods static for external use. - * - * Implements Protocol Buffer encoding size calculation according to: - * https://protobuf.dev/programming-guides/encoding/ - * - * Key features: - * - Object-based approach reduces flash usage by eliminating parameter passing - * - Early-return optimization for zero/default values - * - Static varint methods for external callers - * - Specialized handling for different field types according to protobuf spec - */ - - ProtoSize() = default; - - uint32_t get_size() const { return total_size_; } + // Varint encoding thresholds: values below each threshold fit in N bytes + static constexpr uint32_t VARINT_THRESHOLD_1_BYTE = 1 << 7; // 128 + static constexpr uint32_t VARINT_THRESHOLD_2_BYTE = 1 << 14; // 16384 + static constexpr uint32_t VARINT_THRESHOLD_3_BYTE = 1 << 21; // 2097152 + static constexpr uint32_t VARINT_THRESHOLD_4_BYTE = 1 << 28; // 268435456 /** * @brief Calculates the size in bytes needed to encode a uint32_t value as a varint @@ -488,24 +524,29 @@ class ProtoSize { * @param value The uint32_t value to calculate size for * @return The number of bytes needed to encode the value */ - static constexpr uint32_t varint(uint32_t value) { - // Optimized varint size calculation using leading zeros - // Each 7 bits requires one byte in the varint encoding - if (value < 128) - return 1; // 7 bits, common case for small values - - // For larger values, count bytes needed based on the position of the highest bit set - if (value < 16384) { - return 2; // 14 bits - } else if (value < 2097152) { - return 3; // 21 bits - } else if (value < 268435456) { - return 4; // 28 bits - } else { - return 5; // 32 bits (maximum for uint32_t) - } + static constexpr inline uint32_t ESPHOME_ALWAYS_INLINE varint(uint32_t value) { + if (value < VARINT_THRESHOLD_1_BYTE) [[likely]] + return 1; // Fast path: 7 bits, most common case + if (__builtin_is_constant_evaluated()) + return varint_wide(value); + return varint_slow(value); } + private: + // Slow path for varint >= 128, outlined to keep fast path small + static uint32_t varint_slow(uint32_t value) __attribute__((noinline)); + // Shared cascade for values >= 128 (used by both constexpr and noinline paths) + static constexpr inline uint32_t ESPHOME_ALWAYS_INLINE varint_wide(uint32_t value) { + if (value < VARINT_THRESHOLD_2_BYTE) + return 2; + if (value < VARINT_THRESHOLD_3_BYTE) + return 3; + if (value < VARINT_THRESHOLD_4_BYTE) + return 4; + return 5; + } + + public: /** * @brief Calculates the size in bytes needed to encode a uint64_t value as a varint * @@ -578,312 +619,78 @@ class ProtoSize { return varint(tag); } - /** - * @brief Common parameters for all add_*_field methods - * - * All add_*_field methods follow these common patterns: - * * @param field_id_size Pre-calculated size of the field ID in bytes - * @param value The value to calculate size for (type varies) - * @param force Whether to calculate size even if the value is default/zero/empty - * - * Each method follows this implementation pattern: - * 1. Skip calculation if value is default (0, false, empty) and not forced - * 2. Calculate the size based on the field's encoding rules - * 3. Add the field_id_size + calculated value size to total_size - */ - - /** - * @brief Calculates and adds the size of an int32 field to the total message size - */ - inline void add_int32(uint32_t field_id_size, int32_t value) { - if (value != 0) { - add_int32_force(field_id_size, value); - } + // Static methods that RETURN size contribution (no ProtoSize object needed). + // Used by generated calculate_size() methods to accumulate into a plain uint32_t register. + static constexpr uint32_t calc_int32(uint32_t field_id_size, int32_t value) { + return value ? field_id_size + (value < 0 ? 10 : varint(static_cast(value))) : 0; } - - /** - * @brief Calculates and adds the size of an int32 field to the total message size (force version) - */ - inline void add_int32_force(uint32_t field_id_size, int32_t value) { - // Always calculate size when forced - // Negative values are encoded as 10-byte varints in protobuf - total_size_ += field_id_size + (value < 0 ? 10 : varint(static_cast(value))); + static constexpr uint32_t calc_int32_force(uint32_t field_id_size, int32_t value) { + return field_id_size + (value < 0 ? 10 : varint(static_cast(value))); } - - /** - * @brief Calculates and adds the size of a uint32 field to the total message size - */ - inline void add_uint32(uint32_t field_id_size, uint32_t value) { - if (value != 0) { - add_uint32_force(field_id_size, value); - } + static constexpr uint32_t calc_uint32(uint32_t field_id_size, uint32_t value) { + return value ? field_id_size + varint(value) : 0; } - - /** - * @brief Calculates and adds the size of a uint32 field to the total message size (force version) - */ - inline void add_uint32_force(uint32_t field_id_size, uint32_t value) { - // Always calculate size when force is true - total_size_ += field_id_size + varint(value); + static constexpr uint32_t calc_uint32_force(uint32_t field_id_size, uint32_t value) { + return field_id_size + varint(value); } - - /** - * @brief Calculates and adds the size of a boolean field to the total message size - */ - inline void add_bool(uint32_t field_id_size, bool value) { - if (value) { - // Boolean fields always use 1 byte when true - total_size_ += field_id_size + 1; - } + static constexpr uint32_t calc_bool(uint32_t field_id_size, bool value) { return value ? field_id_size + 1 : 0; } + static constexpr uint32_t calc_bool_force(uint32_t field_id_size) { return field_id_size + 1; } + static constexpr uint32_t calc_float(uint32_t field_id_size, float value) { + return value != 0.0f ? field_id_size + 4 : 0; } - - /** - * @brief Calculates and adds the size of a boolean field to the total message size (force version) - */ - inline void add_bool_force(uint32_t field_id_size, bool value) { - // Always calculate size when force is true - // Boolean fields always use 1 byte - total_size_ += field_id_size + 1; + static constexpr uint32_t calc_fixed32(uint32_t field_id_size, uint32_t value) { + return value ? field_id_size + 4 : 0; } - - /** - * @brief Calculates and adds the size of a float field to the total message size - */ - inline void add_float(uint32_t field_id_size, float value) { - if (value != 0.0f) { - total_size_ += field_id_size + 4; - } + static constexpr uint32_t calc_sfixed32(uint32_t field_id_size, int32_t value) { + return value ? field_id_size + 4 : 0; } - - // NOTE: add_double_field removed - wire type 1 (64-bit: double) not supported - // to reduce overhead on embedded systems - - /** - * @brief Calculates and adds the size of a fixed32 field to the total message size - */ - inline void add_fixed32(uint32_t field_id_size, uint32_t value) { - if (value != 0) { - total_size_ += field_id_size + 4; - } + static constexpr uint32_t calc_sint32(uint32_t field_id_size, int32_t value) { + return value ? field_id_size + varint(encode_zigzag32(value)) : 0; } - - // NOTE: add_fixed64_field removed - wire type 1 (64-bit: fixed64) not supported - // to reduce overhead on embedded systems - - /** - * @brief Calculates and adds the size of a sfixed32 field to the total message size - */ - inline void add_sfixed32(uint32_t field_id_size, int32_t value) { - if (value != 0) { - total_size_ += field_id_size + 4; - } + static constexpr inline uint32_t ESPHOME_ALWAYS_INLINE calc_sint32_force(uint32_t field_id_size, int32_t value) { + return field_id_size + varint(encode_zigzag32(value)); } - - // NOTE: add_sfixed64_field removed - wire type 1 (64-bit: sfixed64) not supported - // to reduce overhead on embedded systems - - /** - * @brief Calculates and adds the size of a sint32 field to the total message size - * - * Sint32 fields use ZigZag encoding, which is more efficient for negative values. - */ - inline void add_sint32(uint32_t field_id_size, int32_t value) { - if (value != 0) { - add_sint32_force(field_id_size, value); - } + static constexpr uint32_t calc_int64(uint32_t field_id_size, int64_t value) { + return value ? field_id_size + varint(value) : 0; } - - /** - * @brief Calculates and adds the size of a sint32 field to the total message size (force version) - * - * Sint32 fields use ZigZag encoding, which is more efficient for negative values. - */ - inline void add_sint32_force(uint32_t field_id_size, int32_t value) { - // Always calculate size when force is true - // ZigZag encoding for sint32 - total_size_ += field_id_size + varint(encode_zigzag32(value)); + static constexpr uint32_t calc_int64_force(uint32_t field_id_size, int64_t value) { + return field_id_size + varint(value); } - - /** - * @brief Calculates and adds the size of an int64 field to the total message size - */ - inline void add_int64(uint32_t field_id_size, int64_t value) { - if (value != 0) { - add_int64_force(field_id_size, value); - } + static constexpr uint32_t calc_uint64(uint32_t field_id_size, uint64_t value) { + return value ? field_id_size + varint(value) : 0; } - - /** - * @brief Calculates and adds the size of an int64 field to the total message size (force version) - */ - inline void add_int64_force(uint32_t field_id_size, int64_t value) { - // Always calculate size when force is true - total_size_ += field_id_size + varint(value); + static constexpr inline uint32_t ESPHOME_ALWAYS_INLINE calc_uint64_force(uint32_t field_id_size, uint64_t value) { + return field_id_size + varint(value); } - - /** - * @brief Calculates and adds the size of a uint64 field to the total message size - */ - inline void add_uint64(uint32_t field_id_size, uint64_t value) { - if (value != 0) { - add_uint64_force(field_id_size, value); - } + static constexpr uint32_t calc_length(uint32_t field_id_size, size_t len) { + return len ? field_id_size + varint(static_cast(len)) + static_cast(len) : 0; } - - /** - * @brief Calculates and adds the size of a uint64 field to the total message size (force version) - */ - inline void add_uint64_force(uint32_t field_id_size, uint64_t value) { - // Always calculate size when force is true - total_size_ += field_id_size + varint(value); + static constexpr inline uint32_t ESPHOME_ALWAYS_INLINE calc_length_force(uint32_t field_id_size, size_t len) { + return field_id_size + varint(static_cast(len)) + static_cast(len); } - - // NOTE: sint64 support functions (add_sint64_field, add_sint64_field_force) removed - // sint64 type is not supported by ESPHome API to reduce overhead on embedded systems - - /** - * @brief Calculates and adds the size of a length-delimited field (string/bytes) to the total message size - */ - inline void add_length(uint32_t field_id_size, size_t len) { - if (len != 0) { - add_length_force(field_id_size, len); - } + static constexpr uint32_t calc_sint64(uint32_t field_id_size, int64_t value) { + return value ? field_id_size + varint(encode_zigzag64(value)) : 0; } - - /** - * @brief Calculates and adds the size of a length-delimited field (string/bytes) to the total message size (repeated - * field version) - */ - inline void add_length_force(uint32_t field_id_size, size_t len) { - // Always calculate size when force is true - // Field ID + length varint + data bytes - total_size_ += field_id_size + varint(static_cast(len)) + static_cast(len); + static constexpr uint32_t calc_sint64_force(uint32_t field_id_size, int64_t value) { + return field_id_size + varint(encode_zigzag64(value)); } - - /** - * @brief Adds a pre-calculated size directly to the total - * - * This is used when we can calculate the total size by multiplying the number - * of elements by the bytes per element (for repeated fixed-size types like float, fixed32, etc.) - * - * @param size The pre-calculated total size to add - */ - inline void add_precalculated_size(uint32_t size) { total_size_ += size; } - - /** - * @brief Calculates and adds the size of a nested message field to the total message size - * - * This helper function directly updates the total_size reference if the nested size - * is greater than zero. - * - * @param nested_size The pre-calculated size of the nested message - */ - inline void add_message_field(uint32_t field_id_size, uint32_t nested_size) { - if (nested_size != 0) { - add_message_field_force(field_id_size, nested_size); - } + static constexpr uint32_t calc_fixed64(uint32_t field_id_size, uint64_t value) { + return value ? field_id_size + 8 : 0; } - - /** - * @brief Calculates and adds the size of a nested message field to the total message size (force version) - * - * @param nested_size The pre-calculated size of the nested message - */ - inline void add_message_field_force(uint32_t field_id_size, uint32_t nested_size) { - // Always calculate size when force is true - // Field ID + length varint + nested message content - total_size_ += field_id_size + varint(nested_size) + nested_size; + static constexpr uint32_t calc_sfixed64(uint32_t field_id_size, int64_t value) { + return value ? field_id_size + 8 : 0; } - - /** - * @brief Calculates and adds the size of a nested message field to the total message size - * - * This version takes a ProtoMessage object, calculates its size internally, - * and updates the total_size reference. This eliminates the need for a temporary variable - * at the call site. - * - * @param message The nested message object - */ - inline void add_message_object(uint32_t field_id_size, const ProtoMessage &message) { - // Calculate nested message size by creating a temporary ProtoSize - ProtoSize nested_calc; - message.calculate_size(nested_calc); - uint32_t nested_size = nested_calc.get_size(); - - // Use the base implementation with the calculated nested_size - add_message_field(field_id_size, nested_size); + static constexpr uint32_t calc_message(uint32_t field_id_size, uint32_t nested_size) { + return nested_size ? field_id_size + varint(nested_size) + nested_size : 0; } - - /** - * @brief Calculates and adds the size of a nested message field to the total message size (force version) - * - * @param message The nested message object - */ - inline void add_message_object_force(uint32_t field_id_size, const ProtoMessage &message) { - // Calculate nested message size by creating a temporary ProtoSize - ProtoSize nested_calc; - message.calculate_size(nested_calc); - uint32_t nested_size = nested_calc.get_size(); - - // Use the base implementation with the calculated nested_size - add_message_field_force(field_id_size, nested_size); - } - - /** - * @brief Calculates and adds the sizes of all messages in a repeated field to the total message size - * - * This helper processes a vector of message objects, calculating the size for each message - * and adding it to the total size. - * - * @tparam MessageType The type of the nested messages in the vector - * @param messages Vector of message objects - */ - template - inline void add_repeated_message(uint32_t field_id_size, const std::vector &messages) { - // Skip if the vector is empty - if (!messages.empty()) { - // Use the force version for all messages in the repeated field - for (const auto &message : messages) { - add_message_object_force(field_id_size, message); - } - } - } - - /** - * @brief Calculates and adds the sizes of all messages in a repeated field to the total message size (FixedVector - * version) - * - * @tparam MessageType The type of the nested messages in the FixedVector - * @param messages FixedVector of message objects - */ - template - inline void add_repeated_message(uint32_t field_id_size, const FixedVector &messages) { - // Skip if the fixed vector is empty - if (!messages.empty()) { - // Use the force version for all messages in the repeated field - for (const auto &message : messages) { - add_message_object_force(field_id_size, message); - } - } - } - - /** - * @brief Calculate size of a packed repeated sint32 field - */ - inline void add_packed_sint32(uint32_t field_id_size, const std::vector &values) { - if (values.empty()) - return; - - size_t packed_size = 0; - for (int value : values) { - packed_size += varint(encode_zigzag32(value)); - } - - // field_id + length varint + packed data - total_size_ += field_id_size + varint(static_cast(packed_size)) + static_cast(packed_size); + static constexpr inline uint32_t ESPHOME_ALWAYS_INLINE calc_message_force(uint32_t field_id_size, + uint32_t nested_size) { + return field_id_size + varint(nested_size) + nested_size; } }; +// Implementation of methods that depend on ProtoSize being fully defined + // Implementation of encode_packed_sint32 - must be after ProtoSize is defined inline void ProtoWriteBuffer::encode_packed_sint32(uint32_t field_id, const std::vector &values) { if (values.empty()) @@ -903,83 +710,29 @@ inline void ProtoWriteBuffer::encode_packed_sint32(uint32_t field_id, const std: } } -// Implementation of encode_message - must be after ProtoMessage is defined -inline void ProtoWriteBuffer::encode_message(uint32_t field_id, const ProtoMessage &value) { - this->encode_field_raw(field_id, 2); // type 2: Length-delimited message - - // Calculate the message size first - ProtoSize msg_size; - value.calculate_size(msg_size); - uint32_t msg_length_bytes = msg_size.get_size(); - - // Calculate how many bytes the length varint needs - uint32_t varint_length_bytes = ProtoSize::varint(msg_length_bytes); - - // Reserve exact space for the length varint - size_t begin = this->buffer_->size(); - this->buffer_->resize(this->buffer_->size() + varint_length_bytes); - - // Write the length varint directly - ProtoVarInt(msg_length_bytes).encode_to_buffer_unchecked(this->buffer_->data() + begin, varint_length_bytes); - - // Now encode the message content - it will append to the buffer - value.encode(*this); - - // Verify that the encoded size matches what we calculated - assert(this->buffer_->size() == begin + varint_length_bytes + msg_length_bytes); +// Encode thunk — converts void* back to concrete type for direct encode() call +template void proto_encode_msg(const void *msg, ProtoWriteBuffer &buf) { + static_cast(msg)->encode(buf); } -// Implementation of decode_to_message - must be after ProtoDecodableMessage is defined -inline void ProtoLengthDelimited::decode_to_message(ProtoDecodableMessage &msg) const { +// Thin template wrapper; delegates to non-template core in proto.cpp. +template inline void ProtoWriteBuffer::encode_sub_message(uint32_t field_id, const T &value) { + this->encode_sub_message(field_id, &value, &proto_encode_msg); +} + +// Thin template wrapper; delegates to non-template core. +template inline void ProtoWriteBuffer::encode_optional_sub_message(uint32_t field_id, const T &value) { + this->encode_optional_sub_message(field_id, value.calculate_size(), &value, &proto_encode_msg); +} + +// Template decode_to_message - preserves concrete type so decode() resolves statically +template void ProtoLengthDelimited::decode_to_message(T &msg) const { msg.decode(this->value_, this->length_); } template const char *proto_enum_to_string(T value); -class ProtoService { - public: - protected: - virtual bool is_authenticated() = 0; - virtual bool is_connection_setup() = 0; - virtual void on_fatal_error() = 0; - virtual void on_no_setup_connection() = 0; - /** - * Create a buffer with a reserved size. - * @param reserve_size The number of bytes to pre-allocate in the buffer. This is a hint - * to optimize memory usage and avoid reallocations during encoding. - * Implementations should aim to allocate at least this size. - * @return A ProtoWriteBuffer object with the reserved size. - */ - virtual ProtoWriteBuffer create_buffer(uint32_t reserve_size) = 0; - virtual bool send_buffer(ProtoWriteBuffer buffer, uint8_t message_type) = 0; - virtual void read_message(uint32_t msg_size, uint32_t msg_type, const uint8_t *msg_data) = 0; - - // Optimized method that pre-allocates buffer based on message size - bool send_message_(const ProtoMessage &msg, uint8_t message_type) { - ProtoSize size; - msg.calculate_size(size); - uint32_t msg_size = size.get_size(); - - // Create a pre-sized buffer - auto buffer = this->create_buffer(msg_size); - - // Encode message into the buffer - msg.encode(buffer); - - // Send the buffer - return this->send_buffer(buffer, message_type); - } - - // Authentication helper methods - inline bool check_connection_setup_() { - if (!this->is_connection_setup()) { - this->on_no_setup_connection(); - return false; - } - return true; - } - - inline bool check_authenticated_() { return this->check_connection_setup_(); } -}; +// ProtoService removed — its methods were inlined into APIConnection. +// APIConnection is the concrete server-side implementation; the extra virtual layer was unnecessary. } // namespace esphome::api diff --git a/esphome/components/api/subscribe_state.h b/esphome/components/api/subscribe_state.h index 3c9f33835a..9edf0f0f0c 100644 --- a/esphome/components/api/subscribe_state.h +++ b/esphome/components/api/subscribe_state.h @@ -16,7 +16,7 @@ class APIConnection; return this->client_->send_##entity_type##_state(entity); \ } -class InitialStateIterator : public ComponentIterator { +class InitialStateIterator final : public ComponentIterator { public: InitialStateIterator(APIConnection *client); #ifdef USE_BINARY_SENSOR @@ -88,7 +88,6 @@ class InitialStateIterator : public ComponentIterator { #ifdef USE_UPDATE bool on_update(update::UpdateEntity *entity) override; #endif - bool completed() { return this->state_ == IteratorState::NONE; } protected: APIConnection *client_; diff --git a/esphome/components/api/user_services.cpp b/esphome/components/api/user_services.cpp index 9c2b4aa79a..28a43c656c 100644 --- a/esphome/components/api/user_services.cpp +++ b/esphome/components/api/user_services.cpp @@ -1,5 +1,6 @@ #include "user_services.h" #include "esphome/core/log.h" +#include "esphome/core/string_ref.h" namespace esphome::api { @@ -11,6 +12,8 @@ template<> int32_t get_execute_arg_value(const ExecuteServiceArgument & } template<> float get_execute_arg_value(const ExecuteServiceArgument &arg) { return arg.float_; } template<> std::string get_execute_arg_value(const ExecuteServiceArgument &arg) { return arg.string_; } +// Zero-copy StringRef version for YAML-generated services (string_ is null-terminated after decode) +template<> StringRef get_execute_arg_value(const ExecuteServiceArgument &arg) { return arg.string_; } // Legacy std::vector versions for external components using custom_api_device.h - optimized with reserve template<> std::vector get_execute_arg_value>(const ExecuteServiceArgument &arg) { @@ -61,6 +64,8 @@ template<> enums::ServiceArgType to_service_arg_type() { return enums::SER template<> enums::ServiceArgType to_service_arg_type() { return enums::SERVICE_ARG_TYPE_INT; } template<> enums::ServiceArgType to_service_arg_type() { return enums::SERVICE_ARG_TYPE_FLOAT; } template<> enums::ServiceArgType to_service_arg_type() { return enums::SERVICE_ARG_TYPE_STRING; } +// Zero-copy StringRef version for YAML-generated services +template<> enums::ServiceArgType to_service_arg_type() { return enums::SERVICE_ARG_TYPE_STRING; } // Legacy std::vector versions for external components using custom_api_device.h template<> enums::ServiceArgType to_service_arg_type>() { return enums::SERVICE_ARG_TYPE_BOOL_ARRAY; } diff --git a/esphome/components/api/user_services.h b/esphome/components/api/user_services.h index 85fba2a435..d1b8a6ef0d 100644 --- a/esphome/components/api/user_services.h +++ b/esphome/components/api/user_services.h @@ -230,7 +230,7 @@ template class APIRespondAction : public Action { void set_is_optional_mode(bool is_optional) { this->is_optional_mode_ = is_optional; } #ifdef USE_API_USER_DEFINED_ACTION_RESPONSES_JSON - void set_data(std::function func) { + void set_data(std::function &&func) { this->json_builder_ = std::move(func); this->has_data_ = true; } @@ -264,9 +264,9 @@ template class APIRespondAction : public Action { // Build and send JSON response json::JsonBuilder builder; this->json_builder_(x..., builder.root()); - std::string json_str = builder.serialize(); + auto json_buf = builder.serialize(); this->parent_->send_action_response(call_id, success, StringRef(error_message), - reinterpret_cast(json_str.data()), json_str.size()); + reinterpret_cast(json_buf.data()), json_buf.size()); return; } #endif diff --git a/esphome/components/aqi/aqi_calculator.h b/esphome/components/aqi/aqi_calculator.h index 993504c1e9..d624af0432 100644 --- a/esphome/components/aqi/aqi_calculator.h +++ b/esphome/components/aqi/aqi_calculator.h @@ -1,5 +1,6 @@ #pragma once +#include #include #include #include "abstract_aqi_calculator.h" @@ -14,7 +15,11 @@ class AQICalculator : public AbstractAQICalculator { float pm2_5_index = calculate_index(pm2_5_value, PM2_5_GRID); float pm10_0_index = calculate_index(pm10_0_value, PM10_0_GRID); - return static_cast(std::round((pm2_5_index < pm10_0_index) ? pm10_0_index : pm2_5_index)); + float aqi = std::max(pm2_5_index, pm10_0_index); + if (aqi < 0.0f) { + aqi = 0.0f; + } + return static_cast(std::lround(aqi)); } protected: @@ -22,13 +27,27 @@ class AQICalculator : public AbstractAQICalculator { static constexpr int INDEX_GRID[NUM_LEVELS][2] = {{0, 50}, {51, 100}, {101, 150}, {151, 200}, {201, 300}, {301, 500}}; - static constexpr float PM2_5_GRID[NUM_LEVELS][2] = {{0.0f, 9.0f}, {9.1f, 35.4f}, - {35.5f, 55.4f}, {55.5f, 125.4f}, - {125.5f, 225.4f}, {225.5f, std::numeric_limits::max()}}; + static constexpr float PM2_5_GRID[NUM_LEVELS][2] = { + // clang-format off + {0.0f, 9.1f}, + {9.1f, 35.5f}, + {35.5f, 55.5f}, + {55.5f, 125.5f}, + {125.5f, 225.5f}, + {225.5f, std::numeric_limits::max()} + // clang-format on + }; - static constexpr float PM10_0_GRID[NUM_LEVELS][2] = {{0.0f, 54.0f}, {55.0f, 154.0f}, - {155.0f, 254.0f}, {255.0f, 354.0f}, - {355.0f, 424.0f}, {425.0f, std::numeric_limits::max()}}; + static constexpr float PM10_0_GRID[NUM_LEVELS][2] = { + // clang-format off + {0.0f, 55.0f}, + {55.0f, 155.0f}, + {155.0f, 255.0f}, + {255.0f, 355.0f}, + {355.0f, 425.0f}, + {425.0f, std::numeric_limits::max()} + // clang-format on + }; static float calculate_index(float value, const float array[NUM_LEVELS][2]) { int grid_index = get_grid_index(value, array); @@ -45,7 +64,10 @@ class AQICalculator : public AbstractAQICalculator { static int get_grid_index(float value, const float array[NUM_LEVELS][2]) { for (int i = 0; i < NUM_LEVELS; i++) { - if (value >= array[i][0] && value <= array[i][1]) { + const bool in_range = + (value >= array[i][0]) && ((i == NUM_LEVELS - 1) ? (value <= array[i][1]) // last bucket inclusive + : (value < array[i][1])); // others exclusive on hi + if (in_range) { return i; } } diff --git a/esphome/components/aqi/aqi_sensor.h b/esphome/components/aqi/aqi_sensor.h index a990f815fe..2e526ca825 100644 --- a/esphome/components/aqi/aqi_sensor.h +++ b/esphome/components/aqi/aqi_sensor.h @@ -10,7 +10,6 @@ class AQISensor : public sensor::Sensor, public Component { public: void setup() override; void dump_config() override; - float get_setup_priority() const override { return setup_priority::DATA; } void set_pm_2_5_sensor(sensor::Sensor *sensor) { this->pm_2_5_sensor_ = sensor; } void set_pm_10_0_sensor(sensor::Sensor *sensor) { this->pm_10_0_sensor_ = sensor; } diff --git a/esphome/components/aqi/caqi_calculator.h b/esphome/components/aqi/caqi_calculator.h index d2ec4bb98f..fe2efe7059 100644 --- a/esphome/components/aqi/caqi_calculator.h +++ b/esphome/components/aqi/caqi_calculator.h @@ -1,5 +1,6 @@ #pragma once +#include #include #include #include "abstract_aqi_calculator.h" @@ -12,7 +13,11 @@ class CAQICalculator : public AbstractAQICalculator { float pm2_5_index = calculate_index(pm2_5_value, PM2_5_GRID); float pm10_0_index = calculate_index(pm10_0_value, PM10_0_GRID); - return static_cast(std::round((pm2_5_index < pm10_0_index) ? pm10_0_index : pm2_5_index)); + float aqi = std::max(pm2_5_index, pm10_0_index); + if (aqi < 0.0f) { + aqi = 0.0f; + } + return static_cast(std::lround(aqi)); } protected: @@ -21,10 +26,24 @@ class CAQICalculator : public AbstractAQICalculator { static constexpr int INDEX_GRID[NUM_LEVELS][2] = {{0, 25}, {26, 50}, {51, 75}, {76, 100}, {101, 400}}; static constexpr float PM2_5_GRID[NUM_LEVELS][2] = { - {0.0f, 15.0f}, {15.1f, 30.0f}, {30.1f, 55.0f}, {55.1f, 110.0f}, {110.1f, std::numeric_limits::max()}}; + // clang-format off + {0.0f, 15.1f}, + {15.1f, 30.1f}, + {30.1f, 55.1f}, + {55.1f, 110.1f}, + {110.1f, std::numeric_limits::max()} + // clang-format on + }; static constexpr float PM10_0_GRID[NUM_LEVELS][2] = { - {0.0f, 25.0f}, {25.1f, 50.0f}, {50.1f, 90.0f}, {90.1f, 180.0f}, {180.1f, std::numeric_limits::max()}}; + // clang-format off + {0.0f, 25.1f}, + {25.1f, 50.1f}, + {50.1f, 90.1f}, + {90.1f, 180.1f}, + {180.1f, std::numeric_limits::max()} + // clang-format on + }; static float calculate_index(float value, const float array[NUM_LEVELS][2]) { int grid_index = get_grid_index(value, array); @@ -42,7 +61,10 @@ class CAQICalculator : public AbstractAQICalculator { static int get_grid_index(float value, const float array[NUM_LEVELS][2]) { for (int i = 0; i < NUM_LEVELS; i++) { - if (value >= array[i][0] && value <= array[i][1]) { + const bool in_range = + (value >= array[i][0]) && ((i == NUM_LEVELS - 1) ? (value <= array[i][1]) // last bucket inclusive + : (value < array[i][1])); // others exclusive on hi + if (in_range) { return i; } } diff --git a/esphome/components/as3935/as3935.cpp b/esphome/components/as3935/as3935.cpp index 93a0bff5b3..c4dc0466a0 100644 --- a/esphome/components/as3935/as3935.cpp +++ b/esphome/components/as3935/as3935.cpp @@ -41,8 +41,6 @@ void AS3935Component::dump_config() { #endif } -float AS3935Component::get_setup_priority() const { return setup_priority::DATA; } - void AS3935Component::loop() { if (!this->irq_pin_->digital_read()) return; @@ -309,9 +307,9 @@ void AS3935Component::tune_antenna() { uint8_t tune_val = this->read_capacitance(); ESP_LOGI(TAG, "Starting antenna tuning\n" - "Division Ratio is set to: %d\n" - "Internal Capacitor is set to: %d\n" - "Displaying oscillator on INT pin. Measure its frequency - multiply value by Division Ratio", + " Division Ratio is set to: %d\n" + " Internal Capacitor is set to: %d\n" + " Displaying oscillator on INT pin. Measure its frequency - multiply value by Division Ratio", div_ratio, tune_val); this->display_oscillator(true, ANTFREQ); } diff --git a/esphome/components/as3935/as3935.h b/esphome/components/as3935/as3935.h index dc590c268e..5f46dadfa8 100644 --- a/esphome/components/as3935/as3935.h +++ b/esphome/components/as3935/as3935.h @@ -41,7 +41,7 @@ enum AS3935RegisterMasks { INT_MASK = 0xF0, THRESH_MASK = 0x0F, R_SPIKE_MASK = 0xF0, - ENERGY_MASK = 0xF0, + ENERGY_MASK = 0xE0, CAP_MASK = 0xF0, LIGHT_MASK = 0xCF, DISTURB_MASK = 0xDF, @@ -74,7 +74,6 @@ class AS3935Component : public Component { public: void setup() override; void dump_config() override; - float get_setup_priority() const override; void loop() override; void set_irq_pin(GPIOPin *irq_pin) { irq_pin_ = irq_pin; } diff --git a/esphome/components/as5600/__init__.py b/esphome/components/as5600/__init__.py index acb1c4d9db..b141329e94 100644 --- a/esphome/components/as5600/__init__.py +++ b/esphome/components/as5600/__init__.py @@ -9,6 +9,7 @@ from esphome.const import ( CONF_ID, CONF_POWER_MODE, CONF_RANGE, + CONF_WATCHDOG, ) CODEOWNERS = ["@ammmze"] @@ -57,7 +58,6 @@ FAST_FILTER = { CONF_RAW_ANGLE = "raw_angle" CONF_RAW_POSITION = "raw_position" -CONF_WATCHDOG = "watchdog" CONF_SLOW_FILTER = "slow_filter" CONF_FAST_FILTER = "fast_filter" CONF_START_POSITION = "start_position" diff --git a/esphome/components/as5600/sensor/__init__.py b/esphome/components/as5600/sensor/__init__.py index 1491852e07..cf67a3f203 100644 --- a/esphome/components/as5600/sensor/__init__.py +++ b/esphome/components/as5600/sensor/__init__.py @@ -2,11 +2,9 @@ import esphome.codegen as cg from esphome.components import sensor import esphome.config_validation as cv from esphome.const import ( - CONF_ANGLE, CONF_GAIN, CONF_ID, CONF_MAGNITUDE, - CONF_POSITION, CONF_STATUS, ENTITY_CATEGORY_DIAGNOSTIC, ICON_MAGNET, @@ -21,9 +19,7 @@ DEPENDENCIES = ["as5600"] AS5600Sensor = as5600_ns.class_("AS5600Sensor", sensor.Sensor, cg.PollingComponent) -CONF_RAW_ANGLE = "raw_angle" CONF_RAW_POSITION = "raw_position" -CONF_WATCHDOG = "watchdog" CONF_SLOW_FILTER = "slow_filter" CONF_FAST_FILTER = "fast_filter" CONF_PWM_FREQUENCY = "pwm_frequency" @@ -90,18 +86,6 @@ async def to_code(config): if out_of_range_mode_config := config.get(CONF_OUT_OF_RANGE_MODE): cg.add(var.set_out_of_range_mode(out_of_range_mode_config)) - if angle_config := config.get(CONF_ANGLE): - sens = await sensor.new_sensor(angle_config) - cg.add(var.set_angle_sensor(sens)) - - if raw_angle_config := config.get(CONF_RAW_ANGLE): - sens = await sensor.new_sensor(raw_angle_config) - cg.add(var.set_raw_angle_sensor(sens)) - - if position_config := config.get(CONF_POSITION): - sens = await sensor.new_sensor(position_config) - cg.add(var.set_position_sensor(sens)) - if raw_position_config := config.get(CONF_RAW_POSITION): sens = await sensor.new_sensor(raw_position_config) cg.add(var.set_raw_position_sensor(sens)) diff --git a/esphome/components/as5600/sensor/as5600_sensor.cpp b/esphome/components/as5600/sensor/as5600_sensor.cpp index feb8f6cebf..4e549d24d5 100644 --- a/esphome/components/as5600/sensor/as5600_sensor.cpp +++ b/esphome/components/as5600/sensor/as5600_sensor.cpp @@ -22,32 +22,13 @@ static const uint8_t REGISTER_STATUS = 0x0B; // 8 bytes / R static const uint8_t REGISTER_AGC = 0x1A; // 8 bytes / R static const uint8_t REGISTER_MAGNITUDE = 0x1B; // 16 bytes / R -float AS5600Sensor::get_setup_priority() const { return setup_priority::DATA; } - void AS5600Sensor::dump_config() { LOG_SENSOR("", "AS5600 Sensor", this); ESP_LOGCONFIG(TAG, " Out of Range Mode: %u", this->out_of_range_mode_); - if (this->angle_sensor_ != nullptr) { - LOG_SENSOR(" ", "Angle Sensor", this->angle_sensor_); - } - if (this->raw_angle_sensor_ != nullptr) { - LOG_SENSOR(" ", "Raw Angle Sensor", this->raw_angle_sensor_); - } - if (this->position_sensor_ != nullptr) { - LOG_SENSOR(" ", "Position Sensor", this->position_sensor_); - } - if (this->raw_position_sensor_ != nullptr) { - LOG_SENSOR(" ", "Raw Position Sensor", this->raw_position_sensor_); - } - if (this->gain_sensor_ != nullptr) { - LOG_SENSOR(" ", "Gain Sensor", this->gain_sensor_); - } - if (this->magnitude_sensor_ != nullptr) { - LOG_SENSOR(" ", "Magnitude Sensor", this->magnitude_sensor_); - } - if (this->status_sensor_ != nullptr) { - LOG_SENSOR(" ", "Status Sensor", this->status_sensor_); - } + LOG_SENSOR(" ", "Raw Position Sensor", this->raw_position_sensor_); + LOG_SENSOR(" ", "Gain Sensor", this->gain_sensor_); + LOG_SENSOR(" ", "Magnitude Sensor", this->magnitude_sensor_); + LOG_SENSOR(" ", "Status Sensor", this->status_sensor_); LOG_UPDATE_INTERVAL(this); } diff --git a/esphome/components/as5600/sensor/as5600_sensor.h b/esphome/components/as5600/sensor/as5600_sensor.h index 0af9b01ae5..77593f4b12 100644 --- a/esphome/components/as5600/sensor/as5600_sensor.h +++ b/esphome/components/as5600/sensor/as5600_sensor.h @@ -14,11 +14,7 @@ class AS5600Sensor : public PollingComponent, public Parented, public: void update() override; void dump_config() override; - float get_setup_priority() const override; - void set_angle_sensor(sensor::Sensor *angle_sensor) { this->angle_sensor_ = angle_sensor; } - void set_raw_angle_sensor(sensor::Sensor *raw_angle_sensor) { this->raw_angle_sensor_ = raw_angle_sensor; } - void set_position_sensor(sensor::Sensor *position_sensor) { this->position_sensor_ = position_sensor; } void set_raw_position_sensor(sensor::Sensor *raw_position_sensor) { this->raw_position_sensor_ = raw_position_sensor; } @@ -29,9 +25,6 @@ class AS5600Sensor : public PollingComponent, public Parented, OutRangeMode get_out_of_range_mode() { return this->out_of_range_mode_; } protected: - sensor::Sensor *angle_sensor_{nullptr}; - sensor::Sensor *raw_angle_sensor_{nullptr}; - sensor::Sensor *position_sensor_{nullptr}; sensor::Sensor *raw_position_sensor_{nullptr}; sensor::Sensor *gain_sensor_{nullptr}; sensor::Sensor *magnitude_sensor_{nullptr}; diff --git a/esphome/components/as7341/as7341.cpp b/esphome/components/as7341/as7341.cpp index 893eaa850f..1e78d814c8 100644 --- a/esphome/components/as7341/as7341.cpp +++ b/esphome/components/as7341/as7341.cpp @@ -58,8 +58,6 @@ void AS7341Component::dump_config() { LOG_SENSOR(" ", "NIR", this->nir_); } -float AS7341Component::get_setup_priority() const { return setup_priority::DATA; } - void AS7341Component::update() { this->read_channels(this->channel_readings_); diff --git a/esphome/components/as7341/as7341.h b/esphome/components/as7341/as7341.h index aed7996cef..3ede9d4aa4 100644 --- a/esphome/components/as7341/as7341.h +++ b/esphome/components/as7341/as7341.h @@ -78,7 +78,6 @@ class AS7341Component : public PollingComponent, public i2c::I2CDevice { public: void setup() override; void dump_config() override; - float get_setup_priority() const override; void update() override; void set_f1_sensor(sensor::Sensor *f1_sensor) { this->f1_ = f1_sensor; } diff --git a/esphome/components/async_tcp/async_tcp_socket.cpp b/esphome/components/async_tcp/async_tcp_socket.cpp index f64e494f5f..e8c0f163b3 100644 --- a/esphome/components/async_tcp/async_tcp_socket.cpp +++ b/esphome/components/async_tcp/async_tcp_socket.cpp @@ -52,11 +52,12 @@ bool AsyncClient::connect(const char *host, uint16_t port) { connect_cb_(connect_arg_, this); return true; } - if (errno != EINPROGRESS) { - ESP_LOGE(TAG, "Connect failed: %d", errno); + const int saved_errno = errno; + if (saved_errno != EINPROGRESS) { + ESP_LOGE(TAG, "Connect failed: %d", saved_errno); close(); if (error_cb_) - error_cb_(error_arg_, this, errno); + error_cb_(error_arg_, this, saved_errno); return false; } @@ -79,11 +80,12 @@ size_t AsyncClient::write(const char *data, size_t len) { ssize_t sent = socket_->write(data, len); if (sent < 0) { - if (errno != EAGAIN && errno != EWOULDBLOCK) { - ESP_LOGE(TAG, "Write error: %d", errno); + const int err = errno; + if (err != EAGAIN && err != EWOULDBLOCK) { + ESP_LOGE(TAG, "Write error: %d", err); close(); if (error_cb_) - error_cb_(error_arg_, this, errno); + error_cb_(error_arg_, this, err); } return 0; } @@ -129,10 +131,11 @@ void AsyncClient::loop() { error_cb_(error_arg_, this, error); } } else if (ret < 0) { - ESP_LOGE(TAG, "Select error: %d", errno); + const int err = errno; + ESP_LOGE(TAG, "Select error: %d", err); close(); if (error_cb_) - error_cb_(error_arg_, this, errno); + error_cb_(error_arg_, this, err); } } else if (connected_) { // For connected sockets, use the Application's select() results @@ -148,11 +151,14 @@ void AsyncClient::loop() { } else if (len > 0) { if (data_cb_) data_cb_(data_arg_, this, buf, len); - } else if (errno != EAGAIN && errno != EWOULDBLOCK) { - ESP_LOGW(TAG, "Read error: %d", errno); - close(); - if (error_cb_) - error_cb_(error_arg_, this, errno); + } else { + const int err = errno; + if (err != EAGAIN && err != EWOULDBLOCK) { + ESP_LOGW(TAG, "Read error: %d", err); + close(); + if (error_cb_) + error_cb_(error_arg_, this, err); + } } } } diff --git a/esphome/components/at581x/__init__.py b/esphome/components/at581x/__init__.py index 117ada123d..34e6570628 100644 --- a/esphome/components/at581x/__init__.py +++ b/esphome/components/at581x/__init__.py @@ -89,6 +89,7 @@ AT581XSettingsAction = at581x_ns.class_("AT581XSettingsAction", automation.Actio cv.Required(CONF_ID): cv.use_id(AT581XComponent), } ), + synchronous=True, ) async def at581x_reset_to_code(config, action_id, template_arg, args): var = cg.new_Pvariable(action_id, template_arg) @@ -160,6 +161,7 @@ RADAR_SETTINGS_SCHEMA = cv.Schema( "at581x.settings", AT581XSettingsAction, RADAR_SETTINGS_SCHEMA, + synchronous=True, ) async def at581x_settings_to_code(config, action_id, template_arg, args): var = cg.new_Pvariable(action_id, template_arg) @@ -175,7 +177,7 @@ async def at581x_settings_to_code(config, action_id, template_arg, args): template_ = int(template_ / 1000000) cg.add(var.set_frequency(template_)) - if sens_dist := config.get(CONF_SENSING_DISTANCE): + if (sens_dist := config.get(CONF_SENSING_DISTANCE)) is not None: template_ = await cg.templatable(sens_dist, args, int) cg.add(var.set_sensing_distance(template_)) @@ -207,7 +209,7 @@ async def at581x_settings_to_code(config, action_id, template_arg, args): template_ = int(template_) cg.add(var.set_trigger_keep(template_)) - if stage_gain := config.get(CONF_STAGE_GAIN): + if (stage_gain := config.get(CONF_STAGE_GAIN)) is not None: template_ = await cg.templatable(stage_gain, args, int) cg.add(var.set_stage_gain(template_)) diff --git a/esphome/components/at581x/at581x.cpp b/esphome/components/at581x/at581x.cpp index 6804a7f4b5..6fc85b0790 100644 --- a/esphome/components/at581x/at581x.cpp +++ b/esphome/components/at581x/at581x.cpp @@ -77,14 +77,14 @@ void AT581XComponent::dump_config() { LOG_I2C_DEVICE(this); } bool AT581XComponent::i2c_write_config() { ESP_LOGCONFIG(TAG, "Writing new config for AT581X\n" - "Frequency: %dMHz\n" - "Sensing distance: %d\n" - "Power: %dµA\n" - "Gain: %d\n" - "Trigger base time: %dms\n" - "Trigger keep time: %dms\n" - "Protect time: %dms\n" - "Self check time: %dms", + " Frequency: %dMHz\n" + " Sensing distance: %d\n" + " Power: %dµA\n" + " Gain: %d\n" + " Trigger base time: %dms\n" + " Trigger keep time: %dms\n" + " Protect time: %dms\n" + " Self check time: %dms", this->freq_, this->delta_, this->power_, this->gain_, this->trigger_base_time_ms_, this->trigger_keep_time_ms_, this->protect_time_ms_, this->self_check_time_ms_); @@ -135,6 +135,11 @@ bool AT581XComponent::i2c_write_config() { } // Set gain + if (this->gain_ < 0 || static_cast(this->gain_) >= ARRAY_SIZE(GAIN5C_TABLE) || + static_cast(this->gain_ >> 1) >= ARRAY_SIZE(GAIN63_TABLE)) { + ESP_LOGE(TAG, "AT581X gain index out of range: %d", this->gain_); + return false; + } if (!this->i2c_write_reg(GAIN_ADDR_TABLE[0], GAIN5C_TABLE[this->gain_]) || !this->i2c_write_reg(GAIN_ADDR_TABLE[1], GAIN63_TABLE[this->gain_ >> 1])) { ESP_LOGE(TAG, "Failed to write AT581X gain registers"); diff --git a/esphome/components/atc_mithermometer/atc_mithermometer.cpp b/esphome/components/atc_mithermometer/atc_mithermometer.cpp index b4d2929742..9afd6334f5 100644 --- a/esphome/components/atc_mithermometer/atc_mithermometer.cpp +++ b/esphome/components/atc_mithermometer/atc_mithermometer.cpp @@ -61,6 +61,10 @@ optional ATCMiThermometer::parse_header_(const esp32_ble_tracker::S } auto raw = service_data.data; + if (raw.size() < 13) { + ESP_LOGVV(TAG, "parse_header_(): service data too short (%zu).", raw.size()); + return {}; + } static uint8_t last_frame_count = 0; if (last_frame_count == raw[12]) { diff --git a/esphome/components/atm90e26/atm90e26.cpp b/esphome/components/atm90e26/atm90e26.cpp index cadc06ac6b..e6602411bb 100644 --- a/esphome/components/atm90e26/atm90e26.cpp +++ b/esphome/components/atm90e26/atm90e26.cpp @@ -146,7 +146,6 @@ void ATM90E26Component::dump_config() { LOG_SENSOR(" ", "Active Reverse Energy A", this->reverse_active_energy_sensor_); LOG_SENSOR(" ", "Frequency", this->freq_sensor_); } -float ATM90E26Component::get_setup_priority() const { return setup_priority::DATA; } uint16_t ATM90E26Component::read16_(uint8_t a_register) { uint8_t data[2]; @@ -198,7 +197,7 @@ float ATM90E26Component::get_reactive_power_() { float ATM90E26Component::get_power_factor_() { const uint16_t val = this->read16_(ATM90E26_REGISTER_POWERF); // signed if (val & 0x8000) { - return -(val & 0x7FF) / 1000.0f; + return -(val & 0x7FFF) / 1000.0f; } else { return val / 1000.0f; } diff --git a/esphome/components/atm90e26/atm90e26.h b/esphome/components/atm90e26/atm90e26.h index 3c098d7e91..d15a53ea43 100644 --- a/esphome/components/atm90e26/atm90e26.h +++ b/esphome/components/atm90e26/atm90e26.h @@ -13,7 +13,6 @@ class ATM90E26Component : public PollingComponent, public: void setup() override; void dump_config() override; - float get_setup_priority() const override; void update() override; void set_voltage_sensor(sensor::Sensor *obj) { this->voltage_sensor_ = obj; } diff --git a/esphome/components/atm90e32/atm90e32.cpp b/esphome/components/atm90e32/atm90e32.cpp index 412964d0f8..db29702c54 100644 --- a/esphome/components/atm90e32/atm90e32.cpp +++ b/esphome/components/atm90e32/atm90e32.cpp @@ -550,8 +550,8 @@ float ATM90E32Component::get_phase_harmonic_active_power_(uint8_t phase) { } float ATM90E32Component::get_phase_angle_(uint8_t phase) { - uint16_t val = this->read16_(ATM90E32_REGISTER_PANGLE + phase) / 10.0; - return (val > 180) ? (float) (val - 360.0f) : (float) val; + float val = this->read16_(ATM90E32_REGISTER_PANGLE + phase) / 10.0f; + return (val > 180.0f) ? val - 360.0f : val; } float ATM90E32Component::get_phase_peak_current_(uint8_t phase) { @@ -619,7 +619,7 @@ void ATM90E32Component::run_gain_calibrations() { ESP_LOGW(TAG, "[CALIBRATION][%s] Phase %s - Skipping voltage calibration: measured voltage is 0.", cs, phase_labels[phase]); } else { - uint32_t new_voltage_gain = static_cast((ref_voltage / measured_voltage) * current_voltage_gain); + uint32_t new_voltage_gain = static_cast((ref_voltage / measured_voltage) * current_voltage_gain); if (new_voltage_gain == 0) { ESP_LOGW(TAG, "[CALIBRATION][%s] Phase %s - Voltage gain would be 0. Check reference and measured voltage.", cs, phase_labels[phase]); @@ -644,7 +644,7 @@ void ATM90E32Component::run_gain_calibrations() { ESP_LOGW(TAG, "[CALIBRATION][%s] Phase %s - Skipping current calibration: measured current is 0.", cs, phase_labels[phase]); } else { - uint32_t new_current_gain = static_cast((ref_current / measured_current) * current_current_gain); + uint32_t new_current_gain = static_cast((ref_current / measured_current) * current_current_gain); if (new_current_gain == 0) { ESP_LOGW(TAG, "[CALIBRATION][%s] Phase %s - Current gain would be 0. Check reference and measured current.", cs, phase_labels[phase]); diff --git a/esphome/components/atm90e32/atm90e32.h b/esphome/components/atm90e32/atm90e32.h index 2524616470..c44a11e3ed 100644 --- a/esphome/components/atm90e32/atm90e32.h +++ b/esphome/components/atm90e32/atm90e32.h @@ -134,7 +134,6 @@ class ATM90E32Component : public PollingComponent, void set_freq_status_text_sensor(text_sensor::TextSensor *sensor) { this->freq_status_text_sensor_ = sensor; } #endif uint16_t calculate_voltage_threshold(int line_freq, uint16_t ugain, float multiplier); - int32_t last_periodic_millis = millis(); protected: #ifdef USE_NUMBER diff --git a/esphome/components/audio/__init__.py b/esphome/components/audio/__init__.py index 6c721652e1..8f2102de6a 100644 --- a/esphome/components/audio/__init__.py +++ b/esphome/components/audio/__init__.py @@ -1,10 +1,14 @@ +from dataclasses import dataclass + 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 from esphome.const import CONF_BITS_PER_SAMPLE, CONF_NUM_CHANNELS, CONF_SAMPLE_RATE +from esphome.core import CORE import esphome.final_validate as fv CODEOWNERS = ["@kahrendt"] +DOMAIN = "audio" audio_ns = cg.esphome_ns.namespace("audio") AudioFile = audio_ns.struct("AudioFile") @@ -14,9 +18,38 @@ AUDIO_FILE_TYPE_ENUM = { "WAV": AudioFileType.WAV, "MP3": AudioFileType.MP3, "FLAC": AudioFileType.FLAC, + "OPUS": AudioFileType.OPUS, } +@dataclass +class AudioData: + flac_support: bool = False + mp3_support: bool = False + opus_support: bool = False + + +def _get_data() -> AudioData: + if DOMAIN not in CORE.data: + CORE.data[DOMAIN] = AudioData() + return CORE.data[DOMAIN] + + +def request_flac_support() -> None: + """Request FLAC codec support for audio decoding.""" + _get_data().flac_support = True + + +def request_mp3_support() -> None: + """Request MP3 codec support for audio decoding.""" + _get_data().mp3_support = True + + +def request_opus_support() -> None: + """Request Opus codec support for audio decoding.""" + _get_data().opus_support = True + + CONF_MIN_BITS_PER_SAMPLE = "min_bits_per_sample" CONF_MAX_BITS_PER_SAMPLE = "max_bits_per_sample" CONF_MIN_CHANNELS = "min_channels" @@ -166,7 +199,20 @@ def final_validate_audio_schema( 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( name="esphome/esp-audio-libs", - ref="2.0.3", + ref="2.0.4", ) + + data = _get_data() + if data.flac_support: + cg.add_define("USE_AUDIO_FLAC_SUPPORT") + add_idf_component(name="esphome/micro-flac", ref="0.1.1") + if data.mp3_support: + cg.add_define("USE_AUDIO_MP3_SUPPORT") + if data.opus_support: + cg.add_define("USE_AUDIO_OPUS_SUPPORT") + add_idf_component(name="esphome/micro-opus", ref="0.3.6") diff --git a/esphome/components/audio/audio.cpp b/esphome/components/audio/audio.cpp index 9cc9b7d0da..3d675109e4 100644 --- a/esphome/components/audio/audio.cpp +++ b/esphome/components/audio/audio.cpp @@ -1,5 +1,9 @@ #include "audio.h" +#include "esphome/core/helpers.h" + +#include + namespace esphome { namespace audio { @@ -46,6 +50,10 @@ const char *audio_file_type_to_string(AudioFileType file_type) { #ifdef USE_AUDIO_MP3_SUPPORT case AudioFileType::MP3: return "MP3"; +#endif +#ifdef USE_AUDIO_OPUS_SUPPORT + case AudioFileType::OPUS: + return "OPUS"; #endif case AudioFileType::WAV: return "WAV"; @@ -54,6 +62,58 @@ const char *audio_file_type_to_string(AudioFileType file_type) { } } +AudioFileType detect_audio_file_type(const char *content_type, const char *url) { + // Try Content-Type header first + if (content_type != nullptr && content_type[0] != '\0') { +#ifdef USE_AUDIO_MP3_SUPPORT + if (strcasecmp(content_type, "mp3") == 0 || strcasecmp(content_type, "audio/mp3") == 0 || + strcasecmp(content_type, "audio/mpeg") == 0) { + return AudioFileType::MP3; + } +#endif + if (strcasecmp(content_type, "audio/wav") == 0) { + return AudioFileType::WAV; + } +#ifdef USE_AUDIO_FLAC_SUPPORT + if (strcasecmp(content_type, "audio/flac") == 0 || strcasecmp(content_type, "audio/x-flac") == 0) { + return AudioFileType::FLAC; + } +#endif +#ifdef USE_AUDIO_OPUS_SUPPORT + // Match "audio/ogg" with a codecs parameter containing "opus" + // Valid forms: audio/ogg;codecs=opus, audio/ogg; codecs="opus", etc. + // Plain "audio/ogg" without opus is not matched (almost always Ogg Vorbis) + if (strncasecmp(content_type, "audio/ogg", 9) == 0 && strcasestr(content_type + 9, "opus") != nullptr) { + return AudioFileType::OPUS; + } +#endif + } + + // Fallback to URL extension + if (url != nullptr && url[0] != '\0') { + if (str_endswith_ignore_case(url, ".wav")) { + return AudioFileType::WAV; + } +#ifdef USE_AUDIO_MP3_SUPPORT + if (str_endswith_ignore_case(url, ".mp3")) { + return AudioFileType::MP3; + } +#endif +#ifdef USE_AUDIO_FLAC_SUPPORT + if (str_endswith_ignore_case(url, ".flac")) { + return AudioFileType::FLAC; + } +#endif +#ifdef USE_AUDIO_OPUS_SUPPORT + if (str_endswith_ignore_case(url, ".opus")) { + return AudioFileType::OPUS; + } +#endif + } + + return AudioFileType::NONE; +} + void scale_audio_samples(const int16_t *audio_samples, int16_t *output_buffer, int16_t scale_factor, size_t samples_to_scale) { // Note the assembly dsps_mulc function has audio glitches if the input and output buffers are the same. diff --git a/esphome/components/audio/audio.h b/esphome/components/audio/audio.h index e01d7eb101..d3b41a362f 100644 --- a/esphome/components/audio/audio.h +++ b/esphome/components/audio/audio.h @@ -112,6 +112,9 @@ enum class AudioFileType : uint8_t { #endif #ifdef USE_AUDIO_MP3_SUPPORT MP3, +#endif +#ifdef USE_AUDIO_OPUS_SUPPORT + OPUS, #endif WAV, }; @@ -127,6 +130,13 @@ struct AudioFile { /// @return const char pointer to the readable file type const char *audio_file_type_to_string(AudioFileType file_type); +/// @brief Detect audio file type from a Content-Type header value and/or URL extension. +/// Tries Content-Type first, then falls back to URL extension. Either parameter may be null. +/// @param content_type Content-Type header value (may be null or empty) +/// @param url URL to inspect for file extension (may be null or empty) +/// @return The detected AudioFileType, or NONE if unknown +AudioFileType detect_audio_file_type(const char *content_type, const char *url); + /// @brief Scales Q15 fixed point audio samples. Scales in place if audio_samples == output_buffer. /// @param audio_samples PCM int16 audio samples /// @param output_buffer Buffer to store the scaled samples diff --git a/esphome/components/audio/audio_decoder.cpp b/esphome/components/audio/audio_decoder.cpp index 8f514468c4..baa4c41c06 100644 --- a/esphome/components/audio/audio_decoder.cpp +++ b/esphome/components/audio/audio_decoder.cpp @@ -3,17 +3,20 @@ #ifdef USE_ESP32 #include "esphome/core/hal.h" +#include "esphome/core/log.h" namespace esphome { namespace audio { +static const char *const TAG = "audio.decoder"; + static const uint32_t DECODING_TIMEOUT_MS = 50; // The decode function will yield after this duration static const uint32_t READ_WRITE_TIMEOUT_MS = 20; // Timeout for transferring audio data static const uint32_t MAX_POTENTIALLY_FAILED_COUNT = 10; -AudioDecoder::AudioDecoder(size_t input_buffer_size, size_t output_buffer_size) { - this->input_transfer_buffer_ = AudioSourceTransferBuffer::create(input_buffer_size); +AudioDecoder::AudioDecoder(size_t input_buffer_size, size_t output_buffer_size) + : input_buffer_size_(input_buffer_size) { this->output_transfer_buffer_ = AudioSinkTransferBuffer::create(output_buffer_size); } @@ -26,11 +29,20 @@ AudioDecoder::~AudioDecoder() { } esp_err_t AudioDecoder::add_source(std::weak_ptr &input_ring_buffer) { - if (this->input_transfer_buffer_ != nullptr) { - this->input_transfer_buffer_->set_source(input_ring_buffer); - return ESP_OK; + auto source = AudioSourceTransferBuffer::create(this->input_buffer_size_); + if (source == nullptr) { + return ESP_ERR_NO_MEM; } - return ESP_ERR_NO_MEM; + source->set_source(input_ring_buffer); + this->input_buffer_ = std::move(source); + return ESP_OK; +} + +esp_err_t AudioDecoder::add_source(const uint8_t *data_pointer, size_t length) { + auto source = make_unique(); + source->set_data(data_pointer, length); + this->input_buffer_ = std::move(source); + return ESP_OK; } esp_err_t AudioDecoder::add_sink(std::weak_ptr &output_ring_buffer) { @@ -51,8 +63,16 @@ esp_err_t AudioDecoder::add_sink(speaker::Speaker *speaker) { } #endif +esp_err_t AudioDecoder::add_sink(AudioSinkCallback *callback) { + if (this->output_transfer_buffer_ != nullptr) { + this->output_transfer_buffer_->set_sink(callback); + return ESP_OK; + } + return ESP_ERR_NO_MEM; +} + esp_err_t AudioDecoder::start(AudioFileType audio_file_type) { - if ((this->input_transfer_buffer_ == nullptr) || (this->output_transfer_buffer_ == nullptr)) { + if (this->output_transfer_buffer_ == nullptr) { return ESP_ERR_NO_MEM; } @@ -64,9 +84,10 @@ esp_err_t AudioDecoder::start(AudioFileType audio_file_type) { switch (this->audio_file_type_) { #ifdef USE_AUDIO_FLAC_SUPPORT case AudioFileType::FLAC: - this->flac_decoder_ = make_unique(); + this->flac_decoder_ = make_unique(); this->free_buffer_required_ = this->output_transfer_buffer_->capacity(); // Adjusted and reallocated after reading the header + this->decoder_buffers_internally_ = true; break; #endif #ifdef USE_AUDIO_MP3_SUPPORT @@ -79,6 +100,14 @@ esp_err_t AudioDecoder::start(AudioFileType audio_file_type) { // Always reallocate the output transfer buffer to the smallest necessary size this->output_transfer_buffer_->reallocate(this->free_buffer_required_); break; +#endif +#ifdef USE_AUDIO_OPUS_SUPPORT + case AudioFileType::OPUS: + this->opus_decoder_ = make_unique(); + this->free_buffer_required_ = + this->output_transfer_buffer_->capacity(); // Adjusted and reallocated after reading the header + this->decoder_buffers_internally_ = true; + break; #endif case AudioFileType::WAV: this->wav_decoder_ = make_unique(); @@ -101,6 +130,10 @@ esp_err_t AudioDecoder::start(AudioFileType audio_file_type) { } AudioDecoderState AudioDecoder::decode(bool stop_gracefully) { + if (this->input_buffer_ == nullptr) { + return AudioDecoderState::FAILED; + } + if (stop_gracefully) { if (this->output_transfer_buffer_->available() == 0) { if (this->end_of_file_) { @@ -108,7 +141,7 @@ AudioDecoderState AudioDecoder::decode(bool stop_gracefully) { return AudioDecoderState::FINISHED; } - if (!this->input_transfer_buffer_->has_buffered_data()) { + if (!this->input_buffer_->has_buffered_data()) { // If all the internal buffers are empty, the decoding is done return AudioDecoderState::FINISHED; } @@ -158,10 +191,11 @@ AudioDecoderState AudioDecoder::decode(bool stop_gracefully) { // Decode more audio // Only shift data on the first loop iteration to avoid unnecessary, slow moves - size_t bytes_read = this->input_transfer_buffer_->transfer_data_from_source(pdMS_TO_TICKS(READ_WRITE_TIMEOUT_MS), - first_loop_iteration); + // If the decoder buffers internally, then never shift + size_t bytes_read = this->input_buffer_->fill(pdMS_TO_TICKS(READ_WRITE_TIMEOUT_MS), + first_loop_iteration && !this->decoder_buffers_internally_); - if (!first_loop_iteration && (this->input_transfer_buffer_->available() < bytes_processed)) { + if (!first_loop_iteration && (this->input_buffer_->available() < bytes_processed)) { // Less data is available than what was processed in last iteration, so don't attempt to decode. // This attempts to avoid the decoder from consistently trying to decode an incomplete frame. The transfer buffer // will shift the remaining data to the start and copy more from the source the next time the decode function is @@ -169,19 +203,21 @@ AudioDecoderState AudioDecoder::decode(bool stop_gracefully) { break; } - bytes_available_before_processing = this->input_transfer_buffer_->available(); + bytes_available_before_processing = this->input_buffer_->available(); if ((this->potentially_failed_count_ > 0) && (bytes_read == 0)) { // Failed to decode in last attempt and there is no new data - if ((this->input_transfer_buffer_->free() == 0) && first_loop_iteration) { - // The input buffer is full. Since it previously failed on the exact same data, we can never recover + if ((this->input_buffer_->free() == 0) && first_loop_iteration) { + // The input buffer is full (or read-only, e.g. const flash source). Since it previously failed on the exact + // same data, we can never recover. For const sources this is correct: the entire file is already available, so + // a decode failure is genuine, not a transient out-of-data condition. state = FileDecoderState::FAILED; } else { // Attempt to get more data next time state = FileDecoderState::IDLE; } - } else if (this->input_transfer_buffer_->available() == 0) { + } else if (this->input_buffer_->available() == 0) { // No data to decode, attempt to get more data next time state = FileDecoderState::IDLE; } else { @@ -195,6 +231,11 @@ AudioDecoderState AudioDecoder::decode(bool stop_gracefully) { case AudioFileType::MP3: state = this->decode_mp3_(); break; +#endif +#ifdef USE_AUDIO_OPUS_SUPPORT + case AudioFileType::OPUS: + state = this->decode_opus_(); + break; #endif case AudioFileType::WAV: state = this->decode_wav_(); @@ -207,7 +248,7 @@ AudioDecoderState AudioDecoder::decode(bool stop_gracefully) { } first_loop_iteration = false; - bytes_processed = bytes_available_before_processing - this->input_transfer_buffer_->available(); + bytes_processed = bytes_available_before_processing - this->input_buffer_->available(); if (state == FileDecoderState::POTENTIALLY_FAILED) { ++this->potentially_failed_count_; @@ -224,61 +265,45 @@ AudioDecoderState AudioDecoder::decode(bool stop_gracefully) { #ifdef USE_AUDIO_FLAC_SUPPORT FileDecoderState AudioDecoder::decode_flac_() { - if (!this->audio_stream_info_.has_value()) { - // Header hasn't been read - auto result = this->flac_decoder_->read_header(this->input_transfer_buffer_->get_buffer_start(), - this->input_transfer_buffer_->available()); + size_t bytes_consumed, samples_decoded; - if (result > esp_audio_libs::flac::FLAC_DECODER_HEADER_OUT_OF_DATA) { - // Serrious error reading FLAC header, there is no recovery - return FileDecoderState::FAILED; + micro_flac::FLACDecoderResult result = this->flac_decoder_->decode( + this->input_buffer_->data(), this->input_buffer_->available(), this->output_transfer_buffer_->get_buffer_end(), + this->output_transfer_buffer_->free(), bytes_consumed, samples_decoded); + + if (result == micro_flac::FLAC_DECODER_SUCCESS) { + if (samples_decoded > 0 && this->audio_stream_info_.has_value()) { + this->output_transfer_buffer_->increase_buffer_length( + this->audio_stream_info_.value().samples_to_bytes(samples_decoded)); } + this->input_buffer_->consume(bytes_consumed); + } else if (result == micro_flac::FLAC_DECODER_HEADER_READY) { + // Header just parsed, stream info now available + const auto &info = this->flac_decoder_->get_stream_info(); + this->audio_stream_info_ = audio::AudioStreamInfo(info.bits_per_sample(), info.num_channels(), info.sample_rate()); - size_t bytes_consumed = this->flac_decoder_->get_bytes_index(); - this->input_transfer_buffer_->decrease_buffer_length(bytes_consumed); - - if (result == esp_audio_libs::flac::FLAC_DECODER_HEADER_OUT_OF_DATA) { - return FileDecoderState::MORE_TO_PROCESS; - } - - // Reallocate the output transfer buffer to the smallest necessary size - this->free_buffer_required_ = flac_decoder_->get_output_buffer_size_bytes(); + // Reallocate the output transfer buffer to the required size + this->free_buffer_required_ = this->flac_decoder_->get_output_buffer_size_samples() * info.bytes_per_sample(); if (!this->output_transfer_buffer_->reallocate(this->free_buffer_required_)) { - // Couldn't reallocate output buffer return FileDecoderState::FAILED; } - - this->audio_stream_info_ = - audio::AudioStreamInfo(this->flac_decoder_->get_sample_depth(), this->flac_decoder_->get_num_channels(), - this->flac_decoder_->get_sample_rate()); - - return FileDecoderState::MORE_TO_PROCESS; - } - - uint32_t output_samples = 0; - auto result = this->flac_decoder_->decode_frame(this->input_transfer_buffer_->get_buffer_start(), - this->input_transfer_buffer_->available(), - this->output_transfer_buffer_->get_buffer_end(), &output_samples); - - if (result == esp_audio_libs::flac::FLAC_DECODER_ERROR_OUT_OF_DATA) { - // Not an issue, just needs more data that we'll get next time. - return FileDecoderState::POTENTIALLY_FAILED; - } - - size_t bytes_consumed = this->flac_decoder_->get_bytes_index(); - this->input_transfer_buffer_->decrease_buffer_length(bytes_consumed); - - if (result > esp_audio_libs::flac::FLAC_DECODER_ERROR_OUT_OF_DATA) { - // Corrupted frame, don't retry with current buffer content, wait for new sync - return FileDecoderState::POTENTIALLY_FAILED; - } - - // We have successfully decoded some input data and have new output data - this->output_transfer_buffer_->increase_buffer_length( - this->audio_stream_info_.value().samples_to_bytes(output_samples)); - - if (result == esp_audio_libs::flac::FLAC_DECODER_NO_MORE_FRAMES) { + this->input_buffer_->consume(bytes_consumed); + } else if (result == micro_flac::FLAC_DECODER_END_OF_STREAM) { + this->input_buffer_->consume(bytes_consumed); return FileDecoderState::END_OF_FILE; + } else if (result == micro_flac::FLAC_DECODER_NEED_MORE_DATA) { + this->input_buffer_->consume(bytes_consumed); + return FileDecoderState::MORE_TO_PROCESS; + } else if (result == micro_flac::FLAC_DECODER_ERROR_OUTPUT_TOO_SMALL) { + // Reallocate to decode the frame on the next call + const auto &info = this->flac_decoder_->get_stream_info(); + this->free_buffer_required_ = this->flac_decoder_->get_output_buffer_size_samples() * info.bytes_per_sample(); + if (!this->output_transfer_buffer_->reallocate(this->free_buffer_required_)) { + return FileDecoderState::FAILED; + } + } else { + ESP_LOGE(TAG, "FLAC decoder failed: %d", static_cast(result)); + return FileDecoderState::POTENTIALLY_FAILED; } return FileDecoderState::MORE_TO_PROCESS; @@ -288,26 +313,25 @@ FileDecoderState AudioDecoder::decode_flac_() { #ifdef USE_AUDIO_MP3_SUPPORT FileDecoderState AudioDecoder::decode_mp3_() { // Look for the next sync word - int buffer_length = (int) this->input_transfer_buffer_->available(); - int32_t offset = - esp_audio_libs::helix_decoder::MP3FindSyncWord(this->input_transfer_buffer_->get_buffer_start(), buffer_length); + int buffer_length = (int) this->input_buffer_->available(); + int32_t offset = esp_audio_libs::helix_decoder::MP3FindSyncWord(this->input_buffer_->data(), buffer_length); if (offset < 0) { // New data may have the sync word - this->input_transfer_buffer_->decrease_buffer_length(buffer_length); + this->input_buffer_->consume(buffer_length); return FileDecoderState::POTENTIALLY_FAILED; } // Advance read pointer to match the offset for the syncword - this->input_transfer_buffer_->decrease_buffer_length(offset); - const uint8_t *buffer_start = this->input_transfer_buffer_->get_buffer_start(); + this->input_buffer_->consume(offset); + const uint8_t *buffer_start = this->input_buffer_->data(); - buffer_length = (int) this->input_transfer_buffer_->available(); + buffer_length = (int) this->input_buffer_->available(); int err = esp_audio_libs::helix_decoder::MP3Decode(this->mp3_decoder_, &buffer_start, &buffer_length, (int16_t *) this->output_transfer_buffer_->get_buffer_end(), 0); - size_t consumed = this->input_transfer_buffer_->available() - buffer_length; - this->input_transfer_buffer_->decrease_buffer_length(consumed); + size_t consumed = this->input_buffer_->available() - buffer_length; + this->input_buffer_->consume(consumed); if (err) { switch (err) { @@ -339,15 +363,53 @@ FileDecoderState AudioDecoder::decode_mp3_() { } #endif +#ifdef USE_AUDIO_OPUS_SUPPORT +FileDecoderState AudioDecoder::decode_opus_() { + bool processed_header = this->opus_decoder_->is_initialized(); + + size_t bytes_consumed, samples_decoded; + + micro_opus::OggOpusResult result = this->opus_decoder_->decode( + this->input_buffer_->data(), this->input_buffer_->available(), this->output_transfer_buffer_->get_buffer_end(), + this->output_transfer_buffer_->free(), bytes_consumed, samples_decoded); + + if (result == micro_opus::OGG_OPUS_OK) { + if (!processed_header && this->opus_decoder_->is_initialized()) { + // Header processed and stream info is available + this->audio_stream_info_ = + audio::AudioStreamInfo(this->opus_decoder_->get_bit_depth(), this->opus_decoder_->get_channels(), + this->opus_decoder_->get_sample_rate()); + } + if (samples_decoded > 0 && this->audio_stream_info_.has_value()) { + // Some audio was processed + this->output_transfer_buffer_->increase_buffer_length( + this->audio_stream_info_.value().frames_to_bytes(samples_decoded)); + } + this->input_buffer_->consume(bytes_consumed); + } else if (result == micro_opus::OGG_OPUS_OUTPUT_BUFFER_TOO_SMALL) { + // Reallocate to decode the packet on the next call + this->free_buffer_required_ = this->opus_decoder_->get_required_output_buffer_size(); + if (!this->output_transfer_buffer_->reallocate(this->free_buffer_required_)) { + // Couldn't reallocate output buffer + return FileDecoderState::FAILED; + } + } else { + ESP_LOGE(TAG, "Opus decoder failed: %" PRId8, result); + return FileDecoderState::POTENTIALLY_FAILED; + } + return FileDecoderState::MORE_TO_PROCESS; +} +#endif + FileDecoderState AudioDecoder::decode_wav_() { if (!this->audio_stream_info_.has_value()) { // Header hasn't been processed - esp_audio_libs::wav_decoder::WAVDecoderResult result = this->wav_decoder_->decode_header( - this->input_transfer_buffer_->get_buffer_start(), this->input_transfer_buffer_->available()); + esp_audio_libs::wav_decoder::WAVDecoderResult result = + this->wav_decoder_->decode_header(this->input_buffer_->data(), this->input_buffer_->available()); if (result == esp_audio_libs::wav_decoder::WAV_DECODER_SUCCESS_IN_DATA) { - this->input_transfer_buffer_->decrease_buffer_length(this->wav_decoder_->bytes_processed()); + this->input_buffer_->consume(this->wav_decoder_->bytes_processed()); this->audio_stream_info_ = audio::AudioStreamInfo( this->wav_decoder_->bits_per_sample(), this->wav_decoder_->num_channels(), this->wav_decoder_->sample_rate()); @@ -363,7 +425,7 @@ FileDecoderState AudioDecoder::decode_wav_() { } } else { if (!this->wav_has_known_end_ || (this->wav_bytes_left_ > 0)) { - size_t bytes_to_copy = this->input_transfer_buffer_->available(); + size_t bytes_to_copy = this->input_buffer_->available(); if (this->wav_has_known_end_) { bytes_to_copy = std::min(bytes_to_copy, this->wav_bytes_left_); @@ -372,9 +434,8 @@ FileDecoderState AudioDecoder::decode_wav_() { bytes_to_copy = std::min(bytes_to_copy, this->output_transfer_buffer_->free()); if (bytes_to_copy > 0) { - std::memcpy(this->output_transfer_buffer_->get_buffer_end(), this->input_transfer_buffer_->get_buffer_start(), - bytes_to_copy); - this->input_transfer_buffer_->decrease_buffer_length(bytes_to_copy); + std::memcpy(this->output_transfer_buffer_->get_buffer_end(), this->input_buffer_->data(), bytes_to_copy); + this->input_buffer_->consume(bytes_to_copy); this->output_transfer_buffer_->increase_buffer_length(bytes_to_copy); if (this->wav_has_known_end_) { this->wav_bytes_left_ -= bytes_to_copy; diff --git a/esphome/components/audio/audio_decoder.h b/esphome/components/audio/audio_decoder.h index 2ca1d623fe..6e3a228a68 100644 --- a/esphome/components/audio/audio_decoder.h +++ b/esphome/components/audio/audio_decoder.h @@ -16,14 +16,21 @@ #include "esp_err.h" // esp-audio-libs -#ifdef USE_AUDIO_FLAC_SUPPORT -#include -#endif #ifdef USE_AUDIO_MP3_SUPPORT #include #endif #include +// micro-flac +#ifdef USE_AUDIO_FLAC_SUPPORT +#include +#endif + +// micro-opus +#ifdef USE_AUDIO_OPUS_SUPPORT +#include +#endif + namespace esphome { namespace audio { @@ -45,17 +52,17 @@ enum class FileDecoderState : uint8_t { class AudioDecoder { /* * @brief Class that facilitates decoding an audio file. - * The audio file is read from a ring buffer source, decoded, and sent to an audio sink (ring buffer or speaker - * component). - * Supports wav, flac, and mp3 formats. + * The audio file is read from a source (ring buffer or const data pointer), decoded, and sent to an audio sink + * (ring buffer, speaker component, or callback). + * Supports wav, flac, mp3, and ogg opus formats. */ public: - /// @brief Allocates the input and output transfer buffers + /// @brief Allocates the output transfer buffer and stores the input buffer size for later use by add_source() /// @param input_buffer_size Size of the input transfer buffer in bytes. /// @param output_buffer_size Size of the output transfer buffer in bytes. AudioDecoder(size_t input_buffer_size, size_t output_buffer_size); - /// @brief Deallocates the MP3 decoder (the flac and wav decoders are deallocated automatically) + /// @brief Deallocates the MP3 decoder (the flac, opus, and wav decoders are deallocated automatically) ~AudioDecoder(); /// @brief Adds a source ring buffer for raw file data. Takes ownership of the ring buffer in a shared_ptr. @@ -75,6 +82,17 @@ class AudioDecoder { esp_err_t add_sink(speaker::Speaker *speaker); #endif + /// @brief Adds a const data pointer as the source for raw file data. Does not allocate a transfer buffer. + /// @param data_pointer Pointer to the const audio data (e.g., stored in flash memory) + /// @param length Size of the data in bytes + /// @return ESP_OK + esp_err_t add_source(const uint8_t *data_pointer, size_t length); + + /// @brief Adds a callback as the sink for decoded audio. + /// @param callback Pointer to the AudioSinkCallback implementation + /// @return ESP_OK if successful, ESP_ERR_NO_MEM if the transfer buffer wasn't allocated + esp_err_t add_sink(AudioSinkCallback *callback); + /// @brief Sets up decoding the file /// @param audio_file_type AudioFileType of the file /// @return ESP_OK if successful, ESP_ERR_NO_MEM if the transfer buffers fail to allocate, or ESP_ERR_NOT_SUPPORTED if @@ -103,31 +121,38 @@ class AudioDecoder { std::unique_ptr wav_decoder_; #ifdef USE_AUDIO_FLAC_SUPPORT FileDecoderState decode_flac_(); - std::unique_ptr flac_decoder_; + std::unique_ptr flac_decoder_; #endif #ifdef USE_AUDIO_MP3_SUPPORT FileDecoderState decode_mp3_(); esp_audio_libs::helix_decoder::HMP3Decoder mp3_decoder_; +#endif +#ifdef USE_AUDIO_OPUS_SUPPORT + FileDecoderState decode_opus_(); + std::unique_ptr opus_decoder_; #endif FileDecoderState decode_wav_(); - std::unique_ptr input_transfer_buffer_; + std::unique_ptr input_buffer_; std::unique_ptr output_transfer_buffer_; AudioFileType audio_file_type_{AudioFileType::NONE}; optional audio_stream_info_{}; + size_t input_buffer_size_{0}; size_t free_buffer_required_{0}; size_t wav_bytes_left_{0}; uint32_t potentially_failed_count_{0}; + uint32_t accumulated_frames_written_{0}; + uint32_t playback_ms_{0}; + bool end_of_file_{false}; bool wav_has_known_end_{false}; - bool pause_output_{false}; + bool decoder_buffers_internally_{false}; - uint32_t accumulated_frames_written_{0}; - uint32_t playback_ms_{0}; + bool pause_output_{false}; }; } // namespace audio } // namespace esphome diff --git a/esphome/components/audio/audio_reader.cpp b/esphome/components/audio/audio_reader.cpp index 4e4bd31f9b..79ebf58889 100644 --- a/esphome/components/audio/audio_reader.cpp +++ b/esphome/components/audio/audio_reader.cpp @@ -185,21 +185,8 @@ esp_err_t AudioReader::start(const std::string &uri, AudioFileType &file_type) { return err; } - if (str_endswith_ignore_case(url, ".wav")) { - file_type = AudioFileType::WAV; - } -#ifdef USE_AUDIO_MP3_SUPPORT - else if (str_endswith_ignore_case(url, ".mp3")) { - file_type = AudioFileType::MP3; - } -#endif -#ifdef USE_AUDIO_FLAC_SUPPORT - else if (str_endswith_ignore_case(url, ".flac")) { - file_type = AudioFileType::FLAC; - } -#endif - else { - file_type = AudioFileType::NONE; + file_type = detect_audio_file_type(nullptr, url); + if (file_type == AudioFileType::NONE) { this->cleanup_connection_(); return ESP_ERR_NOT_SUPPORTED; } @@ -227,24 +214,6 @@ AudioReaderState AudioReader::read() { return AudioReaderState::FAILED; } -AudioFileType AudioReader::get_audio_type(const char *content_type) { -#ifdef USE_AUDIO_MP3_SUPPORT - if (strcasecmp(content_type, "mp3") == 0 || strcasecmp(content_type, "audio/mp3") == 0 || - strcasecmp(content_type, "audio/mpeg") == 0) { - return AudioFileType::MP3; - } -#endif - if (strcasecmp(content_type, "audio/wav") == 0) { - return AudioFileType::WAV; - } -#ifdef USE_AUDIO_FLAC_SUPPORT - if (strcasecmp(content_type, "audio/flac") == 0 || strcasecmp(content_type, "audio/x-flac") == 0) { - return AudioFileType::FLAC; - } -#endif - return AudioFileType::NONE; -} - esp_err_t AudioReader::http_event_handler(esp_http_client_event_t *evt) { // Based on https://github.com/maroc81/WeatherLily/tree/main/main/net accessed 20241224 AudioReader *this_reader = (AudioReader *) evt->user_data; @@ -252,7 +221,7 @@ esp_err_t AudioReader::http_event_handler(esp_http_client_event_t *evt) { switch (evt->event_id) { case HTTP_EVENT_ON_HEADER: if (strcasecmp(evt->header_key, "Content-Type") == 0) { - this_reader->audio_file_type_ = get_audio_type(evt->header_value); + this_reader->audio_file_type_ = detect_audio_file_type(evt->header_value, nullptr); } break; default: diff --git a/esphome/components/audio/audio_reader.h b/esphome/components/audio/audio_reader.h index 0b73923e84..753b310213 100644 --- a/esphome/components/audio/audio_reader.h +++ b/esphome/components/audio/audio_reader.h @@ -58,11 +58,6 @@ class AudioReader { /// @brief Monitors the http client events to attempt determining the file type from the Content-Type header static esp_err_t http_event_handler(esp_http_client_event_t *evt); - /// @brief Determines the audio file type from the http header's Content-Type key - /// @param content_type string with the Content-Type key - /// @return AudioFileType of the url, if it can be determined. If not, return AudioFileType::NONE. - static AudioFileType get_audio_type(const char *content_type); - AudioReaderState file_read_(); AudioReaderState http_read_(); diff --git a/esphome/components/audio/audio_transfer_buffer.cpp b/esphome/components/audio/audio_transfer_buffer.cpp index 790cd62db0..5cd7cf9e63 100644 --- a/esphome/components/audio/audio_transfer_buffer.cpp +++ b/esphome/components/audio/audio_transfer_buffer.cpp @@ -2,6 +2,8 @@ #ifdef USE_ESP32 +#include + #include "esphome/core/helpers.h" namespace esphome { @@ -75,12 +77,32 @@ bool AudioTransferBuffer::has_buffered_data() const { } bool AudioTransferBuffer::reallocate(size_t new_buffer_size) { - if (this->buffer_length_ > 0) { - // Buffer currently has data, so reallocation is impossible + if (this->buffer_ == nullptr) { + return this->allocate_buffer_(new_buffer_size); + } + + if (new_buffer_size < this->buffer_length_) { + // New size is too small to hold existing data return false; } - this->deallocate_buffer_(); - return this->allocate_buffer_(new_buffer_size); + + // Shift existing data to the start of the buffer so realloc preserves it + if ((this->buffer_length_ > 0) && (this->data_start_ != this->buffer_)) { + std::memmove(this->buffer_, this->data_start_, this->buffer_length_); + this->data_start_ = this->buffer_; + } + + RAMAllocator allocator; + uint8_t *new_buffer = allocator.reallocate(this->buffer_, new_buffer_size); + if (new_buffer == nullptr) { + // Reallocation failed, but the original buffer is still valid + return false; + } + + this->buffer_ = new_buffer; + this->data_start_ = this->buffer_; + this->buffer_size_ = new_buffer_size; + return true; } bool AudioTransferBuffer::allocate_buffer_(size_t buffer_size) { @@ -115,12 +137,12 @@ size_t AudioSourceTransferBuffer::transfer_data_from_source(TickType_t ticks_to_ if (pre_shift) { // Shift data in buffer to start if (this->buffer_length_ > 0) { - memmove(this->buffer_, this->data_start_, this->buffer_length_); + std::memmove(this->buffer_, this->data_start_, this->buffer_length_); } this->data_start_ = this->buffer_; } - size_t bytes_to_read = this->free(); + size_t bytes_to_read = AudioTransferBuffer::free(); size_t bytes_read = 0; if (bytes_to_read > 0) { if (this->ring_buffer_.use_count() > 0) { @@ -143,6 +165,8 @@ size_t AudioSinkTransferBuffer::transfer_data_to_sink(TickType_t ticks_to_wait, if (this->ring_buffer_.use_count() > 0) { bytes_written = this->ring_buffer_->write_without_replacement((void *) this->data_start_, this->available(), ticks_to_wait); + } else if (this->sink_callback_ != nullptr) { + bytes_written = this->sink_callback_->audio_sink_write(this->data_start_, this->available(), ticks_to_wait); } this->decrease_buffer_length(bytes_written); @@ -150,7 +174,7 @@ size_t AudioSinkTransferBuffer::transfer_data_to_sink(TickType_t ticks_to_wait, if (post_shift) { // Shift unwritten data to the start of the buffer - memmove(this->buffer_, this->data_start_, this->buffer_length_); + std::memmove(this->buffer_, this->data_start_, this->buffer_length_); this->data_start_ = this->buffer_; } @@ -169,6 +193,21 @@ bool AudioSinkTransferBuffer::has_buffered_data() const { return (this->available() > 0); } +size_t AudioSourceTransferBuffer::free() const { return AudioTransferBuffer::free(); } + +bool AudioSourceTransferBuffer::has_buffered_data() const { return AudioTransferBuffer::has_buffered_data(); } + +void ConstAudioSourceBuffer::set_data(const uint8_t *data, size_t length) { + this->data_start_ = data; + this->length_ = length; +} + +void ConstAudioSourceBuffer::consume(size_t bytes) { + bytes = std::min(bytes, this->length_); + this->length_ -= bytes; + this->data_start_ += bytes; +} + } // namespace audio } // namespace esphome diff --git a/esphome/components/audio/audio_transfer_buffer.h b/esphome/components/audio/audio_transfer_buffer.h index edb484e7d2..c32d4d0e41 100644 --- a/esphome/components/audio/audio_transfer_buffer.h +++ b/esphome/components/audio/audio_transfer_buffer.h @@ -15,6 +15,12 @@ namespace esphome { namespace audio { +/// @brief Abstract interface for writing decoded audio data to a sink. +class AudioSinkCallback { + public: + virtual size_t audio_sink_write(uint8_t *data, size_t length, TickType_t ticks_to_wait) = 0; +}; + class AudioTransferBuffer { /* * @brief Class that facilitates tranferring data between a buffer and an audio source or sink. @@ -26,7 +32,7 @@ class AudioTransferBuffer { /// @brief Destructor that deallocates the transfer buffer ~AudioTransferBuffer(); - /// @brief Returns a pointer to the start of the transfer buffer where available() bytes of exisiting data can be read + /// @brief Returns a pointer to the start of the transfer buffer where available() bytes of existing data can be read uint8_t *get_buffer_start() const { return this->data_start_; } /// @brief Returns a pointer to the end of the transfer buffer where free() bytes of new data can be written @@ -56,6 +62,9 @@ class AudioTransferBuffer { /// @return True if there is data, false otherwise. virtual bool has_buffered_data() const; + /// @brief Reallocates the transfer buffer, preserving any existing data. + /// @param new_buffer_size The new size in bytes. Must be at least as large as available(). + /// @return True if successful, false otherwise. On failure, the original buffer remains valid. bool reallocate(size_t new_buffer_size); protected: @@ -105,6 +114,10 @@ class AudioSinkTransferBuffer : public AudioTransferBuffer { void set_sink(speaker::Speaker *speaker) { this->speaker_ = speaker; } #endif + /// @brief Adds a callback as the transfer buffer's sink. + /// @param callback Pointer to the AudioSinkCallback implementation + void set_sink(AudioSinkCallback *callback) { this->sink_callback_ = callback; } + void clear_buffered_data() override; bool has_buffered_data() const override; @@ -113,12 +126,44 @@ class AudioSinkTransferBuffer : public AudioTransferBuffer { #ifdef USE_SPEAKER speaker::Speaker *speaker_{nullptr}; #endif + AudioSinkCallback *sink_callback_{nullptr}; }; -class AudioSourceTransferBuffer : public AudioTransferBuffer { +/// @brief Abstract interface for reading audio data from a buffer. +/// Provides a common read interface for both mutable transfer buffers and read-only const buffers. +class AudioReadableBuffer { + public: + virtual ~AudioReadableBuffer() = default; + + /// @brief Returns a pointer to the start of readable data + virtual const uint8_t *data() const = 0; + + /// @brief Returns the number of bytes available to read + virtual size_t available() const = 0; + + /// @brief Returns the number of free bytes available to write. Defaults to 0 for read-only buffers. + virtual size_t free() const { return 0; } + + /// @brief Advances past consumed data + /// @param bytes Number of bytes consumed + virtual void consume(size_t bytes) = 0; + + /// @brief Tests if there is any buffered data + virtual bool has_buffered_data() const = 0; + + /// @brief Refills the buffer from its source. No-op by default for read-only buffers. + /// @param ticks_to_wait FreeRTOS ticks to block while waiting for data + /// @param pre_shift If true, shifts existing data to the start of the buffer before reading + /// @return Number of bytes read + virtual size_t fill(TickType_t ticks_to_wait, bool pre_shift) { return 0; } + size_t fill(TickType_t ticks_to_wait) { return this->fill(ticks_to_wait, true); } +}; + +class AudioSourceTransferBuffer : public AudioTransferBuffer, public AudioReadableBuffer { /* * @brief A class that implements a transfer buffer for audio sources. * Supports reading audio data from a ring buffer into the transfer buffer for processing. + * Implements AudioReadableBuffer for use by consumers that only need read access. */ public: /// @brief Creates a new source transfer buffer. @@ -126,7 +171,7 @@ class AudioSourceTransferBuffer : public AudioTransferBuffer { /// @return unique_ptr if successfully allocated, nullptr otherwise static std::unique_ptr create(size_t buffer_size); - /// @brief Reads any available data from the sink into the transfer buffer. + /// @brief Reads any available data from the source into the transfer buffer. /// @param ticks_to_wait FreeRTOS ticks to block while waiting for the source to have enough data /// @param pre_shift If true, any unwritten data is moved to the start of the buffer before transferring from the /// source. Defaults to true. @@ -136,6 +181,36 @@ class AudioSourceTransferBuffer : public AudioTransferBuffer { /// @brief Adds a ring buffer as the transfer buffer's source. /// @param ring_buffer weak_ptr to the allocated ring buffer void set_source(const std::weak_ptr &ring_buffer) { this->ring_buffer_ = ring_buffer.lock(); }; + + // AudioReadableBuffer interface + const uint8_t *data() const override { return this->data_start_; } + size_t available() const override { return this->buffer_length_; } + size_t free() const override; + void consume(size_t bytes) override { this->decrease_buffer_length(bytes); } + bool has_buffered_data() const override; + size_t fill(TickType_t ticks_to_wait, bool pre_shift) override { + return this->transfer_data_from_source(ticks_to_wait, pre_shift); + } +}; + +/// @brief A lightweight read-only audio buffer for const data sources (e.g., flash memory). +/// Does not allocate memory or transfer data from external sources. +class ConstAudioSourceBuffer : public AudioReadableBuffer { + public: + /// @brief Sets the data pointer and length for the buffer + /// @param data Pointer to the const audio data + /// @param length Size of the data in bytes + void set_data(const uint8_t *data, size_t length); + + // AudioReadableBuffer interface + const uint8_t *data() const override { return this->data_start_; } + size_t available() const override { return this->length_; } + void consume(size_t bytes) override; + bool has_buffered_data() const override { return this->length_ > 0; } + + protected: + const uint8_t *data_start_{nullptr}; + size_t length_{0}; }; } // namespace audio diff --git a/esphome/components/audio_adc/__init__.py b/esphome/components/audio_adc/__init__.py index 2f95a039f5..3c9b32e610 100644 --- a/esphome/components/audio_adc/__init__.py +++ b/esphome/components/audio_adc/__init__.py @@ -23,7 +23,10 @@ SET_MIC_GAIN_ACTION_SCHEMA = cv.maybe_simple_value( @automation.register_action( - "audio_adc.set_mic_gain", SetMicGainAction, SET_MIC_GAIN_ACTION_SCHEMA + "audio_adc.set_mic_gain", + SetMicGainAction, + SET_MIC_GAIN_ACTION_SCHEMA, + synchronous=True, ) async def audio_adc_set_mic_gain_to_code(config, action_id, template_arg, args): paren = await cg.get_variable(config[CONF_ID]) diff --git a/esphome/components/audio_dac/__init__.py b/esphome/components/audio_dac/__init__.py index 92e6cb18fa..a950c1967b 100644 --- a/esphome/components/audio_dac/__init__.py +++ b/esphome/components/audio_dac/__init__.py @@ -31,15 +31,22 @@ SET_VOLUME_ACTION_SCHEMA = cv.maybe_simple_value( ) -@automation.register_action("audio_dac.mute_off", MuteOffAction, MUTE_ACTION_SCHEMA) -@automation.register_action("audio_dac.mute_on", MuteOnAction, MUTE_ACTION_SCHEMA) +@automation.register_action( + "audio_dac.mute_off", MuteOffAction, MUTE_ACTION_SCHEMA, synchronous=True +) +@automation.register_action( + "audio_dac.mute_on", MuteOnAction, MUTE_ACTION_SCHEMA, synchronous=True +) async def audio_dac_mute_action_to_code(config, action_id, template_arg, args): paren = await cg.get_variable(config[CONF_ID]) return cg.new_Pvariable(action_id, template_arg, paren) @automation.register_action( - "audio_dac.set_volume", SetVolumeAction, SET_VOLUME_ACTION_SCHEMA + "audio_dac.set_volume", + SetVolumeAction, + SET_VOLUME_ACTION_SCHEMA, + synchronous=True, ) async def audio_dac_set_volume_to_code(config, action_id, template_arg, args): paren = await cg.get_variable(config[CONF_ID]) diff --git a/esphome/components/audio_file/__init__.py b/esphome/components/audio_file/__init__.py new file mode 100644 index 0000000000..3ed6c1cd92 --- /dev/null +++ b/esphome/components/audio_file/__init__.py @@ -0,0 +1,255 @@ +from dataclasses import dataclass, field +import hashlib +import logging +from pathlib import Path + +import puremagic + +from esphome import external_files +import esphome.codegen as cg +from esphome.components import audio +import esphome.config_validation as cv +from esphome.const import ( + CONF_FILE, + CONF_ID, + CONF_PATH, + CONF_RAW_DATA_ID, + CONF_TYPE, + CONF_URL, +) +from esphome.core import CORE, ID, HexInt +from esphome.cpp_generator import MockObj +from esphome.external_files import download_content +from esphome.types import ConfigType + +_LOGGER = logging.getLogger(__name__) + +CODEOWNERS = ["@kahrendt"] + +AUTO_LOAD = ["audio"] + +DOMAIN = "audio_file" + +audio_file_ns = cg.esphome_ns.namespace("audio_file") + +TYPE_LOCAL = "local" +TYPE_WEB = "web" + + +@dataclass +class AudioFileData: + file_ids: dict[str, ID] = field(default_factory=dict) + file_cache: dict[str, tuple[bytes, MockObj]] = field(default_factory=dict) + + +def _get_data() -> AudioFileData: + if DOMAIN not in CORE.data: + CORE.data[DOMAIN] = AudioFileData() + return CORE.data[DOMAIN] + + +def get_audio_file_ids() -> dict[str, ID]: + """Get all registered audio file IDs for cross-component access.""" + return _get_data().file_ids + + +def _compute_local_file_path(value: ConfigType) -> Path: + url = value[CONF_URL] + h = hashlib.new("sha256") + h.update(url.encode()) + key = h.hexdigest()[:8] + base_dir = external_files.compute_local_file_dir(DOMAIN) + _LOGGER.debug("_compute_local_file_path: base_dir=%s", base_dir / key) + return base_dir / key + + +def _download_web_file(value: ConfigType) -> ConfigType: + url = value[CONF_URL] + path = _compute_local_file_path(value) + + download_content(url, path) + _LOGGER.debug("download_web_file: path=%s", path) + return value + + +def _file_schema(value: ConfigType | str) -> ConfigType: + if isinstance(value, str): + return _validate_file_shorthand(value) + return TYPED_FILE_SCHEMA(value) + + +def _validate_file_shorthand(value: str) -> ConfigType: + value = cv.string_strict(value) + if value.startswith("http://") or value.startswith("https://"): + return _file_schema( + { + CONF_TYPE: TYPE_WEB, + CONF_URL: value, + } + ) + return _file_schema( + { + CONF_TYPE: TYPE_LOCAL, + CONF_PATH: value, + } + ) + + +def read_audio_file_and_type(file_config: ConfigType) -> tuple[bytes, MockObj]: + """Read an audio file and determine its type. Used by this component and media_source platform.""" + conf_file = file_config[CONF_FILE] + file_source = conf_file[CONF_TYPE] + if file_source == TYPE_LOCAL: + path = CORE.relative_config_path(conf_file[CONF_PATH]) + elif file_source == TYPE_WEB: + path = _compute_local_file_path(conf_file) + else: + raise cv.Invalid("Unsupported file source") + + with open(path, "rb") as f: + data = f.read() + + try: + file_type: str = puremagic.from_string(data) + file_type = file_type.removeprefix(".") + except puremagic.PureError as e: + raise cv.Invalid( + f"Unable to determine audio file type of '{path}'. " + f"Try re-encoding the file into a supported format. Details: {e}" + ) + + media_file_type = audio.AUDIO_FILE_TYPE_ENUM["NONE"] + if file_type == "wav": + media_file_type = audio.AUDIO_FILE_TYPE_ENUM["WAV"] + elif file_type in ("mp3", "mpeg", "mpga"): + media_file_type = audio.AUDIO_FILE_TYPE_ENUM["MP3"] + elif file_type == "flac": + media_file_type = audio.AUDIO_FILE_TYPE_ENUM["FLAC"] + elif ( + file_type == "ogg" + and len(data) >= 36 + and data.startswith(b"OggS") + and data[28:36] == b"OpusHead" + ): + media_file_type = audio.AUDIO_FILE_TYPE_ENUM["OPUS"] + + return data, media_file_type + + +LOCAL_SCHEMA = cv.Schema( + { + cv.Required(CONF_PATH): cv.file_, + } +) + +WEB_SCHEMA = cv.All( + { + cv.Required(CONF_URL): cv.url, + }, + _download_web_file, +) + + +TYPED_FILE_SCHEMA = cv.typed_schema( + { + TYPE_LOCAL: LOCAL_SCHEMA, + TYPE_WEB: WEB_SCHEMA, + }, +) + + +MEDIA_FILE_TYPE_SCHEMA = cv.Schema( + { + cv.Required(CONF_ID): cv.declare_id(audio.AudioFile), + cv.Required(CONF_FILE): _file_schema, + cv.GenerateID(CONF_RAW_DATA_ID): cv.declare_id(cg.uint8), + } +) + + +MAX_FILE_SIZE = 5 * 1024 * 1024 # 5 MB + + +def _validate_supported_local_file(config: list[ConfigType]) -> list[ConfigType]: + for file_config in config: + data, media_file_type = read_audio_file_and_type(file_config) + + if len(data) > MAX_FILE_SIZE: + file_info = file_config.get(CONF_FILE, {}) + source = ( + file_info.get(CONF_PATH) or file_info.get(CONF_URL) or "unknown source" + ) + raise cv.Invalid( + f"Audio file {source!r} is too large ({len(data)} bytes, max {MAX_FILE_SIZE} bytes)" + ) + + if str(media_file_type) == str(audio.AUDIO_FILE_TYPE_ENUM["NONE"]): + file_info = file_config.get(CONF_FILE, {}) + source = ( + file_info.get(CONF_PATH) or file_info.get(CONF_URL) or "unknown source" + ) + raise cv.Invalid( + f"Unsupported media file from {source!r} (detected type: {media_file_type})" + ) + + # Cache the file data so to_code() doesn't need to re-read it + _get_data().file_cache[str(file_config[CONF_ID])] = (data, media_file_type) + + media_file_type_str = str(media_file_type) + if media_file_type_str == str(audio.AUDIO_FILE_TYPE_ENUM["FLAC"]): + audio.request_flac_support() + elif media_file_type_str == str(audio.AUDIO_FILE_TYPE_ENUM["MP3"]): + audio.request_mp3_support() + elif media_file_type_str == str(audio.AUDIO_FILE_TYPE_ENUM["OPUS"]): + audio.request_opus_support() + + return config + + +CONFIG_SCHEMA = cv.All( + cv.only_on_esp32, + cv.ensure_list(MEDIA_FILE_TYPE_SCHEMA), + _validate_supported_local_file, +) + + +async def to_code(config: list[ConfigType]) -> None: + cache = _get_data().file_cache + + for file_config in config: + file_id = str(file_config[CONF_ID]) + data, media_file_type = cache[file_id] + + rhs = [HexInt(x) for x in data] + prog_arr = cg.progmem_array(file_config[CONF_RAW_DATA_ID], rhs) + + media_files_struct = cg.StructInitializer( + audio.AudioFile, + ( + "data", + prog_arr, + ), + ( + "length", + len(rhs), + ), + ( + "file_type", + media_file_type, + ), + ) + + cg.new_Pvariable( + file_config[CONF_ID], + media_files_struct, + ) + + # Store file ID for cross-component access + _get_data().file_ids[file_id] = file_config[CONF_ID] + + # Register all files in the shared C++ registry + cg.add_define("AUDIO_FILE_MAX_FILES", len(config)) + for file_config in config: + file_id = str(file_config[CONF_ID]) + file_var = await cg.get_variable(file_config[CONF_ID]) + cg.add(audio_file_ns.add_named_audio_file(file_var, file_id)) diff --git a/esphome/components/audio_file/audio_file.h b/esphome/components/audio_file/audio_file.h new file mode 100644 index 0000000000..537e19fb3c --- /dev/null +++ b/esphome/components/audio_file/audio_file.h @@ -0,0 +1,28 @@ +#pragma once + +#include "esphome/core/defines.h" + +#ifdef AUDIO_FILE_MAX_FILES + +#include "esphome/components/audio/audio.h" +#include "esphome/core/helpers.h" + +namespace esphome::audio_file { + +struct NamedAudioFile { + audio::AudioFile *file; + const char *file_id; +}; + +inline StaticVector + named_audio_files; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) + +inline void add_named_audio_file(audio::AudioFile *file, const char *file_id) { + named_audio_files.push_back({file, file_id}); +} + +inline const StaticVector &get_named_audio_files() { return named_audio_files; } + +} // namespace esphome::audio_file + +#endif // AUDIO_FILE_MAX_FILES diff --git a/esphome/components/audio_file/media_source/__init__.py b/esphome/components/audio_file/media_source/__init__.py new file mode 100644 index 0000000000..e9e292a2b2 --- /dev/null +++ b/esphome/components/audio_file/media_source/__init__.py @@ -0,0 +1,38 @@ +import esphome.codegen as cg +from esphome.components import media_source, psram +import esphome.config_validation as cv +from esphome.const import CONF_ID, CONF_TASK_STACK_IN_PSRAM +from esphome.types import ConfigType + +CODEOWNERS = ["@kahrendt"] +AUTO_LOAD = ["audio"] +DEPENDENCIES = ["audio_file"] + +audio_file_ns = cg.esphome_ns.namespace("audio_file") +AudioFileMediaSource = audio_file_ns.class_( + "AudioFileMediaSource", cg.Component, media_source.MediaSource +) + +CONFIG_SCHEMA = cv.All( + media_source.media_source_schema( + AudioFileMediaSource, + ) + .extend( + { + cv.Optional(CONF_TASK_STACK_IN_PSRAM): cv.All( + cv.boolean, cv.requires_component(psram.DOMAIN) + ), + } + ) + .extend(cv.COMPONENT_SCHEMA), + cv.only_on_esp32, +) + + +async def to_code(config: ConfigType) -> None: + var = cg.new_Pvariable(config[CONF_ID]) + await cg.register_component(var, config) + await media_source.register_media_source(var, config) + + if CONF_TASK_STACK_IN_PSRAM in config: + cg.add(var.set_task_stack_in_psram(config[CONF_TASK_STACK_IN_PSRAM])) diff --git a/esphome/components/audio_file/media_source/audio_file_media_source.cpp b/esphome/components/audio_file/media_source/audio_file_media_source.cpp new file mode 100644 index 0000000000..fbb5ecd88d --- /dev/null +++ b/esphome/components/audio_file/media_source/audio_file_media_source.cpp @@ -0,0 +1,284 @@ +#include "audio_file_media_source.h" + +#ifdef USE_ESP32 + +#include "esphome/components/audio/audio_decoder.h" + +#include +#include + +namespace esphome::audio_file { + +namespace { // anonymous namespace for internal linkage +struct AudioSinkAdapter : public audio::AudioSinkCallback { + media_source::MediaSource *source; + audio::AudioStreamInfo stream_info; + + size_t audio_sink_write(uint8_t *data, size_t length, TickType_t ticks_to_wait) override { + return this->source->write_output(data, length, pdTICKS_TO_MS(ticks_to_wait), this->stream_info); + } +}; +} // namespace + +#if defined(USE_AUDIO_OPUS_SUPPORT) +static constexpr uint32_t DECODE_TASK_STACK_SIZE = 5 * 1024; +#else +static constexpr uint32_t DECODE_TASK_STACK_SIZE = 3 * 1024; +#endif + +static const char *const TAG = "audio_file_media_source"; + +enum EventGroupBits : uint32_t { + // Requests to start playback (set by play_uri, handled by loop) + REQUEST_START = (1 << 0), + // Commands from main loop to decode task + COMMAND_STOP = (1 << 1), + COMMAND_PAUSE = (1 << 2), + // Decode task lifecycle signals (one-shot, cleared by loop) + TASK_STARTING = (1 << 7), + TASK_RUNNING = (1 << 8), + TASK_STOPPING = (1 << 9), + TASK_STOPPED = (1 << 10), + TASK_ERROR = (1 << 11), + // Decode task state (level-triggered, set/cleared by decode task) + TASK_PAUSED = (1 << 12), + ALL_BITS = 0x00FFFFFF, // All valid FreeRTOS event group bits +}; + +void AudioFileMediaSource::dump_config() { + ESP_LOGCONFIG(TAG, "Audio File Media Source:"); + ESP_LOGCONFIG(TAG, " Task Stack in PSRAM: %s", this->task_stack_in_psram_ ? "Yes" : "No"); +} + +void AudioFileMediaSource::setup() { + this->disable_loop(); + + this->event_group_ = xEventGroupCreate(); + if (this->event_group_ == nullptr) { + ESP_LOGE(TAG, "Failed to create event group"); + this->mark_failed(); + return; + } +} + +void AudioFileMediaSource::loop() { + EventBits_t event_bits = xEventGroupGetBits(this->event_group_); + + if (event_bits & REQUEST_START) { + xEventGroupClearBits(this->event_group_, REQUEST_START); + this->decoding_state_ = AudioFileDecodingState::START_TASK; + } + + switch (this->decoding_state_) { + case AudioFileDecodingState::START_TASK: { + if (!this->decode_task_.is_created()) { + xEventGroupClearBits(this->event_group_, ALL_BITS); + if (!this->decode_task_.create(decode_task, "AudioFileDec", DECODE_TASK_STACK_SIZE, this, 1, + this->task_stack_in_psram_)) { + ESP_LOGE(TAG, "Failed to create task"); + this->status_momentary_error("task_create", 1000); + this->set_state_(media_source::MediaSourceState::ERROR); + this->decoding_state_ = AudioFileDecodingState::IDLE; + return; + } + } + this->decoding_state_ = AudioFileDecodingState::DECODING; + break; + } + case AudioFileDecodingState::DECODING: { + if (event_bits & TASK_STARTING) { + ESP_LOGD(TAG, "Starting"); + xEventGroupClearBits(this->event_group_, TASK_STARTING); + } + + if (event_bits & TASK_RUNNING) { + ESP_LOGV(TAG, "Started"); + xEventGroupClearBits(this->event_group_, TASK_RUNNING); + this->set_state_(media_source::MediaSourceState::PLAYING); + } + + if ((event_bits & TASK_PAUSED) && this->get_state() != media_source::MediaSourceState::PAUSED) { + this->set_state_(media_source::MediaSourceState::PAUSED); + } else if (!(event_bits & TASK_PAUSED) && this->get_state() == media_source::MediaSourceState::PAUSED) { + this->set_state_(media_source::MediaSourceState::PLAYING); + } + + if (event_bits & TASK_STOPPING) { + ESP_LOGV(TAG, "Stopping"); + xEventGroupClearBits(this->event_group_, TASK_STOPPING); + } + + if (event_bits & TASK_ERROR) { + // Report error so the orchestrator knows playback failed; task will have already logged the specific error + this->set_state_(media_source::MediaSourceState::ERROR); + } + + if (event_bits & TASK_STOPPED) { + ESP_LOGD(TAG, "Stopped"); + xEventGroupClearBits(this->event_group_, ALL_BITS); + + this->decode_task_.deallocate(); + this->set_state_(media_source::MediaSourceState::IDLE); + this->decoding_state_ = AudioFileDecodingState::IDLE; + } + break; + } + case AudioFileDecodingState::IDLE: { + if (this->get_state() == media_source::MediaSourceState::ERROR && !this->status_has_error()) { + this->set_state_(media_source::MediaSourceState::IDLE); + } + break; + } + } + + if ((this->decoding_state_ == AudioFileDecodingState::IDLE) && + (this->get_state() == media_source::MediaSourceState::IDLE)) { + this->disable_loop(); + } +} + +// Called from the orchestrator's main loop, so no synchronization needed with loop() +bool AudioFileMediaSource::play_uri(const std::string &uri) { + if (!this->is_ready() || this->is_failed() || this->status_has_error() || !this->has_listener() || + xEventGroupGetBits(this->event_group_) & REQUEST_START) { + return false; + } + + // Check if source is already playing + if (this->get_state() != media_source::MediaSourceState::IDLE) { + ESP_LOGE(TAG, "Cannot play '%s': source is busy", uri.c_str()); + return false; + } + + // Validate URI starts with "audio-file://" + if (!uri.starts_with("audio-file://")) { + ESP_LOGE(TAG, "Invalid URI: '%s'", uri.c_str()); + return false; + } + + // Strip "audio-file://" prefix and find the file + const char *file_id = uri.c_str() + 13; // "audio-file://" is 13 characters + + for (const auto &named_file : get_named_audio_files()) { + if (strcmp(named_file.file_id, file_id) == 0) { + this->current_file_ = named_file.file; + xEventGroupSetBits(this->event_group_, EventGroupBits::REQUEST_START); + this->enable_loop(); + return true; + } + } + + ESP_LOGE(TAG, "Unknown file: '%s'", file_id); + return false; +} + +// Called from the orchestrator's main loop, so no synchronization needed with loop() +void AudioFileMediaSource::handle_command(media_source::MediaSourceCommand command) { + if (this->decoding_state_ != AudioFileDecodingState::DECODING) { + return; + } + + switch (command) { + case media_source::MediaSourceCommand::STOP: + xEventGroupSetBits(this->event_group_, EventGroupBits::COMMAND_STOP); + break; + case media_source::MediaSourceCommand::PAUSE: + xEventGroupSetBits(this->event_group_, EventGroupBits::COMMAND_PAUSE); + break; + case media_source::MediaSourceCommand::PLAY: + xEventGroupClearBits(this->event_group_, EventGroupBits::COMMAND_PAUSE); + break; + default: + break; + } +} + +void AudioFileMediaSource::decode_task(void *params) { + AudioFileMediaSource *this_source = static_cast(params); + + do { // do-while(false) ensures RAII objects are destroyed on all exit paths via break + + xEventGroupSetBits(this_source->event_group_, EventGroupBits::TASK_STARTING); + + // 0 bytes for input transfer buffer makes it an inplace buffer + std::unique_ptr decoder = make_unique(0, 4096); + + esp_err_t err = decoder->start(this_source->current_file_->file_type); + if (err != ESP_OK) { + ESP_LOGE(TAG, "Failed to start decoder: %s", esp_err_to_name(err)); + xEventGroupSetBits(this_source->event_group_, EventGroupBits::TASK_ERROR | EventGroupBits::TASK_STOPPING); + break; + } + + // Add the file as a const data source + decoder->add_source(this_source->current_file_->data, this_source->current_file_->length); + + xEventGroupSetBits(this_source->event_group_, EventGroupBits::TASK_RUNNING); + + AudioSinkAdapter audio_sink; + bool has_stream_info = false; + + while (true) { + EventBits_t event_bits = xEventGroupGetBits(this_source->event_group_); + + if (event_bits & EventGroupBits::COMMAND_STOP) { + break; + } + + bool paused = event_bits & EventGroupBits::COMMAND_PAUSE; + decoder->set_pause_output_state(paused); + if (paused) { + xEventGroupSetBits(this_source->event_group_, EventGroupBits::TASK_PAUSED); + vTaskDelay(pdMS_TO_TICKS(20)); + } else { + xEventGroupClearBits(this_source->event_group_, EventGroupBits::TASK_PAUSED); + } + + // Will stop gracefully once finished with the current file + audio::AudioDecoderState decoder_state = decoder->decode(true); + + if (decoder_state == audio::AudioDecoderState::FINISHED) { + break; + } else if (decoder_state == audio::AudioDecoderState::FAILED) { + ESP_LOGE(TAG, "Decoder failed"); + xEventGroupSetBits(this_source->event_group_, EventGroupBits::TASK_ERROR); + break; + } + + if (!has_stream_info && decoder->get_audio_stream_info().has_value()) { + has_stream_info = true; + + audio::AudioStreamInfo stream_info = decoder->get_audio_stream_info().value(); + + ESP_LOGD(TAG, "Bits per sample: %d, Channels: %d, Sample rate: %" PRIu32, stream_info.get_bits_per_sample(), + stream_info.get_channels(), stream_info.get_sample_rate()); + + if (stream_info.get_bits_per_sample() != 16 || stream_info.get_channels() > 2) { + ESP_LOGE(TAG, "Incompatible audio stream. Only 16 bits per sample and 1 or 2 channels are supported"); + xEventGroupSetBits(this_source->event_group_, EventGroupBits::TASK_ERROR); + break; + } + + audio_sink.source = this_source; + audio_sink.stream_info = stream_info; + esp_err_t err = decoder->add_sink(&audio_sink); + if (err != ESP_OK) { + ESP_LOGE(TAG, "Failed to add sink: %s", esp_err_to_name(err)); + xEventGroupSetBits(this_source->event_group_, EventGroupBits::TASK_ERROR); + break; + } + } + } + + xEventGroupSetBits(this_source->event_group_, EventGroupBits::TASK_STOPPING); + } while (false); + + // All RAII objects from the do-while block (decoder, audio_sink, etc.) are now destroyed. + + xEventGroupSetBits(this_source->event_group_, EventGroupBits::TASK_STOPPED); + vTaskSuspend(nullptr); // Suspend this task indefinitely until the loop method deletes it +} + +} // namespace esphome::audio_file + +#endif // USE_ESP32 diff --git a/esphome/components/audio_file/media_source/audio_file_media_source.h b/esphome/components/audio_file/media_source/audio_file_media_source.h new file mode 100644 index 0000000000..75e18c13b8 --- /dev/null +++ b/esphome/components/audio_file/media_source/audio_file_media_source.h @@ -0,0 +1,50 @@ +#pragma once + +#include "esphome/core/defines.h" + +#ifdef USE_ESP32 + +#include "esphome/components/audio/audio.h" +#include "esphome/components/audio_file/audio_file.h" +#include "esphome/components/media_source/media_source.h" +#include "esphome/core/component.h" +#include "esphome/core/static_task.h" + +#include +#include + +namespace esphome::audio_file { + +enum class AudioFileDecodingState : uint8_t { + START_TASK, + DECODING, + IDLE, +}; + +class AudioFileMediaSource : public Component, public media_source::MediaSource { + public: + void setup() override; + void loop() override; + void dump_config() override; + + // MediaSource interface implementation + bool play_uri(const std::string &uri) override; + void handle_command(media_source::MediaSourceCommand command) override; + bool can_handle(const std::string &uri) const override { return uri.starts_with("audio-file://"); } + + void set_task_stack_in_psram(bool task_stack_in_psram) { this->task_stack_in_psram_ = task_stack_in_psram; } + + protected: + static void decode_task(void *params); + + audio::AudioFile *current_file_{nullptr}; + AudioFileDecodingState decoding_state_{AudioFileDecodingState::IDLE}; + EventGroupHandle_t event_group_{nullptr}; + StaticTask decode_task_; + + bool task_stack_in_psram_{false}; +}; + +} // namespace esphome::audio_file + +#endif // USE_ESP32 diff --git a/esphome/components/b_parasite/b_parasite.cpp b/esphome/components/b_parasite/b_parasite.cpp index 356f396476..7be26efa7f 100644 --- a/esphome/components/b_parasite/b_parasite.cpp +++ b/esphome/components/b_parasite/b_parasite.cpp @@ -38,6 +38,11 @@ bool BParasite::parse_device(const esp32_ble_tracker::ESPBTDevice &device) { const auto &data = service_data.data; + if (data.size() < 10) { + ESP_LOGW(TAG, "Service data too short: %zu", data.size()); + return false; + } + const uint8_t protocol_version = data[0] >> 4; if (protocol_version != 1 && protocol_version != 2) { ESP_LOGE(TAG, "Unsupported protocol version: %u", protocol_version); @@ -47,6 +52,11 @@ bool BParasite::parse_device(const esp32_ble_tracker::ESPBTDevice &device) { // Some b-parasite versions have an (optional) illuminance sensor. bool has_illuminance = data[0] & 0x1; + if (has_illuminance && data.size() < 18) { + ESP_LOGW(TAG, "Service data too short for illuminance: %zu", data.size()); + return false; + } + // Counter for deduplicating messages. uint8_t counter = data[1] & 0x0f; if (last_processed_counter_ == counter) { diff --git a/esphome/components/ballu/ballu.cpp b/esphome/components/ballu/ballu.cpp index b33ad11c1f..deb742f8c6 100644 --- a/esphome/components/ballu/ballu.cpp +++ b/esphome/components/ballu/ballu.cpp @@ -47,7 +47,7 @@ void BalluClimate::transmit_state() { remote_state[11] = 0x1e; // Fan speed - switch (this->fan_mode.value()) { + switch (this->fan_mode.value_or(climate::CLIMATE_FAN_ON)) { case climate::CLIMATE_FAN_HIGH: remote_state[4] |= BALLU_FAN_HIGH; break; diff --git a/esphome/components/bang_bang/bang_bang_climate.cpp b/esphome/components/bang_bang/bang_bang_climate.cpp index f26377a38a..1058bce6a4 100644 --- a/esphome/components/bang_bang/bang_bang_climate.cpp +++ b/esphome/components/bang_bang/bang_bang_climate.cpp @@ -6,8 +6,7 @@ namespace bang_bang { static const char *const TAG = "bang_bang.climate"; -BangBangClimate::BangBangClimate() - : idle_trigger_(new Trigger<>()), cool_trigger_(new Trigger<>()), heat_trigger_(new Trigger<>()) {} +BangBangClimate::BangBangClimate() = default; void BangBangClimate::setup() { this->sensor_->add_on_state_callback([this](float state) { @@ -46,17 +45,21 @@ void BangBangClimate::setup() { } void BangBangClimate::control(const climate::ClimateCall &call) { - if (call.get_mode().has_value()) { - this->mode = *call.get_mode(); + auto mode = call.get_mode(); + if (mode.has_value()) { + this->mode = *mode; } - if (call.get_target_temperature_low().has_value()) { - this->target_temperature_low = *call.get_target_temperature_low(); + auto target_temperature_low = call.get_target_temperature_low(); + if (target_temperature_low.has_value()) { + this->target_temperature_low = *target_temperature_low; } - if (call.get_target_temperature_high().has_value()) { - this->target_temperature_high = *call.get_target_temperature_high(); + auto target_temperature_high = call.get_target_temperature_high(); + if (target_temperature_high.has_value()) { + this->target_temperature_high = *target_temperature_high; } - if (call.get_preset().has_value()) { - this->change_away_(*call.get_preset() == climate::CLIMATE_PRESET_AWAY); + auto preset = call.get_preset(); + if (preset.has_value()) { + this->change_away_(*preset == climate::CLIMATE_PRESET_AWAY); } this->compute_state_(); @@ -160,13 +163,13 @@ void BangBangClimate::switch_to_action_(climate::ClimateAction action) { switch (action) { case climate::CLIMATE_ACTION_OFF: case climate::CLIMATE_ACTION_IDLE: - trig = this->idle_trigger_; + trig = &this->idle_trigger_; break; case climate::CLIMATE_ACTION_COOLING: - trig = this->cool_trigger_; + trig = &this->cool_trigger_; break; case climate::CLIMATE_ACTION_HEATING: - trig = this->heat_trigger_; + trig = &this->heat_trigger_; break; default: trig = nullptr; @@ -204,9 +207,9 @@ void BangBangClimate::set_away_config(const BangBangClimateTargetTempConfig &awa void BangBangClimate::set_sensor(sensor::Sensor *sensor) { this->sensor_ = sensor; } void BangBangClimate::set_humidity_sensor(sensor::Sensor *humidity_sensor) { this->humidity_sensor_ = humidity_sensor; } -Trigger<> *BangBangClimate::get_idle_trigger() const { return this->idle_trigger_; } -Trigger<> *BangBangClimate::get_cool_trigger() const { return this->cool_trigger_; } -Trigger<> *BangBangClimate::get_heat_trigger() const { return this->heat_trigger_; } +Trigger<> *BangBangClimate::get_idle_trigger() { return &this->idle_trigger_; } +Trigger<> *BangBangClimate::get_cool_trigger() { return &this->cool_trigger_; } +Trigger<> *BangBangClimate::get_heat_trigger() { return &this->heat_trigger_; } void BangBangClimate::set_supports_cool(bool supports_cool) { this->supports_cool_ = supports_cool; } void BangBangClimate::set_supports_heat(bool supports_heat) { this->supports_heat_ = supports_heat; } diff --git a/esphome/components/bang_bang/bang_bang_climate.h b/esphome/components/bang_bang/bang_bang_climate.h index 2e7da93a07..d0ddef2848 100644 --- a/esphome/components/bang_bang/bang_bang_climate.h +++ b/esphome/components/bang_bang/bang_bang_climate.h @@ -30,9 +30,9 @@ class BangBangClimate : public climate::Climate, public Component { void set_normal_config(const BangBangClimateTargetTempConfig &normal_config); void set_away_config(const BangBangClimateTargetTempConfig &away_config); - Trigger<> *get_idle_trigger() const; - Trigger<> *get_cool_trigger() const; - Trigger<> *get_heat_trigger() const; + Trigger<> *get_idle_trigger(); + Trigger<> *get_cool_trigger(); + Trigger<> *get_heat_trigger(); protected: /// Override control to change settings of the climate device. @@ -57,17 +57,13 @@ class BangBangClimate : public climate::Climate, public Component { * * In idle mode, the controller is assumed to have both heating and cooling disabled. */ - Trigger<> *idle_trigger_{nullptr}; + Trigger<> idle_trigger_; /** The trigger to call when the controller should switch to cooling mode. */ - Trigger<> *cool_trigger_{nullptr}; + Trigger<> cool_trigger_; /** The trigger to call when the controller should switch to heating mode. - * - * A null value for this attribute means that the controller has no heating action - * For example window blinds, where only cooling (blinds closed) and not-cooling - * (blinds open) is possible. */ - Trigger<> *heat_trigger_{nullptr}; + Trigger<> heat_trigger_; /** A reference to the trigger that was previously active. * * This is so that the previous trigger can be stopped before enabling a new one. diff --git a/esphome/components/bedjet/bedjet_codec.cpp b/esphome/components/bedjet/bedjet_codec.cpp index 9a312e226c..7a959390f3 100644 --- a/esphome/components/bedjet/bedjet_codec.cpp +++ b/esphome/components/bedjet/bedjet_codec.cpp @@ -1,4 +1,5 @@ #include "bedjet_codec.h" +#include #include #include @@ -68,6 +69,10 @@ BedjetPacket *BedjetCodec::get_set_runtime_remaining_request(const uint8_t hour, /** Decodes the extra bytes that were received after being notified with a partial packet. */ void BedjetCodec::decode_extra(const uint8_t *data, uint16_t length) { + if (length < 5) { + ESP_LOGVV(TAG, "Received extra: %d bytes (too short)", length); + return; + } ESP_LOGVV(TAG, "Received extra: %d bytes: %d %d %d %d", length, data[1], data[2], data[3], data[4]); uint8_t offset = this->last_buffer_size_; if (offset > 0 && length + offset <= sizeof(BedjetStatusPacket)) { @@ -90,14 +95,19 @@ void BedjetCodec::decode_extra(const uint8_t *data, uint16_t length) { * @return `true` if the packet was decoded and represents a "partial" packet; `false` otherwise. */ bool BedjetCodec::decode_notify(const uint8_t *data, uint16_t length) { + if (length < 5) { + ESP_LOGW(TAG, "Received short packet: %d bytes", length); + return false; + } ESP_LOGV(TAG, "Received: %d bytes: %d %d %d %d", length, data[1], data[2], data[3], data[4]); if (data[1] == PACKET_FORMAT_V3_HOME && data[3] == PACKET_TYPE_STATUS) { // Clear old buffer memset(&this->buf_, 0, sizeof(BedjetStatusPacket)); // Copy new data into buffer - memcpy(&this->buf_, data, length); - this->last_buffer_size_ = length; + size_t copy_len = std::min(static_cast(length), sizeof(BedjetStatusPacket)); + memcpy(&this->buf_, data, copy_len); + this->last_buffer_size_ = copy_len; // TODO: validate the packet checksum? if (this->buf_.mode < 7 && this->buf_.target_temp_step >= 38 && this->buf_.target_temp_step <= 86 && @@ -113,13 +123,15 @@ bool BedjetCodec::decode_notify(const uint8_t *data, uint16_t length) { } } else if (data[1] == PACKET_FORMAT_DEBUG || data[3] == PACKET_TYPE_DEBUG) { // We don't actually know the packet format for this. Dump packets to log, in case a pattern presents itself. - ESP_LOGVV(TAG, - "received DEBUG packet: set1=%01fF, set2=%01fF, air=%01fF; [7]=%d, [8]=%d, [9]=%d, [10]=%d, [11]=%d, " - "[12]=%d, [-1]=%d", - bedjet_temp_to_f(data[4]), bedjet_temp_to_f(data[5]), bedjet_temp_to_f(data[6]), data[7], data[8], - data[9], data[10], data[11], data[12], data[length - 1]); + if (length >= 13) { + ESP_LOGVV(TAG, + "received DEBUG packet: set1=%01fF, set2=%01fF, air=%01fF; [7]=%d, [8]=%d, [9]=%d, [10]=%d, [11]=%d, " + "[12]=%d, [-1]=%d", + bedjet_temp_to_f(data[4]), bedjet_temp_to_f(data[5]), bedjet_temp_to_f(data[6]), data[7], data[8], + data[9], data[10], data[11], data[12], data[length - 1]); + } - if (this->has_status()) { + if (this->has_status() && length >= 7) { this->status_packet_->ambient_temp_step = data[6]; } } else { diff --git a/esphome/components/bedjet/bedjet_codec.h b/esphome/components/bedjet/bedjet_codec.h index 07aee32d54..3936ba2315 100644 --- a/esphome/components/bedjet/bedjet_codec.h +++ b/esphome/components/bedjet/bedjet_codec.h @@ -183,7 +183,7 @@ class BedjetCodec { BedjetPacket packet_; - BedjetStatusPacket *status_packet_; + BedjetStatusPacket *status_packet_{nullptr}; BedjetStatusPacket buf_; }; diff --git a/esphome/components/bedjet/bedjet_hub.h b/esphome/components/bedjet/bedjet_hub.h index 6258795b02..59b0af93ad 100644 --- a/esphome/components/bedjet/bedjet_hub.h +++ b/esphome/components/bedjet/bedjet_hub.h @@ -164,10 +164,10 @@ class BedJetHub : public esphome::ble_client::BLEClientNode, public PollingCompo std::unique_ptr codec_; bool discover_characteristics_(); - uint16_t char_handle_cmd_; - uint16_t char_handle_name_; - uint16_t char_handle_status_; - uint16_t config_descr_status_; + uint16_t char_handle_cmd_{0}; + uint16_t char_handle_name_{0}; + uint16_t char_handle_status_{0}; + uint16_t config_descr_status_{0}; uint8_t write_notify_config_descriptor_(bool enable); }; diff --git a/esphome/components/bedjet/climate/bedjet_climate.cpp b/esphome/components/bedjet/climate/bedjet_climate.cpp index 68a0342873..a17407f08f 100644 --- a/esphome/components/bedjet/climate/bedjet_climate.cpp +++ b/esphome/components/bedjet/climate/bedjet_climate.cpp @@ -96,8 +96,9 @@ void BedJetClimate::control(const ClimateCall &call) { return; } - if (call.get_mode().has_value()) { - ClimateMode mode = *call.get_mode(); + auto mode_opt = call.get_mode(); + if (mode_opt.has_value()) { + ClimateMode mode = *mode_opt; bool button_result; switch (mode) { case CLIMATE_MODE_OFF: @@ -125,8 +126,9 @@ void BedJetClimate::control(const ClimateCall &call) { } } - if (call.get_target_temperature().has_value()) { - auto target_temp = *call.get_target_temperature(); + auto target_temp_opt = call.get_target_temperature(); + if (target_temp_opt.has_value()) { + auto target_temp = *target_temp_opt; auto result = this->parent_->set_target_temp(target_temp); if (result) { @@ -134,8 +136,9 @@ void BedJetClimate::control(const ClimateCall &call) { } } - if (call.get_preset().has_value()) { - ClimatePreset preset = *call.get_preset(); + auto preset_opt = call.get_preset(); + if (preset_opt.has_value()) { + ClimatePreset preset = *preset_opt; bool result; if (preset == CLIMATE_PRESET_BOOST) { @@ -187,10 +190,11 @@ void BedJetClimate::control(const ClimateCall &call) { } } - if (call.get_fan_mode().has_value()) { + auto fan_mode_opt = call.get_fan_mode(); + if (fan_mode_opt.has_value()) { // Climate fan mode only supports low/med/high, but the BedJet supports 5-100% increments. // We can still support a ClimateCall that requests low/med/high, and just translate it to a step increment here. - auto fan_mode = *call.get_fan_mode(); + auto fan_mode = *fan_mode_opt; bool result; if (fan_mode == CLIMATE_FAN_LOW) { result = this->parent_->set_fan_speed(20); diff --git a/esphome/components/bedjet/fan/bedjet_fan.cpp b/esphome/components/bedjet/fan/bedjet_fan.cpp index e272241040..9539e169a4 100644 --- a/esphome/components/bedjet/fan/bedjet_fan.cpp +++ b/esphome/components/bedjet/fan/bedjet_fan.cpp @@ -19,7 +19,8 @@ void BedJetFan::control(const fan::FanCall &call) { } bool did_change = false; - if (call.get_state().has_value() && this->state != *call.get_state()) { + auto state_opt = call.get_state(); + if (state_opt.has_value() && this->state != *state_opt) { // Turning off is easy: if (this->state && this->parent_->button_off()) { this->state = false; @@ -36,8 +37,9 @@ void BedJetFan::control(const fan::FanCall &call) { } // ignore speed changes if not on or turning on - if (this->state && call.get_speed().has_value()) { - auto speed = *call.get_speed(); + auto speed_opt = call.get_speed(); + if (this->state && speed_opt.has_value()) { + auto speed = *speed_opt; if (speed >= 1) { this->speed = speed; // Fan.speed is 1-20, but Bedjet expects 0-19, so subtract 1 diff --git a/esphome/components/beken_spi_led_strip/led_strip.cpp b/esphome/components/beken_spi_led_strip/led_strip.cpp index 67b8472257..f425f3ca5c 100644 --- a/esphome/components/beken_spi_led_strip/led_strip.cpp +++ b/esphome/components/beken_spi_led_strip/led_strip.cpp @@ -78,7 +78,7 @@ static void spi_set_clock(uint32_t max_hz) { int source_clk = 0; int spi_clk = 0; int div = 0; - uint32_t param; + uint32_t param = PWD_SPI_CLK_BIT; if (max_hz > 4333000) { if (max_hz > 30000000) { spi_clk = 30000000; diff --git a/esphome/components/bh1750/bh1750.cpp b/esphome/components/bh1750/bh1750.cpp index bd7c667c25..045fb7cf45 100644 --- a/esphome/components/bh1750/bh1750.cpp +++ b/esphome/components/bh1750/bh1750.cpp @@ -265,6 +265,4 @@ void BH1750Sensor::fail_and_reset_() { this->state_ = IDLE; } -float BH1750Sensor::get_setup_priority() const { return setup_priority::DATA; } - } // namespace esphome::bh1750 diff --git a/esphome/components/bh1750/bh1750.h b/esphome/components/bh1750/bh1750.h index 0460427954..39dbd1d6a9 100644 --- a/esphome/components/bh1750/bh1750.h +++ b/esphome/components/bh1750/bh1750.h @@ -21,7 +21,6 @@ class BH1750Sensor : public sensor::Sensor, public PollingComponent, public i2c: void dump_config() override; void update() override; void loop() override; - float get_setup_priority() const override; protected: // State machine states diff --git a/esphome/components/binary/fan/binary_fan.cpp b/esphome/components/binary/fan/binary_fan.cpp index a2f75242de..17d4df095a 100644 --- a/esphome/components/binary/fan/binary_fan.cpp +++ b/esphome/components/binary/fan/binary_fan.cpp @@ -18,12 +18,15 @@ fan::FanTraits BinaryFan::get_traits() { return fan::FanTraits(this->oscillating_ != nullptr, false, this->direction_ != nullptr, 0); } void BinaryFan::control(const fan::FanCall &call) { - if (call.get_state().has_value()) - this->state = *call.get_state(); - if (call.get_oscillating().has_value()) - this->oscillating = *call.get_oscillating(); - if (call.get_direction().has_value()) - this->direction = *call.get_direction(); + auto state = call.get_state(); + if (state.has_value()) + this->state = *state; + auto oscillating = call.get_oscillating(); + if (oscillating.has_value()) + this->oscillating = *oscillating; + auto direction = call.get_direction(); + if (direction.has_value()) + this->direction = *direction; this->write_state_(); this->publish_state(); diff --git a/esphome/components/binary_sensor/__init__.py b/esphome/components/binary_sensor/__init__.py index c38d6b78d3..660f75ccd9 100644 --- a/esphome/components/binary_sensor/__init__.py +++ b/esphome/components/binary_sensor/__init__.py @@ -60,7 +60,11 @@ from esphome.const import ( DEVICE_CLASS_WINDOW, ) from esphome.core import CORE, CoroPriority, coroutine_with_priority -from esphome.core.entity_helpers import entity_duplicate_validator, setup_entity +from esphome.core.entity_helpers import ( + entity_duplicate_validator, + setup_device_class, + setup_entity, +) from esphome.cpp_generator import MockObjClass from esphome.util import Registry @@ -116,25 +120,15 @@ BinarySensorInitiallyOff = binary_sensor_ns.class_( BinarySensorPtr = BinarySensor.operator("ptr") # Triggers -PressTrigger = binary_sensor_ns.class_("PressTrigger", automation.Trigger.template()) -ReleaseTrigger = binary_sensor_ns.class_( - "ReleaseTrigger", automation.Trigger.template() -) ClickTrigger = binary_sensor_ns.class_("ClickTrigger", automation.Trigger.template()) DoubleClickTrigger = binary_sensor_ns.class_( "DoubleClickTrigger", automation.Trigger.template() ) -MultiClickTrigger = binary_sensor_ns.class_( - "MultiClickTrigger", automation.Trigger.template(), cg.Component +MultiClickTriggerBase = binary_sensor_ns.class_( + "MultiClickTriggerBase", automation.Trigger.template(), cg.Component ) +MultiClickTrigger = binary_sensor_ns.class_("MultiClickTrigger", MultiClickTriggerBase) MultiClickTriggerEvent = binary_sensor_ns.struct("MultiClickTriggerEvent") -StateTrigger = binary_sensor_ns.class_( - "StateTrigger", automation.Trigger.template(bool) -) -StateChangeTrigger = binary_sensor_ns.class_( - "StateChangeTrigger", - automation.Trigger.template(cg.optional.template(bool), cg.optional.template(bool)), -) BinarySensorPublishAction = binary_sensor_ns.class_( "BinarySensorPublishAction", automation.Action @@ -262,6 +256,7 @@ async def delayed_off_filter_to_code(config, filter_id): ): cv.positive_time_period_milliseconds, } ), + cv.Length(max=254), ), ) async def autorepeat_filter_to_code(config, filter_id): @@ -290,7 +285,7 @@ async def autorepeat_filter_to_code(config, filter_id): ), ) ] - var = cg.new_Pvariable(filter_id, timings) + var = cg.new_Pvariable(filter_id, cg.TemplateArguments(len(timings)), timings) await cg.register_component(var, {}) return var @@ -454,16 +449,8 @@ _BINARY_SENSOR_SCHEMA = ( ): cv.boolean, cv.Optional(CONF_DEVICE_CLASS): validate_device_class, cv.Optional(CONF_FILTERS): validate_filters, - cv.Optional(CONF_ON_PRESS): automation.validate_automation( - { - cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(PressTrigger), - } - ), - cv.Optional(CONF_ON_RELEASE): automation.validate_automation( - { - cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(ReleaseTrigger), - } - ), + cv.Optional(CONF_ON_PRESS): automation.validate_automation({}), + cv.Optional(CONF_ON_RELEASE): automation.validate_automation({}), cv.Optional(CONF_ON_CLICK): cv.All( automation.validate_automation( { @@ -498,23 +485,17 @@ _BINARY_SENSOR_SCHEMA = ( { cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(MultiClickTrigger), cv.Required(CONF_TIMING): cv.All( - [parse_multi_click_timing_str], validate_multi_click_timing + [parse_multi_click_timing_str], + validate_multi_click_timing, + cv.Length(min=1, max=255), ), cv.Optional( CONF_INVALID_COOLDOWN, default="1s" ): cv.positive_time_period_milliseconds, } ), - cv.Optional(CONF_ON_STATE): automation.validate_automation( - { - cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(StateTrigger), - } - ), - cv.Optional(CONF_ON_STATE_CHANGE): automation.validate_automation( - { - cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(StateChangeTrigger), - } - ), + cv.Optional(CONF_ON_STATE): automation.validate_automation({}), + cv.Optional(CONF_ON_STATE_CHANGE): automation.validate_automation({}), } ) ) @@ -550,28 +531,16 @@ def binary_sensor_schema( return _BINARY_SENSOR_SCHEMA.extend(schema) -async def setup_binary_sensor_core_(var, config): - await setup_entity(var, config, "binary_sensor") - - if (device_class := config.get(CONF_DEVICE_CLASS)) is not None: - cg.add(var.set_device_class(device_class)) - trigger = config.get(CONF_TRIGGER_ON_INITIAL_STATE, False) or config.get( - CONF_PUBLISH_INITIAL_STATE, False - ) - cg.add(var.set_trigger_on_initial_state(trigger)) - if inverted := config.get(CONF_INVERTED): - cg.add(var.set_inverted(inverted)) - if filters_config := config.get(CONF_FILTERS): - filters = await cg.build_registry_list(FILTER_REGISTRY, filters_config) - cg.add(var.add_filters(filters)) - - for conf in config.get(CONF_ON_PRESS, []): - trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) - await automation.build_automation(trigger, [], conf) - - for conf in config.get(CONF_ON_RELEASE, []): - trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) - await automation.build_automation(trigger, [], conf) +@coroutine_with_priority(CoroPriority.AUTOMATION) +async def _build_binary_sensor_automations(var, config): + for conf_key, forwarder in ( + (CONF_ON_PRESS, automation.TriggerOnTrueForwarder), + (CONF_ON_RELEASE, automation.TriggerOnFalseForwarder), + ): + for conf in config.get(conf_key, []): + await automation.build_callback_automation( + var, "add_on_state_callback", [], conf, forwarder=forwarder + ) for conf in config.get(CONF_ON_CLICK, []): trigger = cg.new_Pvariable( @@ -595,20 +564,23 @@ async def setup_binary_sensor_core_(var, config): ) for tim in conf[CONF_TIMING] ] - trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var, timings) + trigger = cg.new_Pvariable( + conf[CONF_TRIGGER_ID], cg.TemplateArguments(len(timings)), var, timings + ) if CONF_INVALID_COOLDOWN in conf: cg.add(trigger.set_invalid_cooldown(conf[CONF_INVALID_COOLDOWN])) await cg.register_component(trigger, conf) await automation.build_automation(trigger, [], conf) for conf in config.get(CONF_ON_STATE, []): - trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) - await automation.build_automation(trigger, [(bool, "x")], conf) + await automation.build_callback_automation( + var, "add_on_state_callback", [(bool, "x")], conf + ) for conf in config.get(CONF_ON_STATE_CHANGE, []): - trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) - await automation.build_automation( - trigger, + await automation.build_callback_automation( + var, + "add_full_state_callback", [ (cg.optional.template(bool), "x_previous"), (cg.optional.template(bool), "x"), @@ -616,6 +588,23 @@ async def setup_binary_sensor_core_(var, config): conf, ) + +@setup_entity("binary_sensor") +async def setup_binary_sensor_core_(var, config): + setup_device_class(config) + trigger = config.get(CONF_TRIGGER_ON_INITIAL_STATE, False) or config.get( + CONF_PUBLISH_INITIAL_STATE, False + ) + cg.add(var.set_trigger_on_initial_state(trigger)) + if inverted := config.get(CONF_INVERTED): + cg.add(var.set_inverted(inverted)) + if filters_config := config.get(CONF_FILTERS): + cg.add_define("USE_BINARY_SENSOR_FILTER") + filters = await cg.build_registry_list(FILTER_REGISTRY, filters_config) + cg.add(var.add_filters(filters)) + + CORE.add_job(_build_binary_sensor_automations, var, config) + if mqtt_id := config.get(CONF_MQTT_ID): mqtt_ = cg.new_Pvariable(mqtt_id, var) await mqtt.register_mqtt_component(mqtt_, config) @@ -677,6 +666,7 @@ async def to_code(config): }, key=CONF_ID, ), + synchronous=True, ) async def binary_sensor_invalidate_state_to_code(config, action_id, template_arg, args): paren = await cg.get_variable(config[CONF_ID]) diff --git a/esphome/components/binary_sensor/automation.cpp b/esphome/components/binary_sensor/automation.cpp index dfe911a2f8..eb68abce3b 100644 --- a/esphome/components/binary_sensor/automation.cpp +++ b/esphome/components/binary_sensor/automation.cpp @@ -5,7 +5,15 @@ namespace esphome::binary_sensor { static const char *const TAG = "binary_sensor.automation"; -void MultiClickTrigger::on_state_(bool state) { +// MultiClickTrigger timeout IDs. +// MultiClickTrigger is its own Component instance, so the scheduler scopes +// IDs by component pointer — no risk of collisions between instances. +constexpr uint32_t MULTICLICK_TRIGGER_ID = 0; +constexpr uint32_t MULTICLICK_COOLDOWN_ID = 1; +constexpr uint32_t MULTICLICK_IS_VALID_ID = 2; +constexpr uint32_t MULTICLICK_IS_NOT_VALID_ID = 3; + +void MultiClickTriggerBase::on_state_(bool state) { // Handle duplicate events if (state == this->last_state_) { return; @@ -21,13 +29,11 @@ void MultiClickTrigger::on_state_(bool state) { // Start matching MultiClickTriggerEvent evt = this->timing_[0]; if (evt.state == state) { - ESP_LOGV(TAG, - "START min=%" PRIu32 " max=%" PRIu32 "\n" - "Multi Click: Starting multi click action!", - evt.min_length, evt.max_length); + ESP_LOGV(TAG, "START min=%" PRIu32 " max=%" PRIu32, evt.min_length, evt.max_length); + ESP_LOGV(TAG, "Multi Click: Starting multi click action!"); this->at_index_ = 1; - if (this->timing_.size() == 1 && evt.max_length == 4294967294UL) { - this->set_timeout("trigger", evt.min_length, [this]() { this->trigger_(); }); + if (this->timing_count_ == 1 && evt.max_length == 4294967294UL) { + this->set_timeout(MULTICLICK_TRIGGER_ID, evt.min_length, [this]() { this->trigger_(); }); } else { this->schedule_is_valid_(evt.min_length); this->schedule_is_not_valid_(evt.max_length); @@ -44,7 +50,7 @@ void MultiClickTrigger::on_state_(bool state) { return; } - if (*this->at_index_ == this->timing_.size()) { + if (*this->at_index_ == this->timing_count_) { this->trigger_(); return; } @@ -55,60 +61,60 @@ void MultiClickTrigger::on_state_(bool state) { ESP_LOGV(TAG, "A i=%zu min=%" PRIu32 " max=%" PRIu32, *this->at_index_, evt.min_length, evt.max_length); // NOLINT this->schedule_is_valid_(evt.min_length); this->schedule_is_not_valid_(evt.max_length); - } else if (*this->at_index_ + 1 != this->timing_.size()) { + } else if (*this->at_index_ + 1 != this->timing_count_) { ESP_LOGV(TAG, "B i=%zu min=%" PRIu32, *this->at_index_, evt.min_length); // NOLINT - this->cancel_timeout("is_not_valid"); + this->cancel_timeout(MULTICLICK_IS_NOT_VALID_ID); this->schedule_is_valid_(evt.min_length); } else { ESP_LOGV(TAG, "C i=%zu min=%" PRIu32, *this->at_index_, evt.min_length); // NOLINT this->is_valid_ = false; - this->cancel_timeout("is_not_valid"); - this->set_timeout("trigger", evt.min_length, [this]() { this->trigger_(); }); + this->cancel_timeout(MULTICLICK_IS_NOT_VALID_ID); + this->set_timeout(MULTICLICK_TRIGGER_ID, evt.min_length, [this]() { this->trigger_(); }); } *this->at_index_ = *this->at_index_ + 1; } -void MultiClickTrigger::schedule_cooldown_() { +void MultiClickTriggerBase::schedule_cooldown_() { ESP_LOGV(TAG, "Multi Click: Invalid length of press, starting cooldown of %" PRIu32 " ms", this->invalid_cooldown_); this->is_in_cooldown_ = true; - this->set_timeout("cooldown", this->invalid_cooldown_, [this]() { + this->set_timeout(MULTICLICK_COOLDOWN_ID, this->invalid_cooldown_, [this]() { ESP_LOGV(TAG, "Multi Click: Cooldown ended, matching is now enabled again."); this->is_in_cooldown_ = false; }); this->at_index_.reset(); - this->cancel_timeout("trigger"); - this->cancel_timeout("is_valid"); - this->cancel_timeout("is_not_valid"); + this->cancel_timeout(MULTICLICK_TRIGGER_ID); + this->cancel_timeout(MULTICLICK_IS_VALID_ID); + this->cancel_timeout(MULTICLICK_IS_NOT_VALID_ID); } -void MultiClickTrigger::schedule_is_valid_(uint32_t min_length) { +void MultiClickTriggerBase::schedule_is_valid_(uint32_t min_length) { if (min_length == 0) { this->is_valid_ = true; return; } this->is_valid_ = false; - this->set_timeout("is_valid", min_length, [this]() { + this->set_timeout(MULTICLICK_IS_VALID_ID, min_length, [this]() { ESP_LOGV(TAG, "Multi Click: You can now %s the button.", this->parent_->state ? "RELEASE" : "PRESS"); this->is_valid_ = true; }); } -void MultiClickTrigger::schedule_is_not_valid_(uint32_t max_length) { - this->set_timeout("is_not_valid", max_length, [this]() { +void MultiClickTriggerBase::schedule_is_not_valid_(uint32_t max_length) { + this->set_timeout(MULTICLICK_IS_NOT_VALID_ID, max_length, [this]() { ESP_LOGV(TAG, "Multi Click: You waited too long to %s.", this->parent_->state ? "RELEASE" : "PRESS"); this->is_valid_ = false; this->schedule_cooldown_(); }); } -void MultiClickTrigger::cancel() { +void MultiClickTriggerBase::cancel() { ESP_LOGV(TAG, "Multi Click: Sequence explicitly cancelled."); this->is_valid_ = false; this->schedule_cooldown_(); } -void MultiClickTrigger::trigger_() { +void MultiClickTriggerBase::trigger_() { ESP_LOGV(TAG, "Multi Click: Hooray, multi click is valid. Triggering!"); this->at_index_.reset(); - this->cancel_timeout("trigger"); - this->cancel_timeout("is_valid"); - this->cancel_timeout("is_not_valid"); + this->cancel_timeout(MULTICLICK_TRIGGER_ID); + this->cancel_timeout(MULTICLICK_IS_VALID_ID); + this->cancel_timeout(MULTICLICK_IS_NOT_VALID_ID); this->trigger(); } diff --git a/esphome/components/binary_sensor/automation.h b/esphome/components/binary_sensor/automation.h index f8b130e08a..1875910aff 100644 --- a/esphome/components/binary_sensor/automation.h +++ b/esphome/components/binary_sensor/automation.h @@ -1,5 +1,6 @@ #pragma once +#include #include #include @@ -89,15 +90,14 @@ class DoubleClickTrigger : public Trigger<> { uint32_t max_length_; /// Maximum length of click. 0 means no maximum. }; -class MultiClickTrigger : public Trigger<>, public Component { +/// Non-template base for MultiClickTrigger (keeps large method bodies out of the header). +class MultiClickTriggerBase : public Trigger<>, public Component { public: - explicit MultiClickTrigger(BinarySensor *parent, std::initializer_list timing) - : parent_(parent), timing_(timing) {} + explicit MultiClickTriggerBase(BinarySensor *parent) : parent_(parent) {} void setup() override { this->last_state_ = this->parent_->get_state_default(false); - auto f = std::bind(&MultiClickTrigger::on_state_, this, std::placeholders::_1); - this->parent_->add_on_state_callback(f); + this->parent_->add_on_state_callback([this](bool state) { this->on_state_(state); }); } float get_setup_priority() const override { return setup_priority::HARDWARE; } @@ -105,6 +105,8 @@ class MultiClickTrigger : public Trigger<>, public Component { void set_invalid_cooldown(uint32_t invalid_cooldown) { this->invalid_cooldown_ = invalid_cooldown; } void cancel(); + MultiClickTriggerBase(const MultiClickTriggerBase &) = delete; + MultiClickTriggerBase &operator=(const MultiClickTriggerBase &) = delete; protected: void on_state_(bool state); @@ -114,14 +116,30 @@ class MultiClickTrigger : public Trigger<>, public Component { void trigger_(); BinarySensor *parent_; - FixedVector timing_; + const MultiClickTriggerEvent *timing_{nullptr}; uint32_t invalid_cooldown_{1000}; optional at_index_{}; + uint8_t timing_count_{0}; bool last_state_{false}; bool is_in_cooldown_{false}; bool is_valid_{false}; }; +/// Template wrapper that provides inline std::array storage for timing events. +/// N is set by code generation to match the exact number of timing events configured in YAML. +template class MultiClickTrigger : public MultiClickTriggerBase { + public: + MultiClickTrigger(BinarySensor *parent, std::initializer_list timing) + : MultiClickTriggerBase(parent) { + init_array_from(this->timing_storage_, timing); + this->timing_ = this->timing_storage_.data(); + this->timing_count_ = N; + } + + protected: + std::array timing_storage_{}; +}; + class StateTrigger : public Trigger { public: explicit StateTrigger(BinarySensor *parent) { diff --git a/esphome/components/binary_sensor/binary_sensor.cpp b/esphome/components/binary_sensor/binary_sensor.cpp index 7c3b06970d..7596975a68 100644 --- a/esphome/components/binary_sensor/binary_sensor.cpp +++ b/esphome/components/binary_sensor/binary_sensor.cpp @@ -18,35 +18,33 @@ void log_binary_sensor(const char *tag, const char *prefix, const char *type, Bi } void BinarySensor::publish_state(bool new_state) { +#ifdef USE_BINARY_SENSOR_FILTER if (this->filter_list_ == nullptr) { +#endif this->send_state_internal(new_state); +#ifdef USE_BINARY_SENSOR_FILTER } else { this->filter_list_->input(new_state); } +#endif } void BinarySensor::publish_initial_state(bool new_state) { this->invalidate_state(); this->publish_state(new_state); } -void BinarySensor::send_state_internal(bool new_state) { - // copy the new state to the visible property for backwards compatibility, before any callbacks - this->state = new_state; - // Note that set_new_state_ de-dups and will only trigger callbacks if the state has actually changed - this->set_new_state(new_state); -} - bool BinarySensor::set_new_state(const optional &new_state) { if (StatefulEntityBase::set_new_state(new_state)) { // weirdly, this file could be compiled even without USE_BINARY_SENSOR defined #if defined(USE_BINARY_SENSOR) && defined(USE_CONTROLLER_REGISTRY) ControllerRegistry::notify_binary_sensor_update(this); #endif - ESP_LOGD(TAG, "'%s' >> %s", this->get_name().c_str(), ONOFFMAYBE(new_state)); + ESP_LOGV(TAG, "'%s' >> %s", this->get_name().c_str(), ONOFFMAYBE(new_state)); return true; } return false; } +#ifdef USE_BINARY_SENSOR_FILTER void BinarySensor::add_filter(Filter *filter) { filter->parent_ = this; if (this->filter_list_ == nullptr) { @@ -63,6 +61,7 @@ void BinarySensor::add_filters(std::initializer_list filters) { this->add_filter(filter); } } +#endif // USE_BINARY_SENSOR_FILTER bool BinarySensor::is_status_binary_sensor() const { return false; } } // namespace esphome::binary_sensor diff --git a/esphome/components/binary_sensor/binary_sensor.h b/esphome/components/binary_sensor/binary_sensor.h index 83c992bfed..28c156763a 100644 --- a/esphome/components/binary_sensor/binary_sensor.h +++ b/esphome/components/binary_sensor/binary_sensor.h @@ -2,7 +2,9 @@ #include "esphome/core/entity_base.h" #include "esphome/core/helpers.h" +#ifdef USE_BINARY_SENSOR_FILTER #include "esphome/components/binary_sensor/filter.h" +#endif #include @@ -28,9 +30,12 @@ void log_binary_sensor(const char *tag, const char *prefix, const char *type, Bi * The sub classes should notify the front-end of new states via the publish_state() method which * handles inverted inputs for you. */ -class BinarySensor : public StatefulEntityBase, public EntityBase_DeviceClass { +class BinarySensor : public StatefulEntityBase { public: - explicit BinarySensor(){}; + explicit BinarySensor() = default; + + const bool &get_state() const override { return this->state; } + void set_trigger_on_initial_state(bool value) { this->trigger_on_initial_state_ = value; } /** Publish a new state to the front-end. * @@ -45,29 +50,41 @@ class BinarySensor : public StatefulEntityBase, public EntityBase_DeviceCl */ void publish_initial_state(bool new_state); +#ifdef USE_BINARY_SENSOR_FILTER void add_filter(Filter *filter); void add_filters(std::initializer_list filters); +#endif // ========== INTERNAL METHODS ========== // (In most use cases you won't need these) - void send_state_internal(bool new_state); + void send_state_internal(bool new_state) { + // Fast path: skip virtual dispatch when state hasn't changed + if (this->flags_.has_state && this->state == new_state) + return; + this->set_new_state(new_state); + } /// Return whether this binary sensor has outputted a state. virtual bool is_status_binary_sensor() const; - // For backward compatibility, provide an accessible property - + /// The current state of this binary sensor. Also used as the backing storage for StatefulEntityBase. bool state{}; protected: + bool get_trigger_on_initial_state() const override { return this->trigger_on_initial_state_; } + void set_state_value(const bool &value) override { this->state = value; } + + bool trigger_on_initial_state_{true}; +#ifdef USE_BINARY_SENSOR_FILTER Filter *filter_list_{nullptr}; +#endif bool set_new_state(const optional &new_state) override; }; class BinarySensorInitiallyOff : public BinarySensor { public: - bool has_state() const override { return true; } + BinarySensorInitiallyOff() { this->set_has_state(true); } }; } // namespace esphome::binary_sensor diff --git a/esphome/components/binary_sensor/filter.cpp b/esphome/components/binary_sensor/filter.cpp index 9c7238f6d7..914060ce13 100644 --- a/esphome/components/binary_sensor/filter.cpp +++ b/esphome/components/binary_sensor/filter.cpp @@ -1,3 +1,6 @@ +#include "esphome/core/defines.h" +#ifdef USE_BINARY_SENSOR_FILTER + #include "filter.h" #include "binary_sensor.h" @@ -6,6 +9,14 @@ namespace esphome::binary_sensor { static const char *const TAG = "sensor.filter"; +// Timeout IDs for filter classes. +// Each filter is its own Component instance, so the scheduler scopes +// IDs by component pointer — no risk of collisions between instances. +constexpr uint32_t FILTER_TIMEOUT_ID = 0; +// AutorepeatFilter needs two distinct IDs (both timeouts on the same component) +constexpr uint32_t AUTOREPEAT_TIMING_ID = 0; +constexpr uint32_t AUTOREPEAT_ON_OFF_ID = 1; + void Filter::output(bool value) { if (this->next_ == nullptr) { this->parent_->send_state_internal(value); @@ -23,16 +34,16 @@ void Filter::input(bool value) { } void TimeoutFilter::input(bool value) { - this->set_timeout("timeout", this->timeout_delay_.value(), [this]() { this->parent_->invalidate_state(); }); + this->set_timeout(FILTER_TIMEOUT_ID, this->timeout_delay_.value(), [this]() { this->parent_->invalidate_state(); }); // we do not de-dup here otherwise changes from invalid to valid state will not be output this->output(value); } optional DelayedOnOffFilter::new_value(bool value) { if (value) { - this->set_timeout("ON_OFF", this->on_delay_.value(), [this]() { this->output(true); }); + this->set_timeout(FILTER_TIMEOUT_ID, this->on_delay_.value(), [this]() { this->output(true); }); } else { - this->set_timeout("ON_OFF", this->off_delay_.value(), [this]() { this->output(false); }); + this->set_timeout(FILTER_TIMEOUT_ID, this->off_delay_.value(), [this]() { this->output(false); }); } return {}; } @@ -41,10 +52,10 @@ float DelayedOnOffFilter::get_setup_priority() const { return setup_priority::HA optional DelayedOnFilter::new_value(bool value) { if (value) { - this->set_timeout("ON", this->delay_.value(), [this]() { this->output(true); }); + this->set_timeout(FILTER_TIMEOUT_ID, this->delay_.value(), [this]() { this->output(true); }); return {}; } else { - this->cancel_timeout("ON"); + this->cancel_timeout(FILTER_TIMEOUT_ID); return false; } } @@ -53,10 +64,10 @@ float DelayedOnFilter::get_setup_priority() const { return setup_priority::HARDW optional DelayedOffFilter::new_value(bool value) { if (!value) { - this->set_timeout("OFF", this->delay_.value(), [this]() { this->output(false); }); + this->set_timeout(FILTER_TIMEOUT_ID, this->delay_.value(), [this]() { this->output(false); }); return {}; } else { - this->cancel_timeout("OFF"); + this->cancel_timeout(FILTER_TIMEOUT_ID); return true; } } @@ -65,49 +76,41 @@ float DelayedOffFilter::get_setup_priority() const { return setup_priority::HARD optional InvertFilter::new_value(bool value) { return !value; } -AutorepeatFilter::AutorepeatFilter(std::initializer_list timings) : timings_(timings) {} - -optional AutorepeatFilter::new_value(bool value) { +// AutorepeatFilterBase +optional AutorepeatFilterBase::new_value(bool value) { if (value) { - // Ignore if already running if (this->active_timing_ != 0) return {}; - this->next_timing_(); return true; } else { - this->cancel_timeout("TIMING"); - this->cancel_timeout("ON_OFF"); + this->cancel_timeout(AUTOREPEAT_TIMING_ID); + this->cancel_timeout(AUTOREPEAT_ON_OFF_ID); this->active_timing_ = 0; return false; } } -void AutorepeatFilter::next_timing_() { - // Entering this method - // 1st time: starts waiting the first delay - // 2nd time: starts waiting the second delay and starts toggling with the first time_off / _on - // last time: no delay to start but have to bump the index to reflect the last - if (this->active_timing_ < this->timings_.size()) - this->set_timeout("TIMING", this->timings_[this->active_timing_].delay, [this]() { this->next_timing_(); }); - - if (this->active_timing_ <= this->timings_.size()) { +void AutorepeatFilterBase::next_timing_() { + if (this->active_timing_ < this->timings_count_) { + this->set_timeout(AUTOREPEAT_TIMING_ID, this->timings_[this->active_timing_].delay, + [this]() { this->next_timing_(); }); + } + if (this->active_timing_ <= this->timings_count_) { this->active_timing_++; } - if (this->active_timing_ == 2) this->next_value_(false); - - // Leaving this method: if the toggling is started, it has to use [active_timing_ - 2] for the intervals } -void AutorepeatFilter::next_value_(bool val) { +void AutorepeatFilterBase::next_value_(bool val) { const AutorepeatFilterTiming &timing = this->timings_[this->active_timing_ - 2]; - this->output(val); // This is at least the second one so not initial - this->set_timeout("ON_OFF", val ? timing.time_on : timing.time_off, [this, val]() { this->next_value_(!val); }); + this->output(val); + this->set_timeout(AUTOREPEAT_ON_OFF_ID, val ? timing.time_on : timing.time_off, + [this, val]() { this->next_value_(!val); }); } -float AutorepeatFilter::get_setup_priority() const { return setup_priority::HARDWARE; } +float AutorepeatFilterBase::get_setup_priority() const { return setup_priority::HARDWARE; } LambdaFilter::LambdaFilter(std::function(bool)> f) : f_(std::move(f)) {} @@ -115,15 +118,14 @@ optional LambdaFilter::new_value(bool value) { return this->f_(value); } optional SettleFilter::new_value(bool value) { if (!this->steady_) { - this->set_timeout("SETTLE", this->delay_.value(), [this, value]() { + this->set_timeout(FILTER_TIMEOUT_ID, this->delay_.value(), [this, value]() { this->steady_ = true; this->output(value); }); return {}; } else { this->steady_ = false; - this->output(value); - this->set_timeout("SETTLE", this->delay_.value(), [this]() { this->steady_ = true; }); + this->set_timeout(FILTER_TIMEOUT_ID, this->delay_.value(), [this]() { this->steady_ = true; }); return value; } } @@ -131,3 +133,5 @@ optional SettleFilter::new_value(bool value) { float SettleFilter::get_setup_priority() const { return setup_priority::HARDWARE; } } // namespace esphome::binary_sensor + +#endif // USE_BINARY_SENSOR_FILTER diff --git a/esphome/components/binary_sensor/filter.h b/esphome/components/binary_sensor/filter.h index 59bc43eeba..37c6bf0092 100644 --- a/esphome/components/binary_sensor/filter.h +++ b/esphome/components/binary_sensor/filter.h @@ -1,5 +1,10 @@ #pragma once +#include "esphome/core/defines.h" +#ifdef USE_BINARY_SENSOR_FILTER + +#include + #include "esphome/core/automation.h" #include "esphome/core/component.h" #include "esphome/core/helpers.h" @@ -34,7 +39,7 @@ class TimeoutFilter : public Filter, public Component { TemplatableValue timeout_delay_{}; }; -class DelayedOnOffFilter : public Filter, public Component { +class DelayedOnOffFilter final : public Filter, public Component { public: optional new_value(bool value) override; @@ -83,22 +88,39 @@ struct AutorepeatFilterTiming { uint32_t time_on; }; -class AutorepeatFilter : public Filter, public Component { +/// Non-template base for AutorepeatFilter — all methods in filter.cpp. +/// Lambdas capture this base pointer, so set_timeout/cancel_timeout are instantiated once. +class AutorepeatFilterBase : public Filter, public Component { public: - explicit AutorepeatFilter(std::initializer_list timings); - optional new_value(bool value) override; - float get_setup_priority() const override; + AutorepeatFilterBase(const AutorepeatFilterBase &) = delete; + AutorepeatFilterBase &operator=(const AutorepeatFilterBase &) = delete; protected: + AutorepeatFilterBase() = default; void next_timing_(); void next_value_(bool val); - FixedVector timings_; + const AutorepeatFilterTiming *timings_{nullptr}; + uint8_t timings_count_{0}; uint8_t active_timing_{0}; }; +/// Template wrapper that provides inline std::array storage for timings. +/// N is set by code generation to match the exact number of timings configured in YAML. +template class AutorepeatFilter : public AutorepeatFilterBase { + public: + explicit AutorepeatFilter(std::initializer_list timings) { + init_array_from(this->timings_storage_, timings); + this->timings_ = this->timings_storage_.data(); + this->timings_count_ = N; + } + + protected: + std::array timings_storage_{}; +}; + class LambdaFilter : public Filter { public: explicit LambdaFilter(std::function(bool)> f); @@ -138,3 +160,5 @@ class SettleFilter : public Filter, public Component { }; } // namespace esphome::binary_sensor + +#endif // USE_BINARY_SENSOR_FILTER diff --git a/esphome/components/bk72xx/boards.py b/esphome/components/bk72xx/boards.py index 3850dbe266..4bee69fe6d 100644 --- a/esphome/components/bk72xx/boards.py +++ b/esphome/components/bk72xx/boards.py @@ -159,6 +159,10 @@ BK72XX_BOARD_PINS = { "A0": 23, }, "cbu": { + "SPI0_CS": 15, + "SPI0_MISO": 17, + "SPI0_MOSI": 16, + "SPI0_SCK": 14, "WIRE1_SCL": 20, "WIRE1_SDA": 21, "WIRE2_SCL": 0, @@ -227,6 +231,10 @@ BK72XX_BOARD_PINS = { "A0": 23, }, "generic-bk7231t-qfn32-tuya": { + "SPI0_CS": 15, + "SPI0_MISO": 17, + "SPI0_MOSI": 16, + "SPI0_SCK": 14, "WIRE1_SCL": 20, "WIRE1_SDA": 21, "WIRE2_SCL": 0, @@ -295,6 +303,10 @@ BK72XX_BOARD_PINS = { "A0": 23, }, "generic-bk7231n-qfn32-tuya": { + "SPI0_CS": 15, + "SPI0_MISO": 17, + "SPI0_MOSI": 16, + "SPI0_SCK": 14, "WIRE1_SCL": 20, "WIRE1_SDA": 21, "WIRE2_SCL": 0, @@ -485,8 +497,7 @@ BK72XX_BOARD_PINS = { }, "cb3s": { "WIRE1_SCL": 20, - "WIRE1_SDA_0": 21, - "WIRE1_SDA_1": 21, + "WIRE1_SDA": 21, "SERIAL1_RX": 10, "SERIAL1_TX": 11, "SERIAL2_TX": 0, @@ -647,6 +658,10 @@ BK72XX_BOARD_PINS = { "A0": 23, }, "generic-bk7252": { + "SPI0_CS": 15, + "SPI0_MISO": 17, + "SPI0_MOSI": 16, + "SPI0_SCK": 14, "WIRE1_SCL": 20, "WIRE1_SDA": 21, "WIRE2_SCL": 0, @@ -1096,6 +1111,10 @@ BK72XX_BOARD_PINS = { "A0": 23, }, "cb3se": { + "SPI0_CS": 15, + "SPI0_MISO": 17, + "SPI0_MOSI": 16, + "SPI0_SCK": 14, "WIRE2_SCL": 0, "WIRE2_SDA": 1, "SERIAL1_RX": 10, diff --git a/esphome/components/bl0906/bl0906.cpp b/esphome/components/bl0906/bl0906.cpp index c1cd48a1ac..70db235a37 100644 --- a/esphome/components/bl0906/bl0906.cpp +++ b/esphome/components/bl0906/bl0906.cpp @@ -10,7 +10,9 @@ static const char *const TAG = "bl0906"; constexpr uint32_t to_uint32_t(ube24_t input) { return input.h << 16 | input.m << 8 | input.l; } -constexpr int32_t to_int32_t(sbe24_t input) { return input.h << 16 | input.m << 8 | input.l; } +constexpr int32_t to_int32_t(sbe24_t input) { + return static_cast(encode_uint32((uint8_t) input.h, input.m, input.l, 0)) >> 8; +} // The SUM byte is (Addr+Data_L+Data_M+Data_H)&0xFF negated; constexpr uint8_t bl0906_checksum(const uint8_t address, const DataPacket *data) { @@ -136,23 +138,24 @@ void BL0906::read_data_(const uint8_t address, const float reference, sensor::Se this->write_byte(BL0906_READ_COMMAND); this->write_byte(address); - if (this->read_array((uint8_t *) &buffer, sizeof(buffer) - 1)) { - if (bl0906_checksum(address, &buffer) == buffer.checksum) { - if (signed_result) { - data_s24.l = buffer.l; - data_s24.m = buffer.m; - data_s24.h = buffer.h; - } else { - data_u24.l = buffer.l; - data_u24.m = buffer.m; - data_u24.h = buffer.h; - } - } else { - ESP_LOGW(TAG, "Junk on wire. Throwing away partial message"); - while (read() >= 0) - ; - return; - } + if (!this->read_array((uint8_t *) &buffer, sizeof(buffer) - 1)) { + ESP_LOGW(TAG, "Read failed"); + return; + } + if (bl0906_checksum(address, &buffer) != buffer.checksum) { + ESP_LOGW(TAG, "Junk on wire. Throwing away partial message"); + while (read() >= 0) + ; + return; + } + if (signed_result) { + data_s24.l = buffer.l; + data_s24.m = buffer.m; + data_s24.h = buffer.h; + } else { + data_u24.l = buffer.l; + data_u24.m = buffer.m; + data_u24.h = buffer.h; } // Power if (reference == BL0906_PREF) { @@ -188,11 +191,9 @@ void BL0906::bias_correction_(uint8_t address, float measurements, float correct float i_rms0 = measurements * ki; float i_rms = correction * ki; int32_t value = (i_rms * i_rms - i_rms0 * i_rms0) / 256; - data.l = value << 24 >> 24; - data.m = value << 16 >> 24; - if (value < 0) { - data.h = (value << 8 >> 24) | 0b10000000; - } + data.l = value & 0xFF; + data.m = (value >> 8) & 0xFF; + data.h = (value >> 16) & 0xFF; data.address = bl0906_checksum(address, &data); ESP_LOGV(TAG, "RMSOS:%02X%02X%02X%02X%02X%02X", BL0906_WRITE_COMMAND, address, data.l, data.m, data.h, data.address); this->write_byte(BL0906_WRITE_COMMAND); diff --git a/esphome/components/bl0906/sensor.py b/esphome/components/bl0906/sensor.py index 42c6f06092..059e10e962 100644 --- a/esphome/components/bl0906/sensor.py +++ b/esphome/components/bl0906/sensor.py @@ -143,6 +143,7 @@ FINAL_VALIDATE_SCHEMA = uart.final_validate_device_schema( cv.Required(CONF_ID): cv.use_id(BL0906), } ), + synchronous=True, ) async def reset_energy_to_code(config, action_id, template_arg, args): var = cg.new_Pvariable(action_id, template_arg) diff --git a/esphome/components/bl0940/bl0940.cpp b/esphome/components/bl0940/bl0940.cpp index 42e20eb69b..31625ebf6d 100644 --- a/esphome/components/bl0940/bl0940.cpp +++ b/esphome/components/bl0940/bl0940.cpp @@ -182,7 +182,10 @@ void BL0940::recalibrate_() { ESP_LOGD(TAG, "Recalibrated reference values:\n" - "Voltage: %f\n, Current: %f\n, Power: %f\n, Energy: %f\n", + " Voltage: %f\n" + " Current: %f\n" + " Power: %f\n" + " Energy: %f", this->voltage_reference_cal_, this->current_reference_cal_, this->power_reference_cal_, this->energy_reference_cal_); } diff --git a/esphome/components/bl0940/bl0940.h b/esphome/components/bl0940/bl0940.h index 93d54003f5..e0ca748a22 100644 --- a/esphome/components/bl0940/bl0940.h +++ b/esphome/components/bl0940/bl0940.h @@ -69,10 +69,8 @@ class BL0940 : public PollingComponent, public uart::UARTDevice { void set_energy_calibration_number(number::Number *num) { this->energy_calibration_number_ = num; } #endif -#ifdef USE_BUTTON - // Resets all calibration values to defaults (can be triggered by a button) + // Resets all calibration values to defaults void reset_calibration(); -#endif // Core component methods void loop() override; diff --git a/esphome/components/bl0940/sensor.py b/esphome/components/bl0940/sensor.py index d2e0ea435d..992064943b 100644 --- a/esphome/components/bl0940/sensor.py +++ b/esphome/components/bl0940/sensor.py @@ -124,9 +124,9 @@ def set_reference_values(config): config.setdefault(CONF_VOLTAGE_REFERENCE, DEFAULT_BL0940_LEGACY_UREF) config.setdefault(CONF_CURRENT_REFERENCE, DEFAULT_BL0940_LEGACY_IREF) config.setdefault(CONF_POWER_REFERENCE, DEFAULT_BL0940_LEGACY_PREF) - config.setdefault(CONF_ENERGY_REFERENCE, DEFAULT_BL0940_LEGACY_PREF) + config.setdefault(CONF_ENERGY_REFERENCE, DEFAULT_BL0940_LEGACY_EREF) else: - vref = config.get(CONF_VOLTAGE_REFERENCE, DEFAULT_BL0940_VREF) + vref = config.get(CONF_REFERENCE_VOLTAGE, DEFAULT_BL0940_VREF) r_one = config.get(CONF_RESISTOR_ONE, DEFAULT_BL0940_R1) r_two = config.get(CONF_RESISTOR_TWO, DEFAULT_BL0940_R2) r_shunt = config.get(CONF_RESISTOR_SHUNT, DEFAULT_BL0940_RL) diff --git a/esphome/components/bl0942/bl0942.cpp b/esphome/components/bl0942/bl0942.cpp index 95dd689b07..7d38597423 100644 --- a/esphome/components/bl0942/bl0942.cpp +++ b/esphome/components/bl0942/bl0942.cpp @@ -46,18 +46,18 @@ static const uint32_t PKT_TIMEOUT_MS = 200; void BL0942::loop() { DataPacket buffer; - int avail = this->available(); + size_t avail = this->available(); if (!avail) { return; } - if (static_cast(avail) < sizeof(buffer)) { - if (!this->rx_start_) { + if (avail < sizeof(buffer)) { + if (!this->rx_start_.has_value()) { this->rx_start_ = millis(); - } else if (millis() > this->rx_start_ + PKT_TIMEOUT_MS) { - ESP_LOGW(TAG, "Junk on wire. Throwing away partial message (%d bytes)", avail); + } else if (millis() - *this->rx_start_ > PKT_TIMEOUT_MS) { + ESP_LOGW(TAG, "Junk on wire. Throwing away partial message (%zu bytes)", avail); this->read_array((uint8_t *) &buffer, avail); - this->rx_start_ = 0; + this->rx_start_.reset(); } return; } @@ -67,7 +67,7 @@ void BL0942::loop() { this->received_package_(&buffer); } } - this->rx_start_ = 0; + this->rx_start_.reset(); } bool BL0942::validate_checksum_(DataPacket *data) { @@ -173,7 +173,7 @@ void BL0942::received_package_(DataPacket *data) { float i_rms = (uint24_t) data->i_rms / current_reference_; float watt = (int24_t) data->watt / power_reference_; float total_energy_consumption = cf_cnt / energy_reference_; - float frequency = 1000000.0f / data->frequency; + float frequency = data->frequency != 0 ? 1000000.0f / data->frequency : NAN; if (voltage_sensor_ != nullptr) { voltage_sensor_->publish_state(v_rms); diff --git a/esphome/components/bl0942/bl0942.h b/esphome/components/bl0942/bl0942.h index 37b884e6ca..3c013f86e7 100644 --- a/esphome/components/bl0942/bl0942.h +++ b/esphome/components/bl0942/bl0942.h @@ -59,10 +59,10 @@ namespace bl0942 { // // Which makes BL0952_EREF = BL0942_PREF * 3600000 / 419430.4 -static const float BL0942_PREF = 596; // taken from tasmota -static const float BL0942_UREF = 15873.35944299; // should be 73989/1.218 -static const float BL0942_IREF = 251213.46469622; // 305978/1.218 -static const float BL0942_EREF = 3304.61127328; // Measured +static const float BL0942_PREF = 623.0270705; // calculated using UREF and IREF +static const float BL0942_UREF = 15883.34116; // calculated for (390k x 5 / 510R) voltage divider +static const float BL0942_IREF = 251065.6814; // calculated for 1mR shunt +static const float BL0942_EREF = 5347.484240; // calculated using UREF and IREF struct DataPacket { uint8_t frame_header; @@ -86,11 +86,11 @@ enum LineFrequency : uint8_t { class BL0942 : public PollingComponent, public uart::UARTDevice { public: - void set_voltage_sensor(sensor::Sensor *voltage_sensor) { voltage_sensor_ = voltage_sensor; } - void set_current_sensor(sensor::Sensor *current_sensor) { current_sensor_ = current_sensor; } - void set_power_sensor(sensor::Sensor *power_sensor) { power_sensor_ = power_sensor; } - void set_energy_sensor(sensor::Sensor *energy_sensor) { energy_sensor_ = energy_sensor; } - void set_frequency_sensor(sensor::Sensor *frequency_sensor) { frequency_sensor_ = frequency_sensor; } + void set_voltage_sensor(sensor::Sensor *voltage_sensor) { this->voltage_sensor_ = voltage_sensor; } + void set_current_sensor(sensor::Sensor *current_sensor) { this->current_sensor_ = current_sensor; } + void set_power_sensor(sensor::Sensor *power_sensor) { this->power_sensor_ = power_sensor; } + void set_energy_sensor(sensor::Sensor *energy_sensor) { this->energy_sensor_ = energy_sensor; } + void set_frequency_sensor(sensor::Sensor *frequency_sensor) { this->frequency_sensor_ = frequency_sensor; } void set_line_freq(LineFrequency freq) { this->line_freq_ = freq; } void set_address(uint8_t address) { this->address_ = address; } void set_reset(bool reset) { this->reset_ = reset; } @@ -140,7 +140,7 @@ class BL0942 : public PollingComponent, public uart::UARTDevice { uint8_t address_ = 0; bool reset_ = false; LineFrequency line_freq_ = LINE_FREQUENCY_50HZ; - uint32_t rx_start_ = 0; + optional rx_start_{}; uint32_t prev_cf_cnt_ = 0; bool validate_checksum_(DataPacket *data); diff --git a/esphome/components/ble_client/__init__.py b/esphome/components/ble_client/__init__.py index 37db181584..56ac2ea147 100644 --- a/esphome/components/ble_client/__init__.py +++ b/esphome/components/ble_client/__init__.py @@ -172,7 +172,10 @@ BLE_REMOVE_BOND_ACTION_SCHEMA = cv.Schema( @automation.register_action( - "ble_client.disconnect", BLEDisconnectAction, BLE_CONNECT_ACTION_SCHEMA + "ble_client.disconnect", + BLEDisconnectAction, + BLE_CONNECT_ACTION_SCHEMA, + synchronous=False, ) async def ble_disconnect_to_code(config, action_id, template_arg, args): parent = await cg.get_variable(config[CONF_ID]) @@ -180,7 +183,10 @@ async def ble_disconnect_to_code(config, action_id, template_arg, args): @automation.register_action( - "ble_client.connect", BLEConnectAction, BLE_CONNECT_ACTION_SCHEMA + "ble_client.connect", + BLEConnectAction, + BLE_CONNECT_ACTION_SCHEMA, + synchronous=False, ) async def ble_connect_to_code(config, action_id, template_arg, args): parent = await cg.get_variable(config[CONF_ID]) @@ -188,7 +194,10 @@ async def ble_connect_to_code(config, action_id, template_arg, args): @automation.register_action( - "ble_client.ble_write", BLEWriteAction, BLE_WRITE_ACTION_SCHEMA + "ble_client.ble_write", + BLEWriteAction, + BLE_WRITE_ACTION_SCHEMA, + synchronous=False, ) async def ble_write_to_code(config, action_id, template_arg, args): parent = await cg.get_variable(config[CONF_ID]) @@ -247,6 +256,7 @@ async def ble_write_to_code(config, action_id, template_arg, args): "ble_client.numeric_comparison_reply", BLENumericComparisonReplyAction, BLE_NUMERIC_COMPARISON_REPLY_ACTION_SCHEMA, + synchronous=True, ) async def numeric_comparison_reply_to_code(config, action_id, template_arg, args): parent = await cg.get_variable(config[CONF_ID]) @@ -263,7 +273,10 @@ async def numeric_comparison_reply_to_code(config, action_id, template_arg, args @automation.register_action( - "ble_client.passkey_reply", BLEPasskeyReplyAction, BLE_PASSKEY_REPLY_ACTION_SCHEMA + "ble_client.passkey_reply", + BLEPasskeyReplyAction, + BLE_PASSKEY_REPLY_ACTION_SCHEMA, + synchronous=True, ) async def passkey_reply_to_code(config, action_id, template_arg, args): parent = await cg.get_variable(config[CONF_ID]) @@ -283,6 +296,7 @@ async def passkey_reply_to_code(config, action_id, template_arg, args): "ble_client.remove_bond", BLERemoveBondAction, BLE_REMOVE_BOND_ACTION_SCHEMA, + synchronous=True, ) async def remove_bond_to_code(config, action_id, template_arg, args): parent = await cg.get_variable(config[CONF_ID]) diff --git a/esphome/components/ble_client/sensor/ble_rssi_sensor.cpp b/esphome/components/ble_client/sensor/ble_rssi_sensor.cpp index dc032a7a98..715298e592 100644 --- a/esphome/components/ble_client/sensor/ble_rssi_sensor.cpp +++ b/esphome/components/ble_client/sensor/ble_rssi_sensor.cpp @@ -47,6 +47,8 @@ void BLEClientRSSISensor::gap_event_handler(esp_gap_ble_cb_event_t event, esp_bl switch (event) { // server response on RSSI request: case ESP_GAP_BLE_READ_RSSI_COMPLETE_EVT: + if (!this->parent()->check_addr(param->read_rssi_cmpl.remote_addr)) + return; if (param->read_rssi_cmpl.status == ESP_BT_STATUS_SUCCESS) { int8_t rssi = param->read_rssi_cmpl.rssi; ESP_LOGI(TAG, "ESP_GAP_BLE_READ_RSSI_COMPLETE_EVT RSSI: %d", rssi); diff --git a/esphome/components/ble_client/sensor/ble_sensor.cpp b/esphome/components/ble_client/sensor/ble_sensor.cpp index fe5f11bbc2..4bd871dc81 100644 --- a/esphome/components/ble_client/sensor/ble_sensor.cpp +++ b/esphome/components/ble_client/sensor/ble_sensor.cpp @@ -102,6 +102,10 @@ void BLESensor::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t ga break; } case ESP_GATTC_NOTIFY_EVT: { + if (param->notify.value_len == 0) { + ESP_LOGW(TAG, "[%s] ESP_GATTC_NOTIFY_EVT: empty value", this->get_name().c_str()); + break; + } ESP_LOGD(TAG, "[%s] ESP_GATTC_NOTIFY_EVT: handle=0x%x, value=0x%x", this->get_name().c_str(), param->notify.handle, param->notify.value[0]); if (param->notify.handle != this->handle) @@ -131,8 +135,10 @@ float BLESensor::parse_data_(uint8_t *value, uint16_t value_len) { if (this->has_data_to_value_) { std::vector data(value, value + value_len); return this->data_to_value_func_(data); - } else { + } else if (value_len > 0) { return value[0]; + } else { + return NAN; } } diff --git a/esphome/components/ble_client/text_sensor/__init__.py b/esphome/components/ble_client/text_sensor/__init__.py index a6b8956f93..0f53cccdad 100644 --- a/esphome/components/ble_client/text_sensor/__init__.py +++ b/esphome/components/ble_client/text_sensor/__init__.py @@ -88,7 +88,7 @@ async def to_code(config): ) cg.add(var.set_char_uuid128(uuid128)) - if descriptor_uuid := config: + if descriptor_uuid := config.get(CONF_DESCRIPTOR_UUID): if len(descriptor_uuid) == len(esp32_ble_tracker.bt_uuid16_format): cg.add(var.set_descr_uuid16(esp32_ble_tracker.as_hex(descriptor_uuid))) elif len(descriptor_uuid) == len(esp32_ble_tracker.bt_uuid32_format): diff --git a/esphome/components/ble_client/text_sensor/ble_text_sensor.cpp b/esphome/components/ble_client/text_sensor/ble_text_sensor.cpp index cacf1b4835..7eaa6af076 100644 --- a/esphome/components/ble_client/text_sensor/ble_text_sensor.cpp +++ b/esphome/components/ble_client/text_sensor/ble_text_sensor.cpp @@ -104,6 +104,10 @@ void BLETextSensor::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_ case ESP_GATTC_NOTIFY_EVT: { if (param->notify.handle != this->handle) break; + if (param->notify.value_len == 0) { + ESP_LOGW(TAG, "[%s] ESP_GATTC_NOTIFY_EVT: empty value", this->get_name().c_str()); + break; + } ESP_LOGV(TAG, "[%s] ESP_GATTC_NOTIFY_EVT: handle=0x%x, value=0x%x", this->get_name().c_str(), param->notify.handle, param->notify.value[0]); this->publish_state(reinterpret_cast(param->notify.value), param->notify.value_len); diff --git a/esphome/components/ble_nus/__init__.py b/esphome/components/ble_nus/__init__.py index 6581ce1cfa..c0837da402 100644 --- a/esphome/components/ble_nus/__init__.py +++ b/esphome/components/ble_nus/__init__.py @@ -1,29 +1,64 @@ import esphome.codegen as cg from esphome.components.logger import request_log_listener +from esphome.components.uart import ( + UARTComponent, + debug_to_code, + maybe_empty_debug, + uart_ns, +) from esphome.components.zephyr import zephyr_add_prj_conf import esphome.config_validation as cv -from esphome.const import CONF_ID, CONF_LOGS, CONF_TYPE +from esphome.const import ( + CONF_DEBUG, + CONF_ID, + CONF_LOGS, + CONF_RX_BUFFER_SIZE, + CONF_TX_BUFFER_SIZE, + CONF_TYPE, +) +from esphome.types import ConfigType -AUTO_LOAD = ["zephyr_ble_server"] +AUTO_LOAD = ["zephyr_ble_server", "uart"] CODEOWNERS = ["@tomaszduda23"] ble_nus_ns = cg.esphome_ns.namespace("ble_nus") -BLENUS = ble_nus_ns.class_("BLENUS", cg.Component) +BLENUS = ble_nus_ns.class_("BLENUS", cg.Component, UARTComponent) + +CONF_UART = "uart" + + +def validate_rx_buffer(config: ConfigType) -> ConfigType: + config = config.copy() + if config[CONF_TYPE] == CONF_LOGS: + if CONF_RX_BUFFER_SIZE in config: + raise cv.Invalid("logs does not support rx_buffer_size") + elif CONF_RX_BUFFER_SIZE not in config: + config[CONF_RX_BUFFER_SIZE] = 512 + return config + CONFIG_SCHEMA = cv.All( cv.Schema( { cv.GenerateID(): cv.declare_id(BLENUS), cv.Optional(CONF_TYPE, default=CONF_LOGS): cv.one_of( - *[CONF_LOGS], lower=True + *[CONF_LOGS, CONF_UART], lower=True ), + cv.Optional(CONF_TX_BUFFER_SIZE, default=512): cv.All( + cv.validate_bytes, cv.int_range(min=160, max=8192) + ), + cv.Optional(CONF_RX_BUFFER_SIZE): cv.All( + cv.validate_bytes, cv.int_range(min=160, max=8192) + ), + cv.Optional(CONF_DEBUG): maybe_empty_debug, } ).extend(cv.COMPONENT_SCHEMA), cv.only_with_framework("zephyr"), + validate_rx_buffer, ) -async def to_code(config): +async def to_code(config: ConfigType) -> None: var = cg.new_Pvariable(config[CONF_ID]) zephyr_add_prj_conf("BT_NUS", True) expose_log = config[CONF_TYPE] == CONF_LOGS @@ -31,3 +66,11 @@ async def to_code(config): if expose_log: request_log_listener() # Request a log listener slot for BLE NUS log streaming await cg.register_component(var, config) + cg.add_define("ESPHOME_BLE_NUS_TX_RING_BUFFER_SIZE", config[CONF_TX_BUFFER_SIZE]) + if CONF_RX_BUFFER_SIZE in config: + cg.add_define( + "ESPHOME_BLE_NUS_RX_RING_BUFFER_SIZE", config[CONF_RX_BUFFER_SIZE] + ) + if CONF_DEBUG in config: + cg.add_global(uart_ns.using) + await debug_to_code(config[CONF_DEBUG], var) diff --git a/esphome/components/ble_nus/ble_nus.cpp b/esphome/components/ble_nus/ble_nus.cpp index 0de65b623f..71d98332e0 100644 --- a/esphome/components/ble_nus/ble_nus.cpp +++ b/esphome/components/ble_nus/ble_nus.cpp @@ -11,25 +11,115 @@ namespace esphome::ble_nus { -constexpr size_t BLE_TX_BUF_SIZE = 2048; - // NOLINTBEGIN(cppcoreguidelines-avoid-non-const-global-variables) BLENUS *global_ble_nus; -RING_BUF_DECLARE(global_ble_tx_ring_buf, BLE_TX_BUF_SIZE); +RING_BUF_DECLARE(global_ble_tx_ring_buf, ESPHOME_BLE_NUS_TX_RING_BUFFER_SIZE); +#ifdef ESPHOME_BLE_NUS_RX_RING_BUFFER_SIZE +RING_BUF_DECLARE(global_ble_rx_ring_buf, ESPHOME_BLE_NUS_RX_RING_BUFFER_SIZE); +#endif // NOLINTEND(cppcoreguidelines-avoid-non-const-global-variables) static const char *const TAG = "ble_nus"; -size_t BLENUS::write_array(const uint8_t *data, size_t len) { +void BLENUS::write_array(const uint8_t *data, size_t len) { if (atomic_get(&this->tx_status_) == TX_DISABLED) { - return 0; + return; } - return ring_buf_put(&global_ble_tx_ring_buf, data, len); + auto sent = ring_buf_put(&global_ble_tx_ring_buf, data, len); + if (sent < len) { + ESP_LOGE(TAG, "TX dropping %u bytes", len - sent); + return; + } +#ifdef USE_UART_DEBUGGER + for (size_t i = 0; i < len; i++) { + this->debug_callback_.call(uart::UART_DIRECTION_TX, data[i]); + } +#endif +} + +bool BLENUS::peek_byte(uint8_t *data) { +#ifdef ESPHOME_BLE_NUS_RX_RING_BUFFER_SIZE + if (this->has_peek_) { + *data = this->peek_buffer_; + return true; + } + + if (this->read_byte(&this->peek_buffer_)) { + *data = this->peek_buffer_; + this->has_peek_ = true; + return true; + } + + return false; +#else + return false; +#endif +} + +bool BLENUS::read_array(uint8_t *data, size_t len) { +#ifdef ESPHOME_BLE_NUS_RX_RING_BUFFER_SIZE + if (len == 0) { + return true; + } + if (this->available() < len) { + return false; + } + + // First, use the peek buffer if available + if (this->has_peek_) { +#ifdef USE_UART_DEBUGGER + this->debug_callback_.call(uart::UART_DIRECTION_RX, this->peek_buffer_); +#endif + data[0] = this->peek_buffer_; + this->has_peek_ = false; + data++; + if (--len == 0) { // Decrement len first, then check it... + return true; // No more to read + } + } + + if (ring_buf_get(&global_ble_rx_ring_buf, data, len) != len) { + ESP_LOGE(TAG, "UART BLE unexpected size"); + return false; + } +#ifdef USE_UART_DEBUGGER + for (size_t i = 0; i < len; i++) { + this->debug_callback_.call(uart::UART_DIRECTION_RX, data[i]); + } +#endif + return true; +#else + return false; +#endif +} + +size_t BLENUS::available() { +#ifdef ESPHOME_BLE_NUS_RX_RING_BUFFER_SIZE + uint32_t size = ring_buf_size_get(&global_ble_rx_ring_buf); + ESP_LOGVV(TAG, "UART BLE available %u", size); + return size + (this->has_peek_ ? 1 : 0); +#else + return 0; +#endif +} + +uart::UARTFlushResult BLENUS::flush() { + constexpr uint32_t timeout_500ms = 500; + uint32_t start = millis(); + while (atomic_get(&this->tx_status_) != TX_DISABLED && !ring_buf_is_empty(&global_ble_tx_ring_buf)) { + if (millis() - start > timeout_500ms) { + ESP_LOGW(TAG, "Flush timeout"); + return uart::UARTFlushResult::UART_FLUSH_RESULT_TIMEOUT; + } + delay(1); + } + return uart::UARTFlushResult::UART_FLUSH_RESULT_SUCCESS; } void BLENUS::connected(bt_conn *conn, uint8_t err) { if (err == 0) { global_ble_nus->conn_.store(bt_conn_ref(conn)); + global_ble_nus->connected_ = true; } } @@ -38,6 +128,7 @@ void BLENUS::disconnected(bt_conn *conn, uint8_t reason) { bt_conn_unref(global_ble_nus->conn_.load()); // Connection array is global static. // Reference can be kept even if disconnected. + global_ble_nus->connected_ = false; } } @@ -63,12 +154,19 @@ void BLENUS::send_enabled_callback(bt_nus_send_status status) { break; } } - void BLENUS::rx_callback(bt_conn *conn, const uint8_t *const data, uint16_t len) { - ESP_LOGD(TAG, "Received %d bytes.", len); + ESP_LOGV(TAG, "Received %d bytes.", len); +#ifdef ESPHOME_BLE_NUS_RX_RING_BUFFER_SIZE + auto recv_len = ring_buf_put(&global_ble_rx_ring_buf, data, len); + if (recv_len < len) { + ESP_LOGE(TAG, "RX dropping %u bytes", len - recv_len); + } +#endif } - void BLENUS::setup() { +#ifdef ESPHOME_BLE_NUS_RX_RING_BUFFER_SIZE + this->rx_buffer_size_ = ESPHOME_BLE_NUS_RX_RING_BUFFER_SIZE; +#endif bt_nus_cb callbacks = { .received = rx_callback, .sent = tx_callback, @@ -87,7 +185,10 @@ void BLENUS::setup() { global_ble_nus = this; #ifdef USE_LOGGER if (logger::global_logger != nullptr && this->expose_log_) { - logger::global_logger->add_log_listener(this); + logger::global_logger->add_log_callback( + this, [](void *self, uint8_t level, const char *tag, const char *message, size_t message_len) { + static_cast(self)->on_log(level, tag, message, message_len); + }); } #endif } @@ -103,16 +204,17 @@ void BLENUS::on_log(uint8_t level, const char *tag, const char *message, size_t #endif void BLENUS::dump_config() { - ESP_LOGCONFIG(TAG, - "ble nus:\n" - " log: %s", - YESNO(this->expose_log_)); uint32_t mtu = 0; bt_conn *conn = this->conn_.load(); - if (conn) { + if (conn && this->connected_) { mtu = bt_nus_get_mtu(conn); } - ESP_LOGCONFIG(TAG, " MTU: %u", mtu); + ESP_LOGCONFIG(TAG, + "ble nus:\n" + " log: %s\n" + " connected: %s\n" + " MTU: %u", + YESNO(this->expose_log_), YESNO(this->connected_.load()), mtu); } void BLENUS::loop() { diff --git a/esphome/components/ble_nus/ble_nus.h b/esphome/components/ble_nus/ble_nus.h index ef20fc5e5b..f1afd54af9 100644 --- a/esphome/components/ble_nus/ble_nus.h +++ b/esphome/components/ble_nus/ble_nus.h @@ -2,6 +2,7 @@ #ifdef USE_ZEPHYR #include "esphome/core/defines.h" #include "esphome/core/component.h" +#include "esphome/components/uart/uart_component.h" #ifdef USE_LOGGER #include "esphome/components/logger/logger.h" #endif @@ -10,12 +11,7 @@ namespace esphome::ble_nus { -class BLENUS : public Component -#ifdef USE_LOGGER - , - public logger::LogListener -#endif -{ +class BLENUS : public uart::UARTComponent, public Component { enum TxStatus { TX_DISABLED, TX_ENABLED, @@ -26,10 +22,15 @@ class BLENUS : public Component void setup() override; void dump_config() override; void loop() override; - size_t write_array(const uint8_t *data, size_t len); + void write_array(const uint8_t *data, size_t len) override; + bool peek_byte(uint8_t *data) override; + bool read_array(uint8_t *data, size_t len) override; + size_t available() override; + uart::UARTFlushResult flush() override; + void check_logger_conflict() override {} void set_expose_log(bool expose_log) { this->expose_log_ = expose_log; } #ifdef USE_LOGGER - void on_log(uint8_t level, const char *tag, const char *message, size_t message_len) override; + void on_log(uint8_t level, const char *tag, const char *message, size_t message_len); #endif protected: @@ -42,6 +43,12 @@ class BLENUS : public Component std::atomic conn_ = nullptr; bool expose_log_ = false; atomic_t tx_status_ = ATOMIC_INIT(TX_DISABLED); + std::atomic connected_{}; +#ifdef ESPHOME_BLE_NUS_RX_RING_BUFFER_SIZE + // RX buffer for peek functionality + uint8_t peek_buffer_{0}; + bool has_peek_{false}; +#endif }; } // namespace esphome::ble_nus diff --git a/esphome/components/ble_presence/ble_presence_device.h b/esphome/components/ble_presence/ble_presence_device.h index 70ecc67c32..8ae5edab3a 100644 --- a/esphome/components/ble_presence/ble_presence_device.h +++ b/esphome/components/ble_presence/ble_presence_device.h @@ -76,11 +76,12 @@ class BLEPresenceDevice : public binary_sensor::BinarySensorInitiallyOff, } break; case MATCH_BY_IBEACON_UUID: - if (!device.get_ibeacon().has_value()) { + auto maybe_ibeacon = device.get_ibeacon(); + if (!maybe_ibeacon.has_value()) { return false; } - auto ibeacon = device.get_ibeacon().value(); + auto ibeacon = *maybe_ibeacon; if (this->ibeacon_uuid_ != ibeacon.get_uuid()) { return false; @@ -101,7 +102,7 @@ class BLEPresenceDevice : public binary_sensor::BinarySensorInitiallyOff, } void loop() override { - if (this->found_ && this->last_seen_ + this->timeout_ < millis()) + if (this->found_ && millis() - this->last_seen_ > this->timeout_) this->set_found_(false); } void dump_config() override; diff --git a/esphome/components/ble_rssi/ble_rssi_sensor.h b/esphome/components/ble_rssi/ble_rssi_sensor.h index 80245a1fe1..81f21c94dd 100644 --- a/esphome/components/ble_rssi/ble_rssi_sensor.h +++ b/esphome/components/ble_rssi/ble_rssi_sensor.h @@ -74,11 +74,12 @@ class BLERSSISensor : public sensor::Sensor, public esp32_ble_tracker::ESPBTDevi } break; case MATCH_BY_IBEACON_UUID: - if (!device.get_ibeacon().has_value()) { + auto maybe_ibeacon = device.get_ibeacon(); + if (!maybe_ibeacon.has_value()) { return false; } - auto ibeacon = device.get_ibeacon().value(); + auto ibeacon = *maybe_ibeacon; if (this->ibeacon_uuid_ != ibeacon.get_uuid()) { return false; diff --git a/esphome/components/ble_scanner/ble_scanner.h b/esphome/components/ble_scanner/ble_scanner.h index 7061b6d336..c6d7f24cce 100644 --- a/esphome/components/ble_scanner/ble_scanner.h +++ b/esphome/components/ble_scanner/ble_scanner.h @@ -16,12 +16,27 @@ namespace ble_scanner { class BLEScanner : public text_sensor::TextSensor, public esp32_ble_tracker::ESPBTDeviceListener, public Component { public: bool parse_device(const esp32_ble_tracker::ESPBTDevice &device) override { - // Format JSON using stack buffer to avoid heap allocations from string concatenation - char buf[128]; char addr_buf[MAC_ADDRESS_PRETTY_BUFFER_SIZE]; + // Escape special characters in the device name for valid JSON + const char *name = device.get_name().c_str(); + char escaped_name[128]; + size_t pos = 0; + for (; *name != '\0' && pos < sizeof(escaped_name) - 7; name++) { + uint8_t c = static_cast(*name); + if (c == '"' || c == '\\') { + escaped_name[pos++] = '\\'; + escaped_name[pos++] = c; + } else if (c < 0x20) { + pos += snprintf(escaped_name + pos, sizeof(escaped_name) - pos, "\\u%04x", c); + } else { + escaped_name[pos++] = c; + } + } + escaped_name[pos] = '\0'; + + char buf[256]; snprintf(buf, sizeof(buf), "{\"timestamp\":%" PRId64 ",\"address\":\"%s\",\"rssi\":%d,\"name\":\"%s\"}", - static_cast(::time(nullptr)), device.address_str_to(addr_buf), device.get_rssi(), - device.get_name().c_str()); + static_cast(::time(nullptr)), device.address_str_to(addr_buf), device.get_rssi(), escaped_name); this->publish_state(buf); return true; } diff --git a/esphome/components/bluetooth_proxy/bluetooth_connection.cpp b/esphome/components/bluetooth_proxy/bluetooth_connection.cpp index 60f56fda54..21573f0184 100644 --- a/esphome/components/bluetooth_proxy/bluetooth_connection.cpp +++ b/esphome/components/bluetooth_proxy/bluetooth_connection.cpp @@ -183,10 +183,7 @@ void BluetoothConnection::send_service_for_discovery_() { static constexpr size_t MAX_PACKET_SIZE = 1360; // Keep running total of actual message size - size_t current_size = 0; - api::ProtoSize size; - resp.calculate_size(size); - current_size = size.get_size(); + size_t current_size = resp.calculate_size(); while (this->send_service_ < this->service_count_) { esp_gattc_service_elem_t service_result; @@ -302,9 +299,7 @@ void BluetoothConnection::send_service_for_discovery_() { } // end if (total_char_count > 0) // Calculate the actual size of just this service - api::ProtoSize service_sizer; - service_resp.calculate_size(service_sizer); - size_t service_size = service_sizer.get_size() + 1; // +1 for field tag + size_t service_size = service_resp.calculate_size() + 1; // +1 for field tag // Check if adding this service would exceed the limit if (current_size + service_size > MAX_PACKET_SIZE) { @@ -333,7 +328,7 @@ void BluetoothConnection::send_service_for_discovery_() { } // Send the message with dynamically batched services - api_conn->send_message(resp, api::BluetoothGATTGetServicesResponse::MESSAGE_TYPE); + api_conn->send_message(resp); } void BluetoothConnection::log_connection_error_(const char *operation, esp_gatt_status_t status) { @@ -415,11 +410,14 @@ bool BluetoothConnection::gattc_event_handler(esp_gattc_cb_event_t event, esp_ga this->proxy_->send_gatt_error(this->address_, param->read.handle, param->read.status); break; } + auto *api_connection = this->proxy_->get_api_connection(); + if (api_connection == nullptr) + break; api::BluetoothGATTReadResponse resp; resp.address = this->address_; resp.handle = param->read.handle; resp.set_data(param->read.value, param->read.value_len); - this->proxy_->get_api_connection()->send_message(resp, api::BluetoothGATTReadResponse::MESSAGE_TYPE); + api_connection->send_message(resp); break; } case ESP_GATTC_WRITE_CHAR_EVT: @@ -429,10 +427,13 @@ bool BluetoothConnection::gattc_event_handler(esp_gattc_cb_event_t event, esp_ga this->proxy_->send_gatt_error(this->address_, param->write.handle, param->write.status); break; } + auto *api_connection = this->proxy_->get_api_connection(); + if (api_connection == nullptr) + break; api::BluetoothGATTWriteResponse resp; resp.address = this->address_; resp.handle = param->write.handle; - this->proxy_->get_api_connection()->send_message(resp, api::BluetoothGATTWriteResponse::MESSAGE_TYPE); + api_connection->send_message(resp); break; } case ESP_GATTC_UNREG_FOR_NOTIFY_EVT: { @@ -442,10 +443,13 @@ bool BluetoothConnection::gattc_event_handler(esp_gattc_cb_event_t event, esp_ga this->proxy_->send_gatt_error(this->address_, param->unreg_for_notify.handle, param->unreg_for_notify.status); break; } + auto *api_connection = this->proxy_->get_api_connection(); + if (api_connection == nullptr) + break; api::BluetoothGATTNotifyResponse resp; resp.address = this->address_; resp.handle = param->unreg_for_notify.handle; - this->proxy_->get_api_connection()->send_message(resp, api::BluetoothGATTNotifyResponse::MESSAGE_TYPE); + api_connection->send_message(resp); break; } case ESP_GATTC_REG_FOR_NOTIFY_EVT: { @@ -455,20 +459,26 @@ bool BluetoothConnection::gattc_event_handler(esp_gattc_cb_event_t event, esp_ga this->proxy_->send_gatt_error(this->address_, param->reg_for_notify.handle, param->reg_for_notify.status); break; } + auto *api_connection = this->proxy_->get_api_connection(); + if (api_connection == nullptr) + break; api::BluetoothGATTNotifyResponse resp; resp.address = this->address_; resp.handle = param->reg_for_notify.handle; - this->proxy_->get_api_connection()->send_message(resp, api::BluetoothGATTNotifyResponse::MESSAGE_TYPE); + api_connection->send_message(resp); break; } case ESP_GATTC_NOTIFY_EVT: { ESP_LOGV(TAG, "[%d] [%s] ESP_GATTC_NOTIFY_EVT: handle=0x%2X", this->connection_index_, this->address_str_, param->notify.handle); + auto *api_connection = this->proxy_->get_api_connection(); + if (api_connection == nullptr) + break; api::BluetoothGATTNotifyDataResponse resp; resp.address = this->address_; resp.handle = param->notify.handle; resp.set_data(param->notify.value, param->notify.value_len); - this->proxy_->get_api_connection()->send_message(resp, api::BluetoothGATTNotifyDataResponse::MESSAGE_TYPE); + api_connection->send_message(resp); break; } default: diff --git a/esphome/components/bluetooth_proxy/bluetooth_connection.h b/esphome/components/bluetooth_proxy/bluetooth_connection.h index 60bbc93e8b..b50ea2d6a2 100644 --- a/esphome/components/bluetooth_proxy/bluetooth_connection.h +++ b/esphome/components/bluetooth_proxy/bluetooth_connection.h @@ -24,6 +24,10 @@ class BluetoothConnection final : public esp32_ble_client::BLEClientBase { esp_err_t notify_characteristic(uint16_t handle, bool enable); + esp_err_t update_connection_params(uint16_t min_interval, uint16_t max_interval, uint16_t latency, uint16_t timeout) { + return this->update_conn_params_(min_interval, max_interval, latency, timeout, "custom"); + } + void set_address(uint64_t address) override; protected: diff --git a/esphome/components/bluetooth_proxy/bluetooth_proxy.cpp b/esphome/components/bluetooth_proxy/bluetooth_proxy.cpp index d45377b3f6..c69163b1f7 100644 --- a/esphome/components/bluetooth_proxy/bluetooth_proxy.cpp +++ b/esphome/components/bluetooth_proxy/bluetooth_proxy.cpp @@ -3,7 +3,9 @@ #include "esphome/core/log.h" #include "esphome/core/macros.h" #include "esphome/core/application.h" +#include #include +#include #ifdef USE_ESP32 @@ -28,6 +30,19 @@ void BluetoothProxy::setup() { this->configured_scan_active_ = this->parent_->get_scan_active(); this->parent_->add_scanner_state_listener(this); + + this->set_interval(100, [this]() { + if (api::global_api_server->is_connected() && this->api_connection_ != nullptr) { + this->flush_pending_advertisements_(); + return; + } + for (uint8_t i = 0; i < this->connection_count_; i++) { + auto *connection = this->connections_[i]; + if (connection->get_address() != 0 && !connection->disconnect_pending()) { + connection->disconnect(); + } + } + }); } void BluetoothProxy::on_scanner_state(esp32_ble_tracker::ScannerState state) { @@ -44,7 +59,7 @@ void BluetoothProxy::send_bluetooth_scanner_state_(esp32_ble_tracker::ScannerSta resp.configured_mode = this->configured_scan_active_ ? api::enums::BluetoothScannerMode::BLUETOOTH_SCANNER_MODE_ACTIVE : api::enums::BluetoothScannerMode::BLUETOOTH_SCANNER_MODE_PASSIVE; - this->api_connection_->send_message(resp, api::BluetoothScannerStateResponse::MESSAGE_TYPE); + this->api_connection_->send_message(resp); } void BluetoothProxy::log_connection_request_ignored_(BluetoothConnection *connection, espbt::ClientState state) { @@ -99,25 +114,15 @@ bool BluetoothProxy::parse_devices(const esp32_ble::BLEScanResult *scan_results, // Flush if we have reached BLUETOOTH_PROXY_ADVERTISEMENT_BATCH_SIZE if (this->response_.advertisements_len >= BLUETOOTH_PROXY_ADVERTISEMENT_BATCH_SIZE) { - this->flush_pending_advertisements(); + this->flush_pending_advertisements_(); } } return true; } -void BluetoothProxy::flush_pending_advertisements() { - if (this->response_.advertisements_len == 0 || !api::global_api_server->is_connected() || - this->api_connection_ == nullptr) - return; - - // Send the message - this->api_connection_->send_message(this->response_, api::BluetoothLERawAdvertisementsResponse::MESSAGE_TYPE); - +void BluetoothProxy::log_advertisement_flush_() { ESP_LOGV(TAG, "Sent batch of %u BLE advertisements", this->response_.advertisements_len); - - // Reset the length for the next batch - this->response_.advertisements_len = 0; } void BluetoothProxy::dump_config() { @@ -128,27 +133,6 @@ void BluetoothProxy::dump_config() { YESNO(this->active_), this->connection_count_); } -void BluetoothProxy::loop() { - if (!api::global_api_server->is_connected() || this->api_connection_ == nullptr) { - for (uint8_t i = 0; i < this->connection_count_; i++) { - auto *connection = this->connections_[i]; - if (connection->get_address() != 0 && !connection->disconnect_pending()) { - connection->disconnect(); - } - } - return; - } - - // Flush any pending BLE advertisements that have been accumulated but not yet sent - uint32_t now = App.get_loop_component_start_time(); - - // Flush accumulated advertisements every 100ms - if (now - this->last_advertisement_flush_time_ >= 100) { - this->flush_pending_advertisements(); - this->last_advertisement_flush_time_ = now; - } -} - esp32_ble_tracker::AdvertisementParserType BluetoothProxy::get_advertisement_parser_type() { return esp32_ble_tracker::AdvertisementParserType::RAW_ADVERTISEMENTS; } @@ -269,7 +253,7 @@ void BluetoothProxy::bluetooth_device_request(const api::BluetoothDeviceRequest call.success = ret == ESP_OK; call.error = ret; - this->api_connection_->send_message(call, api::BluetoothDeviceClearCacheResponse::MESSAGE_TYPE); + this->api_connection_->send_message(call); break; } @@ -361,6 +345,33 @@ void BluetoothProxy::bluetooth_gatt_notify(const api::BluetoothGATTNotifyRequest } } +void BluetoothProxy::bluetooth_set_connection_params(const api::BluetoothSetConnectionParamsRequest &msg) { + if (this->api_connection_ == nullptr) + return; + + auto *connection = this->get_connection_(msg.address, false); + api::BluetoothSetConnectionParamsResponse resp; + resp.address = msg.address; + + if (connection == nullptr || !connection->connected()) { + ESP_LOGW(TAG, "[%d] [%s] Cannot set connection params, not connected", + connection ? static_cast(connection->connection_index_) : -1, + connection ? connection->address_str() : "unknown"); + resp.error = ESP_GATT_NOT_CONNECTED; + this->api_connection_->send_message(resp); + return; + } + + // Protobuf fields are uint32_t to future-proof the API if BLE ever supports wider values; + // clamp to uint16_t since the current BLE spec defines these as 16-bit. + constexpr uint32_t max_val = std::numeric_limits::max(); + resp.error = connection->update_connection_params(static_cast(std::min(msg.min_interval, max_val)), + static_cast(std::min(msg.max_interval, max_val)), + static_cast(std::min(msg.latency, max_val)), + static_cast(std::min(msg.timeout, max_val))); + this->api_connection_->send_message(resp); +} + void BluetoothProxy::subscribe_api_connection(api::APIConnection *api_connection, uint32_t flags) { if (this->api_connection_ != nullptr) { ESP_LOGE(TAG, "Only one API subscription is allowed at a time"); @@ -389,7 +400,7 @@ void BluetoothProxy::send_device_connection(uint64_t address, bool connected, ui call.connected = connected; call.mtu = mtu; call.error = error; - this->api_connection_->send_message(call, api::BluetoothDeviceConnectionResponse::MESSAGE_TYPE); + this->api_connection_->send_message(call); } void BluetoothProxy::send_connections_free() { if (this->api_connection_ != nullptr) { @@ -398,7 +409,7 @@ void BluetoothProxy::send_connections_free() { } void BluetoothProxy::send_connections_free(api::APIConnection *api_connection) { - api_connection->send_message(this->connections_free_response_, api::BluetoothConnectionsFreeResponse::MESSAGE_TYPE); + api_connection->send_message(this->connections_free_response_); } void BluetoothProxy::send_gatt_services_done(uint64_t address) { @@ -406,7 +417,7 @@ void BluetoothProxy::send_gatt_services_done(uint64_t address) { return; api::BluetoothGATTGetServicesDoneResponse call; call.address = address; - this->api_connection_->send_message(call, api::BluetoothGATTGetServicesDoneResponse::MESSAGE_TYPE); + this->api_connection_->send_message(call); } void BluetoothProxy::send_gatt_error(uint64_t address, uint16_t handle, esp_err_t error) { @@ -416,25 +427,29 @@ void BluetoothProxy::send_gatt_error(uint64_t address, uint16_t handle, esp_err_ call.address = address; call.handle = handle; call.error = error; - this->api_connection_->send_message(call, api::BluetoothGATTWriteResponse::MESSAGE_TYPE); + this->api_connection_->send_message(call); } void BluetoothProxy::send_device_pairing(uint64_t address, bool paired, esp_err_t error) { + if (this->api_connection_ == nullptr) + return; api::BluetoothDevicePairingResponse call; call.address = address; call.paired = paired; call.error = error; - this->api_connection_->send_message(call, api::BluetoothDevicePairingResponse::MESSAGE_TYPE); + this->api_connection_->send_message(call); } void BluetoothProxy::send_device_unpairing(uint64_t address, bool success, esp_err_t error) { + if (this->api_connection_ == nullptr) + return; api::BluetoothDeviceUnpairingResponse call; call.address = address; call.success = success; call.error = error; - this->api_connection_->send_message(call, api::BluetoothDeviceUnpairingResponse::MESSAGE_TYPE); + this->api_connection_->send_message(call); } void BluetoothProxy::bluetooth_scanner_set_mode(bool active) { diff --git a/esphome/components/bluetooth_proxy/bluetooth_proxy.h b/esphome/components/bluetooth_proxy/bluetooth_proxy.h index ab9aee2d81..6680ab0e84 100644 --- a/esphome/components/bluetooth_proxy/bluetooth_proxy.h +++ b/esphome/components/bluetooth_proxy/bluetooth_proxy.h @@ -23,9 +23,9 @@ namespace esphome::bluetooth_proxy { -static const esp_err_t ESP_GATT_NOT_CONNECTED = -1; -static const int DONE_SENDING_SERVICES = -2; -static const int INIT_SENDING_SERVICES = -3; +static constexpr esp_err_t ESP_GATT_NOT_CONNECTED = -1; +static constexpr int DONE_SENDING_SERVICES = -2; +static constexpr int INIT_SENDING_SERVICES = -3; using namespace esp32_ble_client; @@ -35,8 +35,8 @@ using namespace esp32_ble_client; // Version 3: New connection API // Version 4: Pairing support // Version 5: Cache clear support -static const uint32_t LEGACY_ACTIVE_CONNECTIONS_VERSION = 5; -static const uint32_t LEGACY_PASSIVE_ONLY_VERSION = 1; +static constexpr uint32_t LEGACY_ACTIVE_CONNECTIONS_VERSION = 5; +static constexpr uint32_t LEGACY_PASSIVE_ONLY_VERSION = 1; enum BluetoothProxyFeature : uint32_t { FEATURE_PASSIVE_SCAN = 1 << 0, @@ -46,6 +46,7 @@ enum BluetoothProxyFeature : uint32_t { FEATURE_CACHE_CLEARING = 1 << 4, FEATURE_RAW_ADVERTISEMENTS = 1 << 5, FEATURE_STATE_AND_MODE = 1 << 6, + FEATURE_CONNECTION_PARAMS_SETTING = 1 << 7, }; enum BluetoothProxySubscriptionFlag : uint32_t { @@ -64,8 +65,6 @@ class BluetoothProxy final : public esp32_ble_tracker::ESPBTDeviceListener, bool parse_devices(const esp32_ble::BLEScanResult *scan_results, size_t count) override; void dump_config() override; void setup() override; - void loop() override; - void flush_pending_advertisements(); esp32_ble_tracker::AdvertisementParserType get_advertisement_parser_type() override; void register_connection(BluetoothConnection *connection) { @@ -82,6 +81,7 @@ class BluetoothProxy final : public esp32_ble_tracker::ESPBTDeviceListener, void bluetooth_gatt_write_descriptor(const api::BluetoothGATTWriteDescriptorRequest &msg); void bluetooth_gatt_send_services(const api::BluetoothGATTGetServicesRequest &msg); void bluetooth_gatt_notify(const api::BluetoothGATTNotifyRequest &msg); + void bluetooth_set_connection_params(const api::BluetoothSetConnectionParamsRequest &msg); void subscribe_api_connection(api::APIConnection *api_connection, uint32_t flags); void unsubscribe_api_connection(api::APIConnection *api_connection); @@ -130,6 +130,7 @@ class BluetoothProxy final : public esp32_ble_tracker::ESPBTDeviceListener, flags |= BluetoothProxyFeature::FEATURE_REMOTE_CACHING; flags |= BluetoothProxyFeature::FEATURE_PAIRING; flags |= BluetoothProxyFeature::FEATURE_CACHE_CLEARING; + flags |= BluetoothProxyFeature::FEATURE_CONNECTION_PARAMS_SETTING; } return flags; @@ -147,6 +148,18 @@ class BluetoothProxy final : public esp32_ble_tracker::ESPBTDeviceListener, protected: void send_bluetooth_scanner_state_(esp32_ble_tracker::ScannerState state); + /// Caller must ensure api_connection_ is non-null and API server is connected. + void flush_pending_advertisements_() { + if (this->response_.advertisements_len == 0) + return; + this->api_connection_->send_message(this->response_); +#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE + this->log_advertisement_flush_(); +#endif + this->response_.advertisements_len = 0; + } + void log_advertisement_flush_(); + BluetoothConnection *get_connection_(uint64_t address, bool reserve); void log_connection_request_ignored_(BluetoothConnection *connection, espbt::ClientState state); void log_connection_info_(BluetoothConnection *connection, const char *message); @@ -163,9 +176,6 @@ class BluetoothProxy final : public esp32_ble_tracker::ESPBTDeviceListener, // BLE advertisement batching api::BluetoothLERawAdvertisementsResponse response_; - // Group 3: 4-byte types - uint32_t last_advertisement_flush_time_{0}; - // Pre-allocated response message - always ready to send api::BluetoothConnectionsFreeResponse connections_free_response_; diff --git a/esphome/components/bm8563/bm8563.cpp b/esphome/components/bm8563/bm8563.cpp index 07831485c1..062094c036 100644 --- a/esphome/components/bm8563/bm8563.cpp +++ b/esphome/components/bm8563/bm8563.cpp @@ -1,4 +1,7 @@ #include "bm8563.h" + +#include + #include "esphome/core/log.h" namespace esphome::bm8563 { @@ -56,7 +59,6 @@ void BM8563::read_time() { ESPTime rtc_time; this->get_time_(rtc_time); this->get_date_(rtc_time); - rtc_time.day_of_year = 1; // unused by recalc_timestamp_utc, but needs to be valid ESP_LOGD(TAG, "Read time: %i-%i-%i %i, %i:%i:%i", rtc_time.year, rtc_time.month, rtc_time.day_of_month, rtc_time.day_of_week, rtc_time.hour, rtc_time.minute, rtc_time.second); @@ -147,10 +149,10 @@ optional BM8563::read_register_(uint8_t reg) { } void BM8563::set_timer_irq_(uint32_t duration_s) { - ESP_LOGI(TAG, "Timer Duration: %u s", duration_s); + ESP_LOGI(TAG, "Timer Duration: %" PRIu32 " s", duration_s); if (duration_s > MAX_TIMER_DURATION_S) { - ESP_LOGW(TAG, "Timer duration %u s exceeds maximum %u s", duration_s, MAX_TIMER_DURATION_S); + ESP_LOGW(TAG, "Timer duration %" PRIu32 " s exceeds maximum %" PRIu32 " s", duration_s, MAX_TIMER_DURATION_S); return; } diff --git a/esphome/components/bm8563/time.py b/esphome/components/bm8563/time.py index 2785315af2..ba264f00bf 100644 --- a/esphome/components/bm8563/time.py +++ b/esphome/components/bm8563/time.py @@ -33,6 +33,7 @@ CONFIG_SCHEMA = ( cv.GenerateID(): cv.use_id(BM8563), } ), + synchronous=True, ) async def bm8563_write_time_to_code(config, action_id, template_arg, args): var = cg.new_Pvariable(action_id, template_arg) @@ -49,6 +50,7 @@ async def bm8563_write_time_to_code(config, action_id, template_arg, args): cv.Required(CONF_DURATION): cv.templatable(cv.positive_time_period_seconds), } ), + synchronous=True, ) async def bm8563_start_timer_to_code(config, action_id, template_arg, args): var = cg.new_Pvariable(action_id, template_arg) @@ -66,6 +68,7 @@ async def bm8563_start_timer_to_code(config, action_id, template_arg, args): cv.GenerateID(): cv.use_id(BM8563), } ), + synchronous=True, ) async def bm8563_read_time_to_code(config, action_id, template_arg, args): var = cg.new_Pvariable(action_id, template_arg) diff --git a/esphome/components/bme280_base/bme280_base.cpp b/esphome/components/bme280_base/bme280_base.cpp index c5d4c9c0a5..f31940df10 100644 --- a/esphome/components/bme280_base/bme280_base.cpp +++ b/esphome/components/bme280_base/bme280_base.cpp @@ -7,7 +7,7 @@ #include #include -#define BME280_ERROR_WRONG_CHIP_ID "Wrong chip ID" +#define BME280_ERROR_WRONG_CHIP_ID "Wrong chip ID or no response" namespace esphome { namespace bme280_base { @@ -147,8 +147,11 @@ void BME280Component::setup() { this->calibration_.h1 = read_u8_(BME280_REGISTER_DIG_H1); this->calibration_.h2 = read_s16_le_(BME280_REGISTER_DIG_H2); this->calibration_.h3 = read_u8_(BME280_REGISTER_DIG_H3); - this->calibration_.h4 = read_u8_(BME280_REGISTER_DIG_H4) << 4 | (read_u8_(BME280_REGISTER_DIG_H4 + 1) & 0x0F); - this->calibration_.h5 = read_u8_(BME280_REGISTER_DIG_H5 + 1) << 4 | (read_u8_(BME280_REGISTER_DIG_H5) >> 4); + // h4 and h5 are signed 12-bit values; shift left then arithmetic right shift to sign-extend + int16_t h4_raw = read_u8_(BME280_REGISTER_DIG_H4) << 4 | (read_u8_(BME280_REGISTER_DIG_H4 + 1) & 0x0F); + this->calibration_.h4 = static_cast(h4_raw << 4) >> 4; + int16_t h5_raw = read_u8_(BME280_REGISTER_DIG_H5 + 1) << 4 | (read_u8_(BME280_REGISTER_DIG_H5) >> 4); + this->calibration_.h5 = static_cast(h5_raw << 4) >> 4; this->calibration_.h6 = read_u8_(BME280_REGISTER_DIG_H6); uint8_t humid_control_val = 0; @@ -199,7 +202,6 @@ void BME280Component::dump_config() { LOG_SENSOR(" ", "Humidity", this->humidity_sensor_); ESP_LOGCONFIG(TAG, " Oversampling: %s", oversampling_to_str(this->humidity_oversampling_)); } -float BME280Component::get_setup_priority() const { return setup_priority::DATA; } inline uint8_t oversampling_to_time(BME280Oversampling over_sampling) { return (1 << uint8_t(over_sampling)) >> 1; } diff --git a/esphome/components/bme280_base/bme280_base.h b/esphome/components/bme280_base/bme280_base.h index 0f55ad0101..00781d05b2 100644 --- a/esphome/components/bme280_base/bme280_base.h +++ b/esphome/components/bme280_base/bme280_base.h @@ -76,7 +76,6 @@ class BME280Component : public PollingComponent { // (In most use cases you won't need these) void setup() override; void dump_config() override; - float get_setup_priority() const override; void update() override; protected: diff --git a/esphome/components/bme680/bme680.cpp b/esphome/components/bme680/bme680.cpp index 16435ccfee..e3cd80de00 100644 --- a/esphome/components/bme680/bme680.cpp +++ b/esphome/components/bme680/bme680.cpp @@ -22,11 +22,11 @@ static const uint8_t BME680_REGISTER_CHIPID = 0xD0; static const uint8_t BME680_REGISTER_FIELD0 = 0x1D; -const float BME680_GAS_LOOKUP_TABLE_1[16] PROGMEM = {0.0, 0.0, 0.0, 0.0, 0.0, -1.0, 0.0, -0.8, - 0.0, 0.0, -0.2, -0.5, 0.0, -1.0, 0.0, 0.0}; +constexpr float BME680_GAS_LOOKUP_TABLE_1[16] PROGMEM = {0.0, 0.0, 0.0, 0.0, 0.0, -1.0, 0.0, -0.8, + 0.0, 0.0, -0.2, -0.5, 0.0, -1.0, 0.0, 0.0}; -const float BME680_GAS_LOOKUP_TABLE_2[16] PROGMEM = {0.0, 0.0, 0.0, 0.0, 0.1, 0.7, 0.0, -0.8, - -0.1, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0}; +constexpr float BME680_GAS_LOOKUP_TABLE_2[16] PROGMEM = {0.0, 0.0, 0.0, 0.0, 0.1, 0.7, 0.0, -0.8, + -0.1, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0}; [[maybe_unused]] static const char *oversampling_to_str(BME680Oversampling oversampling) { switch (oversampling) { @@ -233,8 +233,6 @@ void BME680Component::dump_config() { } } -float BME680Component::get_setup_priority() const { return setup_priority::DATA; } - void BME680Component::update() { uint8_t meas_control = 0; // No need to fetch, we're setting all fields meas_control |= (this->temperature_oversampling_ & 0b111) << 5; diff --git a/esphome/components/bme680/bme680.h b/esphome/components/bme680/bme680.h index cfa7aaca20..239823fa8c 100644 --- a/esphome/components/bme680/bme680.h +++ b/esphome/components/bme680/bme680.h @@ -32,8 +32,8 @@ enum BME680Oversampling { /// Struct for storing calibration data for the BME680. struct BME680CalibrationData { uint16_t t1; - uint16_t t2; - uint8_t t3; + int16_t t2; + int8_t t3; uint16_t p1; int16_t p2; @@ -99,7 +99,6 @@ class BME680Component : public PollingComponent, public i2c::I2CDevice { // (In most use cases you won't need these) void setup() override; void dump_config() override; - float get_setup_priority() const override; void update() override; protected: diff --git a/esphome/components/bme680_bsec/__init__.py b/esphome/components/bme680_bsec/__init__.py index 06e641d34d..a86e061cd4 100644 --- a/esphome/components/bme680_bsec/__init__.py +++ b/esphome/components/bme680_bsec/__init__.py @@ -89,8 +89,9 @@ async def to_code(config): var.set_state_save_interval(config[CONF_STATE_SAVE_INTERVAL].total_milliseconds) ) - # Although this component does not use SPI, the BSEC library requires the SPI library + # Although this component does not use SPI/Wire directly, the BSEC library requires them cg.add_library("SPI", None) + cg.add_library("Wire", None) cg.add_define("USE_BSEC") cg.add_library("boschsensortec/BSEC Software Library", "1.6.1480") diff --git a/esphome/components/bme680_bsec/bme680_bsec.cpp b/esphome/components/bme680_bsec/bme680_bsec.cpp index d969c8fd98..bb0417b823 100644 --- a/esphome/components/bme680_bsec/bme680_bsec.cpp +++ b/esphome/components/bme680_bsec/bme680_bsec.cpp @@ -181,8 +181,6 @@ void BME680BSECComponent::dump_config() { LOG_SENSOR(" ", "Breath VOC Equivalent", this->breath_voc_equivalent_sensor_); } -float BME680BSECComponent::get_setup_priority() const { return setup_priority::DATA; } - void BME680BSECComponent::loop() { this->run_(); @@ -273,10 +271,16 @@ void BME680BSECComponent::read_() { int64_t curr_time_ns = this->get_time_ns_(); if (this->bme680_settings_.trigger_measurement) { + uint32_t start = millis(); while (this->bme680_.power_mode != BME680_SLEEP_MODE) { + if (millis() - start > 50) { + ESP_LOGE(TAG, "Timeout waiting for BME680 to enter sleep mode"); + return; + } this->bme680_status_ = bme680_get_sensor_mode(&this->bme680_); if (this->bme680_status_ != BME680_OK) { - ESP_LOGW(TAG, "Failed to get sensor mode (BME680 Error Code %d)", this->bme680_status_); + ESP_LOGE(TAG, "Failed to get sensor mode (BME680 Error Code %d)", this->bme680_status_); + return; } } } @@ -385,7 +389,7 @@ void BME680BSECComponent::publish_(const bsec_output_t *outputs, uint8_t num_out switch (outputs[i].sensor_id) { case BSEC_OUTPUT_IAQ: case BSEC_OUTPUT_STATIC_IAQ: { - uint8_t accuracy = outputs[i].accuracy; + uint8_t accuracy = std::min(outputs[i].accuracy, std::size(IAQ_ACCURACY_STATES) - 1); this->queue_push_([this, signal]() { this->publish_sensor_(this->iaq_sensor_, signal); }); this->queue_push_([this, accuracy]() { this->publish_sensor_(this->iaq_accuracy_text_sensor_, IAQ_ACCURACY_STATES[accuracy]); @@ -517,7 +521,7 @@ int BME680BSECComponent::reinit_bsec_lib_() { } void BME680BSECComponent::load_state_() { - uint32_t hash = fnv1_hash("bme680_bsec_state_" + this->device_id_); + uint32_t hash = fnv1_hash_extend(fnv1_hash("bme680_bsec_state_"), this->device_id_); this->bsec_state_ = global_preferences->make_preference(hash, true); if (!this->bsec_state_.load(&this->bsec_state_data_)) { diff --git a/esphome/components/bme680_bsec/bme680_bsec.h b/esphome/components/bme680_bsec/bme680_bsec.h index e52dbe964b..22aa2789e6 100644 --- a/esphome/components/bme680_bsec/bme680_bsec.h +++ b/esphome/components/bme680_bsec/bme680_bsec.h @@ -7,6 +7,7 @@ #include "esphome/core/preferences.h" #include "esphome/core/defines.h" #include +#include #ifdef USE_BSEC #include @@ -64,7 +65,6 @@ class BME680BSECComponent : public Component, public i2c::I2CDevice { void setup() override; void dump_config() override; - float get_setup_priority() const override; void loop() override; protected: diff --git a/esphome/components/bme68x_bsec2/__init__.py b/esphome/components/bme68x_bsec2/__init__.py index e421efb2d6..5f0afa9c9f 100644 --- a/esphome/components/bme68x_bsec2/__init__.py +++ b/esphome/components/bme68x_bsec2/__init__.py @@ -178,13 +178,16 @@ async def to_code_base(config): bsec2_arr = cg.progmem_array(config[CONF_RAW_DATA_ID], rhs) cg.add(var.set_bsec2_configuration(bsec2_arr, len(rhs))) - # Although this component does not use SPI, the BSEC2 Arduino library requires the SPI library + # The BSEC2 and BME68x Arduino libraries unconditionally include Wire.h and + # SPI.h in their source files, so these libraries must be available even though + # ESPHome uses its own I2C/SPI abstractions instead of the Arduino ones. if core.CORE.using_arduino: + cg.add_library("Wire", None) cg.add_library("SPI", None) cg.add_library( "BME68x Sensor library", - "1.3.40408", - "https://github.com/boschsensortec/Bosch-BME68x-Library", + None, + "https://github.com/boschsensortec/Bosch-BME68x-Library#v1.3.40408", ) cg.add_library( "BSEC2 Software Library", diff --git a/esphome/components/bme68x_bsec2/bme68x_bsec2.cpp b/esphome/components/bme68x_bsec2/bme68x_bsec2.cpp index 91383c8d45..d4ac57d750 100644 --- a/esphome/components/bme68x_bsec2/bme68x_bsec2.cpp +++ b/esphome/components/bme68x_bsec2/bme68x_bsec2.cpp @@ -6,10 +6,7 @@ #ifdef USE_BSEC2 #include "bme68x_bsec2.h" -#include - -namespace esphome { -namespace bme68x_bsec2 { +namespace esphome::bme68x_bsec2 { #define BME68X_BSEC2_ALGORITHM_OUTPUT_LOG(a) (a == ALGORITHM_OUTPUT_CLASSIFICATION ? "Classification" : "Regression") #define BME68X_BSEC2_OPERATING_AGE_LOG(o) (o == OPERATING_AGE_4D ? "4 days" : "28 days") @@ -18,9 +15,19 @@ namespace bme68x_bsec2 { static const char *const TAG = "bme68x_bsec2.sensor"; -static const std::string IAQ_ACCURACY_STATES[4] = {"Stabilizing", "Uncertain", "Calibrating", "Calibrated"}; +static constexpr const char *const IAQ_ACCURACY_STATES[4] = {"Stabilizing", "Uncertain", "Calibrating", "Calibrated"}; + +static bool is_no_new_data_warning(int8_t status) { +#ifdef BME68X_W_NO_NEW_DATA + return status == BME68X_W_NO_NEW_DATA; +#else + return status == 2; +#endif +} void BME68xBSEC2Component::setup() { + this->warn_if_blocking_over_ = 60; // initial reads may block for up to 60ms + this->bsec_status_ = bsec_init_m(&this->bsec_instance_); if (this->bsec_status_ != BSEC_OK) { this->mark_failed(); @@ -82,7 +89,7 @@ void BME68xBSEC2Component::dump_config() { " Operating age: %s\n" " Sample rate: %s\n" " Voltage: %s\n" - " State save interval: %ims\n" + " State save interval: %" PRIu32 "ms\n" " Temperature offset: %.2f", BME68X_BSEC2_OPERATING_AGE_LOG(this->operating_age_), BME68X_BSEC2_SAMPLE_RATE_LOG(this->sample_rate_), BME68X_BSEC2_VOLTAGE_LOG(this->voltage_), this->state_save_interval_ms_, this->temperature_offset_); @@ -106,8 +113,6 @@ void BME68xBSEC2Component::dump_config() { #endif } -float BME68xBSEC2Component::get_setup_priority() const { return setup_priority::DATA; } - void BME68xBSEC2Component::loop() { this->run_(); @@ -116,7 +121,8 @@ void BME68xBSEC2Component::loop() { } else { this->status_clear_error(); } - if (this->bsec_status_ > BSEC_OK || this->bme68x_status_ > BME68X_OK) { + const bool has_bme68x_warning = this->bme68x_status_ > BME68X_OK && !is_no_new_data_warning(this->bme68x_status_); + if (this->bsec_status_ > BSEC_OK || has_bme68x_warning) { this->status_set_warning(); } else { this->status_clear_warning(); @@ -132,7 +138,7 @@ void BME68xBSEC2Component::loop() { void BME68xBSEC2Component::set_config_(const uint8_t *config, uint32_t len) { if (len > BSEC_MAX_PROPERTY_BLOB_SIZE) { - ESP_LOGE(TAG, "Configuration is larger than BSEC_MAX_PROPERTY_BLOB_SIZE"); + ESP_LOGE(TAG, "Configuration blob too large"); this->mark_failed(); return; } @@ -214,14 +220,12 @@ void BME68xBSEC2Component::run_() { if (curr_time_ns < this->bsec_settings_.next_call) { return; } - uint8_t status; - ESP_LOGV(TAG, "Performing sensor run"); struct bme68x_conf bme68x_conf; this->bsec_status_ = bsec_sensor_control_m(&this->bsec_instance_, curr_time_ns, &this->bsec_settings_); if (this->bsec_status_ < BSEC_OK) { - ESP_LOGW(TAG, "Failed to fetch sensor control settings (BSEC2 error code %d)", this->bsec_status_); + ESP_LOGW(TAG, "Fetching control settings failed (BSEC2 error code %d)", this->bsec_status_); return; } @@ -237,9 +241,9 @@ void BME68xBSEC2Component::run_() { this->bme68x_heatr_conf_.heatr_temp = this->bsec_settings_.heater_temperature; this->bme68x_heatr_conf_.heatr_dur = this->bsec_settings_.heater_duration; - // status = bme68x_set_op_mode(this->bsec_settings_.op_mode, &this->bme68x_); - status = bme68x_set_heatr_conf(BME68X_FORCED_MODE, &this->bme68x_heatr_conf_, &this->bme68x_); - status = bme68x_set_op_mode(BME68X_FORCED_MODE, &this->bme68x_); + // this->bme68x_status_ = bme68x_set_op_mode(this->bsec_settings_.op_mode, &this->bme68x_); + this->bme68x_status_ = bme68x_set_heatr_conf(BME68X_FORCED_MODE, &this->bme68x_heatr_conf_, &this->bme68x_); + this->bme68x_status_ = bme68x_set_op_mode(BME68X_FORCED_MODE, &this->bme68x_); this->op_mode_ = BME68X_FORCED_MODE; ESP_LOGV(TAG, "Using forced mode"); @@ -261,9 +265,8 @@ void BME68xBSEC2Component::run_() { BSEC_TOTAL_HEAT_DUR - (bme68x_get_meas_dur(BME68X_PARALLEL_MODE, &bme68x_conf, &this->bme68x_) / INT64_C(1000)); - status = bme68x_set_heatr_conf(BME68X_PARALLEL_MODE, &this->bme68x_heatr_conf_, &this->bme68x_); - - status = bme68x_set_op_mode(BME68X_PARALLEL_MODE, &this->bme68x_); + this->bme68x_status_ = bme68x_set_heatr_conf(BME68X_PARALLEL_MODE, &this->bme68x_heatr_conf_, &this->bme68x_); + this->bme68x_status_ = bme68x_set_op_mode(BME68X_PARALLEL_MODE, &this->bme68x_); this->op_mode_ = BME68X_PARALLEL_MODE; ESP_LOGV(TAG, "Using parallel mode"); } @@ -278,29 +281,21 @@ void BME68xBSEC2Component::run_() { } if (this->bsec_settings_.trigger_measurement && this->bsec_settings_.op_mode != BME68X_SLEEP_MODE) { - uint32_t meas_dur = 0; - meas_dur = bme68x_get_meas_dur(this->op_mode_, &bme68x_conf, &this->bme68x_); - ESP_LOGV(TAG, "Queueing read in %uus", meas_dur); - this->set_timeout("read", meas_dur / 1000, [this, curr_time_ns]() { this->read_(curr_time_ns); }); + bme68x_get_conf(&bme68x_conf, &this->bme68x_); + uint32_t meas_dur = bme68x_get_meas_dur(this->op_mode_, &bme68x_conf, &this->bme68x_); + ESP_LOGV(TAG, "Queueing read in %" PRIu32 "us", meas_dur); + this->trigger_time_ns_ = curr_time_ns; + this->set_timeout("read", meas_dur / 1000, [this]() { this->read_(this->trigger_time_ns_); }); } else { - ESP_LOGV(TAG, "Measurement not required"); - this->read_(curr_time_ns); + ESP_LOGV(TAG, "Measurement not required, queueing immediate read"); + this->trigger_time_ns_ = curr_time_ns; + this->set_timeout("read", 0, [this]() { this->read_(this->trigger_time_ns_); }); } } void BME68xBSEC2Component::read_(int64_t trigger_time_ns) { ESP_LOGV(TAG, "Reading data"); - if (this->bsec_settings_.trigger_measurement) { - uint8_t current_op_mode; - this->bme68x_status_ = bme68x_get_op_mode(¤t_op_mode, &this->bme68x_); - - if (current_op_mode == BME68X_SLEEP_MODE) { - ESP_LOGV(TAG, "Still in sleep mode, doing nothing"); - return; - } - } - if (!this->bsec_settings_.process_data) { ESP_LOGV(TAG, "Data processing not required"); return; @@ -310,12 +305,16 @@ void BME68xBSEC2Component::read_(int64_t trigger_time_ns) { uint8_t nFields = 0; this->bme68x_status_ = bme68x_get_data(this->op_mode_, &data[0], &nFields, &this->bme68x_); + if (is_no_new_data_warning(this->bme68x_status_)) { + ESP_LOGV(TAG, "BME68X did not provide new data"); + return; + } if (this->bme68x_status_ != BME68X_OK) { - ESP_LOGW(TAG, "Failed to get sensor data (BME68X error code %d)", this->bme68x_status_); + ESP_LOGW(TAG, "Fetching data failed (BME68X error code %d)", this->bme68x_status_); return; } if (nFields < 1) { - ESP_LOGD(TAG, "BME68X did not provide new data"); + ESP_LOGV(TAG, "BME68X did not provide new fields"); return; } @@ -374,7 +373,7 @@ void BME68xBSEC2Component::read_(int64_t trigger_time_ns) { uint8_t num_outputs = BSEC_NUMBER_OUTPUTS; this->bsec_status_ = bsec_do_steps_m(&this->bsec_instance_, inputs, num_inputs, outputs, &num_outputs); if (this->bsec_status_ != BSEC_OK) { - ESP_LOGW(TAG, "BSEC2 failed to process signals (BSEC2 error code %d)", this->bsec_status_); + ESP_LOGW(TAG, "Signal processing failed (BSEC2 error code %d)", this->bsec_status_); return; } if (num_outputs < 1) { @@ -440,6 +439,7 @@ void BME68xBSEC2Component::publish_(const bsec_output_t *outputs, uint8_t num_ou } } if (update_accuracy) { + max_accuracy = std::min(max_accuracy, std::size(IAQ_ACCURACY_STATES) - 1); #ifdef USE_SENSOR this->queue_push_( [this, max_accuracy]() { this->publish_sensor_(this->iaq_accuracy_sensor_, max_accuracy, true); }); @@ -474,7 +474,7 @@ void BME68xBSEC2Component::publish_sensor_(sensor::Sensor *sensor, float value, #endif #ifdef USE_TEXT_SENSOR -void BME68xBSEC2Component::publish_sensor_(text_sensor::TextSensor *sensor, const std::string &value) { +void BME68xBSEC2Component::publish_sensor_(text_sensor::TextSensor *sensor, const char *value) { if (!sensor || (sensor->has_state() && sensor->state == value)) { return; } @@ -526,6 +526,5 @@ void BME68xBSEC2Component::save_state_(uint8_t accuracy) { ESP_LOGI(TAG, "Saved state"); } -} // namespace bme68x_bsec2 -} // namespace esphome +} // namespace esphome::bme68x_bsec2 #endif diff --git a/esphome/components/bme68x_bsec2/bme68x_bsec2.h b/esphome/components/bme68x_bsec2/bme68x_bsec2.h index 86d3e5dfbf..9317229a1f 100644 --- a/esphome/components/bme68x_bsec2/bme68x_bsec2.h +++ b/esphome/components/bme68x_bsec2/bme68x_bsec2.h @@ -19,8 +19,7 @@ #include -namespace esphome { -namespace bme68x_bsec2 { +namespace esphome::bme68x_bsec2 { enum AlgorithmOutput { ALGORITHM_OUTPUT_IAQ, @@ -48,7 +47,6 @@ class BME68xBSEC2Component : public Component { public: void setup() override; void dump_config() override; - float get_setup_priority() const override; void loop() override; void set_algorithm_output(AlgorithmOutput algorithm_output) { this->algorithm_output_ = algorithm_output; } @@ -98,7 +96,7 @@ class BME68xBSEC2Component : public Component { void publish_sensor_(sensor::Sensor *sensor, float value, bool change_only = false); #endif #ifdef USE_TEXT_SENSOR - void publish_sensor_(text_sensor::TextSensor *sensor, const std::string &value); + void publish_sensor_(text_sensor::TextSensor *sensor, const char *value); #endif void load_state_(); @@ -109,37 +107,12 @@ class BME68xBSEC2Component : public Component { struct bme68x_dev bme68x_; bsec_bme_settings_t bsec_settings_; bsec_version_t version_; - uint8_t bsec_instance_[BSEC_INSTANCE_SIZE]; - struct bme68x_heatr_conf bme68x_heatr_conf_; - uint8_t op_mode_; // operating mode of sensor - bsec_library_return_t bsec_status_{BSEC_OK}; - int8_t bme68x_status_{BME68X_OK}; - - int64_t last_time_ms_{0}; - uint32_t millis_overflow_counter_{0}; std::queue> queue_; + ESPPreferenceObject bsec_state_; uint8_t const *bsec2_configuration_{nullptr}; - uint32_t bsec2_configuration_length_{0}; - bool bsec2_blob_configured_{false}; - - ESPPreferenceObject bsec_state_; - uint32_t state_save_interval_ms_{21600000}; // 6 hours - 4 times a day - uint32_t last_state_save_ms_ = 0; - - float temperature_offset_{0}; - - AlgorithmOutput algorithm_output_{ALGORITHM_OUTPUT_IAQ}; - OperatingAge operating_age_{OPERATING_AGE_28D}; - Voltage voltage_{VOLTAGE_3_3V}; - - SampleRate sample_rate_{SAMPLE_RATE_LP}; // Core/gas sample rate - SampleRate temperature_sample_rate_{SAMPLE_RATE_DEFAULT}; - SampleRate pressure_sample_rate_{SAMPLE_RATE_DEFAULT}; - SampleRate humidity_sample_rate_{SAMPLE_RATE_DEFAULT}; - #ifdef USE_SENSOR sensor::Sensor *temperature_sensor_{nullptr}; sensor::Sensor *pressure_sensor_{nullptr}; @@ -154,8 +127,32 @@ class BME68xBSEC2Component : public Component { #ifdef USE_TEXT_SENSOR text_sensor::TextSensor *iaq_accuracy_text_sensor_{nullptr}; #endif + + int64_t last_time_ms_{0}; + int64_t trigger_time_ns_{0}; // Stored for set_timeout lambda to help avoid heap allocation on supported 32-bit + // toolchains with small std::function SBO + + uint32_t state_save_interval_ms_{21600000}; // 6 hours - 4 times a day + uint32_t last_state_save_ms_{0}; + uint32_t millis_overflow_counter_{0}; + uint32_t bsec2_configuration_length_{0}; + bsec_library_return_t bsec_status_{BSEC_OK}; + + float temperature_offset_{0}; + + AlgorithmOutput algorithm_output_{ALGORITHM_OUTPUT_IAQ}; + OperatingAge operating_age_{OPERATING_AGE_28D}; + Voltage voltage_{VOLTAGE_3_3V}; + SampleRate sample_rate_{SAMPLE_RATE_LP}; // Core/gas sample rate + SampleRate temperature_sample_rate_{SAMPLE_RATE_DEFAULT}; + SampleRate pressure_sample_rate_{SAMPLE_RATE_DEFAULT}; + SampleRate humidity_sample_rate_{SAMPLE_RATE_DEFAULT}; + + uint8_t bsec_instance_[BSEC_INSTANCE_SIZE]; + uint8_t op_mode_; // operating mode of sensor + int8_t bme68x_status_{BME68X_OK}; + bool bsec2_blob_configured_{false}; }; -} // namespace bme68x_bsec2 -} // namespace esphome +} // namespace esphome::bme68x_bsec2 #endif diff --git a/esphome/components/bmi160/bmi160.cpp b/esphome/components/bmi160/bmi160.cpp index 4fcc3edb82..ed92979d24 100644 --- a/esphome/components/bmi160/bmi160.cpp +++ b/esphome/components/bmi160/bmi160.cpp @@ -6,6 +6,7 @@ namespace esphome { namespace bmi160 { static const char *const TAG = "bmi160"; +static constexpr uint32_t GYRO_WAKEUP_TIMEOUT_MS = 100; const uint8_t BMI160_REGISTER_CHIPID = 0x00; @@ -144,7 +145,7 @@ void BMI160Component::internal_setup_(int stage) { } ESP_LOGV(TAG, " Waiting for gyroscope to wake up"); // wait between 51 & 81ms, doing 100 to be safe - this->set_timeout(10, [this]() { this->internal_setup_(2); }); + this->set_timeout(GYRO_WAKEUP_TIMEOUT_MS, [this]() { this->internal_setup_(2); }); break; case 2: @@ -263,7 +264,6 @@ void BMI160Component::update() { this->status_clear_warning(); } -float BMI160Component::get_setup_priority() const { return setup_priority::DATA; } } // namespace bmi160 } // namespace esphome diff --git a/esphome/components/bmi160/bmi160.h b/esphome/components/bmi160/bmi160.h index 47691a4de9..16cab69733 100644 --- a/esphome/components/bmi160/bmi160.h +++ b/esphome/components/bmi160/bmi160.h @@ -14,8 +14,6 @@ class BMI160Component : public PollingComponent, public i2c::I2CDevice { void update() override; - float get_setup_priority() const override; - void set_accel_x_sensor(sensor::Sensor *accel_x_sensor) { accel_x_sensor_ = accel_x_sensor; } void set_accel_y_sensor(sensor::Sensor *accel_y_sensor) { accel_y_sensor_ = accel_y_sensor; } void set_accel_z_sensor(sensor::Sensor *accel_z_sensor) { accel_z_sensor_ = accel_z_sensor; } diff --git a/esphome/components/bmp085/bmp085.cpp b/esphome/components/bmp085/bmp085.cpp index 657da34f9b..9a383b2654 100644 --- a/esphome/components/bmp085/bmp085.cpp +++ b/esphome/components/bmp085/bmp085.cpp @@ -131,7 +131,6 @@ bool BMP085Component::set_mode_(uint8_t mode) { ESP_LOGV(TAG, "Setting mode to 0x%02X", mode); return this->write_byte(BMP085_REGISTER_CONTROL, mode); } -float BMP085Component::get_setup_priority() const { return setup_priority::DATA; } } // namespace bmp085 } // namespace esphome diff --git a/esphome/components/bmp085/bmp085.h b/esphome/components/bmp085/bmp085.h index d84b4d43ef..c7315827e0 100644 --- a/esphome/components/bmp085/bmp085.h +++ b/esphome/components/bmp085/bmp085.h @@ -18,8 +18,6 @@ class BMP085Component : public PollingComponent, public i2c::I2CDevice { void setup() override; void dump_config() override; - float get_setup_priority() const override; - protected: struct CalibrationData { int16_t ac1, ac2, ac3; diff --git a/esphome/components/bmp280_base/bmp280_base.cpp b/esphome/components/bmp280_base/bmp280_base.cpp index 728eead521..603966a2b5 100644 --- a/esphome/components/bmp280_base/bmp280_base.cpp +++ b/esphome/components/bmp280_base/bmp280_base.cpp @@ -2,7 +2,7 @@ #include "esphome/core/hal.h" #include "esphome/core/log.h" -#define BMP280_ERROR_WRONG_CHIP_ID "Wrong chip ID" +#define BMP280_ERROR_WRONG_CHIP_ID "Wrong chip ID or no response" namespace esphome { namespace bmp280_base { @@ -148,7 +148,6 @@ void BMP280Component::dump_config() { LOG_SENSOR(" ", "Pressure", this->pressure_sensor_); ESP_LOGCONFIG(TAG, " Oversampling: %s", oversampling_to_str(this->pressure_oversampling_)); } -float BMP280Component::get_setup_priority() const { return setup_priority::DATA; } inline uint8_t oversampling_to_time(BMP280Oversampling over_sampling) { return (1 << uint8_t(over_sampling)) >> 1; } diff --git a/esphome/components/bmp280_base/bmp280_base.h b/esphome/components/bmp280_base/bmp280_base.h index a47a794e96..836eafaf8b 100644 --- a/esphome/components/bmp280_base/bmp280_base.h +++ b/esphome/components/bmp280_base/bmp280_base.h @@ -64,7 +64,6 @@ class BMP280Component : public PollingComponent { void setup() override; void dump_config() override; - float get_setup_priority() const override; void update() override; protected: diff --git a/esphome/components/bmp3xx_base/bmp3xx_base.cpp b/esphome/components/bmp3xx_base/bmp3xx_base.cpp index acc28d4e85..c781252de3 100644 --- a/esphome/components/bmp3xx_base/bmp3xx_base.cpp +++ b/esphome/components/bmp3xx_base/bmp3xx_base.cpp @@ -6,8 +6,9 @@ */ #include "bmp3xx_base.h" -#include "esphome/core/log.h" #include "esphome/core/hal.h" +#include "esphome/core/log.h" +#include "esphome/core/progmem.h" #include namespace esphome { @@ -26,46 +27,18 @@ static const LogString *chip_type_to_str(uint8_t chip_type) { } } +// Oversampling strings indexed by Oversampling enum (0-5): NONE, X2, X4, X8, X16, X32 +PROGMEM_STRING_TABLE(OversamplingStrings, "None", "2x", "4x", "8x", "16x", "32x", ""); + static const LogString *oversampling_to_str(Oversampling oversampling) { - switch (oversampling) { - case Oversampling::OVERSAMPLING_NONE: - return LOG_STR("None"); - case Oversampling::OVERSAMPLING_X2: - return LOG_STR("2x"); - case Oversampling::OVERSAMPLING_X4: - return LOG_STR("4x"); - case Oversampling::OVERSAMPLING_X8: - return LOG_STR("8x"); - case Oversampling::OVERSAMPLING_X16: - return LOG_STR("16x"); - case Oversampling::OVERSAMPLING_X32: - return LOG_STR("32x"); - default: - return LOG_STR(""); - } + return OversamplingStrings::get_log_str(static_cast(oversampling), OversamplingStrings::LAST_INDEX); } +// IIR filter strings indexed by IIRFilter enum (0-7): OFF, 2, 4, 8, 16, 32, 64, 128 +PROGMEM_STRING_TABLE(IIRFilterStrings, "OFF", "2x", "4x", "8x", "16x", "32x", "64x", "128x", ""); + static const LogString *iir_filter_to_str(IIRFilter filter) { - switch (filter) { - case IIRFilter::IIR_FILTER_OFF: - return LOG_STR("OFF"); - case IIRFilter::IIR_FILTER_2: - return LOG_STR("2x"); - case IIRFilter::IIR_FILTER_4: - return LOG_STR("4x"); - case IIRFilter::IIR_FILTER_8: - return LOG_STR("8x"); - case IIRFilter::IIR_FILTER_16: - return LOG_STR("16x"); - case IIRFilter::IIR_FILTER_32: - return LOG_STR("32x"); - case IIRFilter::IIR_FILTER_64: - return LOG_STR("64x"); - case IIRFilter::IIR_FILTER_128: - return LOG_STR("128x"); - default: - return LOG_STR(""); - } + return IIRFilterStrings::get_log_str(static_cast(filter), IIRFilterStrings::LAST_INDEX); } void BMP3XXComponent::setup() { @@ -179,7 +152,6 @@ void BMP3XXComponent::dump_config() { ESP_LOGCONFIG(TAG, " Oversampling: %s", LOG_STR_ARG(oversampling_to_str(this->pressure_oversampling_))); } } -float BMP3XXComponent::get_setup_priority() const { return setup_priority::DATA; } inline uint8_t oversampling_to_time(Oversampling over_sampling) { return (1 << uint8_t(over_sampling)); } diff --git a/esphome/components/bmp3xx_base/bmp3xx_base.h b/esphome/components/bmp3xx_base/bmp3xx_base.h index 50f92e04c1..8d2312231b 100644 --- a/esphome/components/bmp3xx_base/bmp3xx_base.h +++ b/esphome/components/bmp3xx_base/bmp3xx_base.h @@ -73,7 +73,6 @@ class BMP3XXComponent : public PollingComponent { public: void setup() override; void dump_config() override; - float get_setup_priority() const override; void update() override; void set_temperature_sensor(sensor::Sensor *temperature_sensor) { temperature_sensor_ = temperature_sensor; } diff --git a/esphome/components/bmp581_base/bmp581_base.cpp b/esphome/components/bmp581_base/bmp581_base.cpp index 67c6771862..7a627eee03 100644 --- a/esphome/components/bmp581_base/bmp581_base.cpp +++ b/esphome/components/bmp581_base/bmp581_base.cpp @@ -11,57 +11,26 @@ */ #include "bmp581_base.h" -#include "esphome/core/log.h" #include "esphome/core/hal.h" +#include "esphome/core/log.h" +#include "esphome/core/progmem.h" namespace esphome::bmp581_base { static const char *const TAG = "bmp581"; +// Oversampling strings indexed by Oversampling enum (0-7): NONE, X2, X4, X8, X16, X32, X64, X128 +PROGMEM_STRING_TABLE(OversamplingStrings, "None", "2x", "4x", "8x", "16x", "32x", "64x", "128x", ""); + static const LogString *oversampling_to_str(Oversampling oversampling) { - switch (oversampling) { - case Oversampling::OVERSAMPLING_NONE: - return LOG_STR("None"); - case Oversampling::OVERSAMPLING_X2: - return LOG_STR("2x"); - case Oversampling::OVERSAMPLING_X4: - return LOG_STR("4x"); - case Oversampling::OVERSAMPLING_X8: - return LOG_STR("8x"); - case Oversampling::OVERSAMPLING_X16: - return LOG_STR("16x"); - case Oversampling::OVERSAMPLING_X32: - return LOG_STR("32x"); - case Oversampling::OVERSAMPLING_X64: - return LOG_STR("64x"); - case Oversampling::OVERSAMPLING_X128: - return LOG_STR("128x"); - default: - return LOG_STR(""); - } + return OversamplingStrings::get_log_str(static_cast(oversampling), OversamplingStrings::LAST_INDEX); } +// IIR filter strings indexed by IIRFilter enum (0-7): OFF, 2, 4, 8, 16, 32, 64, 128 +PROGMEM_STRING_TABLE(IIRFilterStrings, "OFF", "2x", "4x", "8x", "16x", "32x", "64x", "128x", ""); + static const LogString *iir_filter_to_str(IIRFilter filter) { - switch (filter) { - case IIRFilter::IIR_FILTER_OFF: - return LOG_STR("OFF"); - case IIRFilter::IIR_FILTER_2: - return LOG_STR("2x"); - case IIRFilter::IIR_FILTER_4: - return LOG_STR("4x"); - case IIRFilter::IIR_FILTER_8: - return LOG_STR("8x"); - case IIRFilter::IIR_FILTER_16: - return LOG_STR("16x"); - case IIRFilter::IIR_FILTER_32: - return LOG_STR("32x"); - case IIRFilter::IIR_FILTER_64: - return LOG_STR("64x"); - case IIRFilter::IIR_FILTER_128: - return LOG_STR("128x"); - default: - return LOG_STR(""); - } + return IIRFilterStrings::get_log_str(static_cast(filter), IIRFilterStrings::LAST_INDEX); } void BMP581Component::dump_config() { @@ -157,7 +126,7 @@ void BMP581Component::setup() { } // verify id - if (chip_id != BMP581_ASIC_ID) { + if (chip_id != BMP581_ASIC_ID && chip_id != BMP585_ASIC_ID) { ESP_LOGE(TAG, "Unknown chip ID"); this->error_code_ = ERROR_WRONG_CHIP_ID; @@ -460,7 +429,7 @@ bool BMP581Component::read_temperature_(float &temperature) { } // temperature MSB is in data[2], LSB is in data[1], XLSB in data[0] - int32_t raw_temp = (int32_t) data[2] << 16 | (int32_t) data[1] << 8 | (int32_t) data[0]; + int32_t raw_temp = static_cast(encode_uint32(data[2], data[1], data[0], 0)) >> 8; temperature = (float) (raw_temp / 65536.0); // convert measurement to degrees Celsius (page 22 of datasheet) return true; @@ -489,7 +458,7 @@ bool BMP581Component::read_temperature_and_pressure_(float &temperature, float & } // temperature MSB is in data[2], LSB is in data[1], XLSB in data[0] - int32_t raw_temp = (int32_t) data[2] << 16 | (int32_t) data[1] << 8 | (int32_t) data[0]; + int32_t raw_temp = static_cast(encode_uint32(data[2], data[1], data[0], 0)) >> 8; temperature = (float) (raw_temp / 65536.0); // convert measurement to degrees Celsius (page 22 of datasheet) // pressure MSB is in data[5], LSB is in data[4], XLSB in data[3] @@ -500,14 +469,18 @@ bool BMP581Component::read_temperature_and_pressure_(float &temperature, float & } bool BMP581Component::reset_() { + // - activates interface (only relevant for SPI mode) // - writes reset command to the command register // - waits for sensor to complete reset + // - activates interface (only relevant for SPI mode) // - returns the Power-On-Reboot interrupt status, which is asserted if successful + // activates communication interface (SPI only) + this->activate_interface(); + // writes reset command to BMP's command register if (!this->bmp_write_byte(BMP581_COMMAND, RESET_COMMAND)) { ESP_LOGE(TAG, "Failed to write reset command"); - return false; } @@ -515,6 +488,9 @@ bool BMP581Component::reset_() { // - round up to 3 ms delay(3); + // reactivates communication interface after reset (SPI only) + this->activate_interface(); + // read interrupt status register if (!this->bmp_read_byte(BMP581_INT_STATUS, &this->int_status_.reg)) { ESP_LOGE(TAG, "Failed to read interrupt status register"); @@ -522,7 +498,7 @@ bool BMP581Component::reset_() { return false; } - // Power-On-Reboot bit is asserted if sensor successfully reset + // power-On-Reboot bit is asserted if sensor successfully reset return this->int_status_.bit.por; } diff --git a/esphome/components/bmp581_base/bmp581_base.h b/esphome/components/bmp581_base/bmp581_base.h index d99c420272..1a73a91558 100644 --- a/esphome/components/bmp581_base/bmp581_base.h +++ b/esphome/components/bmp581_base/bmp581_base.h @@ -8,7 +8,8 @@ namespace esphome::bmp581_base { static const uint8_t BMP581_ASIC_ID = 0x50; // BMP581's ASIC chip ID (page 51 of datasheet) -static const uint8_t RESET_COMMAND = 0xB6; // Soft reset command +static const uint8_t BMP585_ASIC_ID = 0x51; +static const uint8_t RESET_COMMAND = 0xB6; // Soft reset command // BMP581 Register Addresses enum { @@ -87,6 +88,9 @@ class BMP581Component : public PollingComponent { virtual bool bmp_read_bytes(uint8_t a_register, uint8_t *data, size_t len) = 0; virtual bool bmp_write_bytes(uint8_t a_register, uint8_t *data, size_t len) = 0; + // Interface activation function. Only used for SPI interface; no-op for I2C. + virtual void activate_interface() {} + sensor::Sensor *temperature_sensor_{nullptr}; sensor::Sensor *pressure_sensor_{nullptr}; diff --git a/esphome/components/bmp581_spi/__init__.py b/esphome/components/bmp581_spi/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/esphome/components/bmp581_spi/bmp581_spi.cpp b/esphome/components/bmp581_spi/bmp581_spi.cpp new file mode 100644 index 0000000000..01435880f0 --- /dev/null +++ b/esphome/components/bmp581_spi/bmp581_spi.cpp @@ -0,0 +1,73 @@ +#include +#include + +#include "bmp581_spi.h" +#include "esphome/components/bmp581_base/bmp581_base.h" +#include "esphome/components/spi/spi.h" + +namespace esphome::bmp581_spi { + +static const char *const TAG = "bmp581_spi"; + +// OR (|) register with BMP_SPI_READ for read +inline constexpr uint8_t BMP_SPI_READ = 0x80; + +// AND (&) register with BMP_SPI_WRITE for write +inline constexpr uint8_t BMP_SPI_WRITE = 0x7F; + +void BMP581SPIComponent::dump_config() { + BMP581Component::dump_config(); + LOG_SPI_DEVICE(this); +} + +void BMP581SPIComponent::setup() { + this->spi_setup(); + BMP581Component::setup(); +} + +void BMP581SPIComponent::activate_interface() { + // - forces the device into SPI mode using a dummy read + uint8_t dummy_read = 0; + this->bmp_read_byte(bmp581_base::BMP581_CHIP_ID, &dummy_read); +} + +// In SPI mode, only 7 bits of the register addresses are used; the MSB of register address is not used +// and replaced by a read/write bit (RW = ‘0’ for write and RW = ‘1’ for read). +// Example: address 0xF7 is accessed by using SPI register address 0x77. For write access, the byte +// 0x77 is transferred, for read access, the byte 0xF7 is transferred. +// The expressions BMP_SPI_READ (| with register) and BMP_SPI_WRITE (& with register) +// are defined for readability. +// https://www.bosch-sensortec.com/media/boschsensortec/downloads/datasheets/bst-bmp581-ds004.pdf + +bool BMP581SPIComponent::bmp_read_byte(uint8_t a_register, uint8_t *data) { + this->enable(); + this->transfer_byte(a_register | BMP_SPI_READ); + *data = this->transfer_byte(0); + this->disable(); + return true; +} + +bool BMP581SPIComponent::bmp_write_byte(uint8_t a_register, uint8_t data) { + this->enable(); + this->transfer_byte(a_register & BMP_SPI_WRITE); + this->transfer_byte(data); + this->disable(); + return true; +} + +bool BMP581SPIComponent::bmp_read_bytes(uint8_t a_register, uint8_t *data, size_t len) { + this->enable(); + this->transfer_byte(a_register | BMP_SPI_READ); + this->read_array(data, len); + this->disable(); + return true; +} + +bool BMP581SPIComponent::bmp_write_bytes(uint8_t a_register, uint8_t *data, size_t len) { + this->enable(); + this->transfer_byte(a_register & BMP_SPI_WRITE); + this->write_array(data, len); + this->disable(); + return true; +} +} // namespace esphome::bmp581_spi diff --git a/esphome/components/bmp581_spi/bmp581_spi.h b/esphome/components/bmp581_spi/bmp581_spi.h new file mode 100644 index 0000000000..57f75588d5 --- /dev/null +++ b/esphome/components/bmp581_spi/bmp581_spi.h @@ -0,0 +1,24 @@ +#pragma once + +#include "esphome/components/bmp581_base/bmp581_base.h" +#include "esphome/components/spi/spi.h" + +namespace esphome::bmp581_spi { + +// BMP581 is technically compatible with SPI Mode0 and Mode3. Default to Mode3. +class BMP581SPIComponent : public esphome::bmp581_base::BMP581Component, + public spi::SPIDevice { + public: + void setup() override; + bool bmp_read_byte(uint8_t a_register, uint8_t *data) override; + bool bmp_write_byte(uint8_t a_register, uint8_t data) override; + bool bmp_read_bytes(uint8_t a_register, uint8_t *data, size_t len) override; + bool bmp_write_bytes(uint8_t a_register, uint8_t *data, size_t len) override; + void dump_config() override; + + protected: + void activate_interface() override; +}; + +} // namespace esphome::bmp581_spi diff --git a/esphome/components/bmp581_spi/sensor.py b/esphome/components/bmp581_spi/sensor.py new file mode 100644 index 0000000000..75f60b2460 --- /dev/null +++ b/esphome/components/bmp581_spi/sensor.py @@ -0,0 +1,48 @@ +import logging + +import esphome.codegen as cg +from esphome.components import spi +from esphome.components.spi import CONF_SPI_MODE +import esphome.config_validation as cv + +from ..bmp581_base import CONFIG_SCHEMA_BASE, to_code_base + +AUTO_LOAD = ["bmp581_base"] +CODEOWNERS = ["@kahrendt", "@danielkent-net"] +DEPENDENCIES = ["spi"] + +_LOGGER = logging.getLogger(__name__) + +VALID_SPI_MODES = { + 0: "MODE0", + "0": "MODE0", + "MODE0": "MODE0", + 3: "MODE3", + "3": "MODE3", + "MODE3": "MODE3", +} + +bmp581_ns = cg.esphome_ns.namespace("bmp581_spi") +BMP581SPIComponent = bmp581_ns.class_( + "BMP581SPIComponent", cg.PollingComponent, spi.SPIDevice +) + + +def check_spi_mode(config): + spi_mode = config.get(CONF_SPI_MODE) + if spi_mode not in VALID_SPI_MODES: + raise cv.Invalid("BMP581 only supports SPI mode 3") + return config + + +CONFIG_SCHEMA = cv.All( + CONFIG_SCHEMA_BASE.extend(spi.spi_device_schema(default_mode="mode3")).extend( + {cv.GenerateID(): cv.declare_id(BMP581SPIComponent)} + ), + check_spi_mode, +) + + +async def to_code(config): + var = await to_code_base(config) + await spi.register_spi_device(var, config) diff --git a/esphome/components/bthome_mithermometer/bthome_ble.cpp b/esphome/components/bthome_mithermometer/bthome_ble.cpp index 2b73d8735c..32278dbfbd 100644 --- a/esphome/components/bthome_mithermometer/bthome_ble.cpp +++ b/esphome/components/bthome_mithermometer/bthome_ble.cpp @@ -10,7 +10,12 @@ #ifdef USE_ESP32 +#include +#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(6, 0, 0) +#include +#else #include "mbedtls/ccm.h" +#endif namespace esphome { namespace bthome_mithermometer { @@ -196,6 +201,37 @@ bool BTHomeMiThermometer::decrypt_bthome_payload_(const std::vector &da const uint8_t *ciphertext = data.data() + 1; const uint8_t *mic = data.data() + data.size() - BTHOME_MIC_SIZE; +#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(6, 0, 0) + // PSA AEAD expects ciphertext + tag concatenated + // BLE advertisement max payload is 31 bytes, so this is always sufficient + static constexpr size_t MAX_CT_WITH_TAG = 32; + uint8_t ct_with_tag[MAX_CT_WITH_TAG]; + size_t ct_with_tag_size = ciphertext_size + BTHOME_MIC_SIZE; + memcpy(ct_with_tag, ciphertext, ciphertext_size); + memcpy(ct_with_tag + ciphertext_size, mic, BTHOME_MIC_SIZE); + + psa_key_attributes_t attributes = PSA_KEY_ATTRIBUTES_INIT; + psa_set_key_type(&attributes, PSA_KEY_TYPE_AES); + psa_set_key_bits(&attributes, BTHOME_BINDKEY_SIZE * 8); + psa_set_key_usage_flags(&attributes, PSA_KEY_USAGE_DECRYPT); + psa_set_key_algorithm(&attributes, PSA_ALG_AEAD_WITH_SHORTENED_TAG(PSA_ALG_CCM, BTHOME_MIC_SIZE)); + + mbedtls_svc_key_id_t key_id; + if (psa_import_key(&attributes, this->bindkey_, BTHOME_BINDKEY_SIZE, &key_id) != PSA_SUCCESS) { + ESP_LOGVV(TAG, "psa_import_key() failed."); + return false; + } + + size_t plaintext_length; + psa_status_t status = psa_aead_decrypt(key_id, PSA_ALG_AEAD_WITH_SHORTENED_TAG(PSA_ALG_CCM, BTHOME_MIC_SIZE), + nonce.data(), nonce.size(), nullptr, 0, ct_with_tag, ct_with_tag_size, + payload.data(), ciphertext_size, &plaintext_length); + psa_destroy_key(key_id); + if (status != PSA_SUCCESS || plaintext_length != ciphertext_size) { + ESP_LOGVV(TAG, "BTHome decryption failed."); + return false; + } +#else mbedtls_ccm_context ctx; mbedtls_ccm_init(&ctx); @@ -213,6 +249,7 @@ bool BTHomeMiThermometer::decrypt_bthome_payload_(const std::vector &da ESP_LOGVV(TAG, "BTHome decryption failed (ret=%d).", ret); return false; } +#endif return true; } diff --git a/esphome/components/button/__init__.py b/esphome/components/button/__init__.py index d2f143b97e..f279b6ffe3 100644 --- a/esphome/components/button/__init__.py +++ b/esphome/components/button/__init__.py @@ -10,7 +10,6 @@ from esphome.const import ( CONF_ID, CONF_MQTT_ID, CONF_ON_PRESS, - CONF_TRIGGER_ID, CONF_WEB_SERVER, DEVICE_CLASS_EMPTY, DEVICE_CLASS_IDENTIFY, @@ -18,7 +17,11 @@ from esphome.const import ( DEVICE_CLASS_UPDATE, ) from esphome.core import CORE, CoroPriority, coroutine_with_priority -from esphome.core.entity_helpers import entity_duplicate_validator, setup_entity +from esphome.core.entity_helpers import ( + entity_duplicate_validator, + setup_device_class, + setup_entity, +) from esphome.cpp_generator import MockObjClass CODEOWNERS = ["@esphome/core"] @@ -37,10 +40,6 @@ ButtonPtr = Button.operator("ptr") PressAction = button_ns.class_("PressAction", automation.Action) -ButtonPressTrigger = button_ns.class_( - "ButtonPressTrigger", automation.Trigger.template() -) - validate_device_class = cv.one_of(*DEVICE_CLASSES, lower=True, space="_") @@ -51,11 +50,7 @@ _BUTTON_SCHEMA = ( { cv.OnlyWith(CONF_MQTT_ID, "mqtt"): cv.declare_id(mqtt.MQTTButtonComponent), cv.Optional(CONF_DEVICE_CLASS): validate_device_class, - cv.Optional(CONF_ON_PRESS): automation.validate_automation( - { - cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(ButtonPressTrigger), - } - ), + cv.Optional(CONF_ON_PRESS): automation.validate_automation({}), } ) ) @@ -84,15 +79,14 @@ def button_schema( return _BUTTON_SCHEMA.extend(schema) +@setup_entity("button") async def setup_button_core_(var, config): - await setup_entity(var, config, "button") - for conf in config.get(CONF_ON_PRESS, []): - trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) - await automation.build_automation(trigger, [], conf) + await automation.build_callback_automation( + var, "add_on_press_callback", [], conf + ) - if device_class := config.get(CONF_DEVICE_CLASS): - cg.add(var.set_device_class(device_class)) + setup_device_class(config) if mqtt_id := config.get(CONF_MQTT_ID): mqtt_ = cg.new_Pvariable(mqtt_id, var) @@ -123,7 +117,9 @@ BUTTON_PRESS_SCHEMA = maybe_simple_id( ) -@automation.register_action("button.press", PressAction, BUTTON_PRESS_SCHEMA) +@automation.register_action( + "button.press", PressAction, BUTTON_PRESS_SCHEMA, synchronous=True +) async def button_press_to_code(config, action_id, template_arg, args): paren = await cg.get_variable(config[CONF_ID]) return cg.new_Pvariable(action_id, template_arg, paren) diff --git a/esphome/components/button/button.cpp b/esphome/components/button/button.cpp index 8c06cfe59b..b1c491805e 100644 --- a/esphome/components/button/button.cpp +++ b/esphome/components/button/button.cpp @@ -20,6 +20,5 @@ void Button::press() { this->press_action(); this->press_callback_.call(); } -void Button::add_on_press_callback(std::function &&callback) { this->press_callback_.add(std::move(callback)); } } // namespace esphome::button diff --git a/esphome/components/button/button.h b/esphome/components/button/button.h index be6e080917..96e9107532 100644 --- a/esphome/components/button/button.h +++ b/esphome/components/button/button.h @@ -22,7 +22,7 @@ void log_button(const char *tag, const char *prefix, const char *type, Button *o * * A button is just a momentary switch that does not have a state, only a trigger. */ -class Button : public EntityBase, public EntityBase_DeviceClass { +class Button : public EntityBase { public: /** Press this button. This is called by the front-end. * @@ -34,7 +34,9 @@ class Button : public EntityBase, public EntityBase_DeviceClass { * * @param callback The void() callback. */ - void add_on_press_callback(std::function &&callback); + template void add_on_press_callback(F &&callback) { + this->press_callback_.add(std::forward(callback)); + } protected: /** You should implement this virtual method if you want to create your own button. diff --git a/esphome/components/bytebuffer/bytebuffer.h b/esphome/components/bytebuffer/bytebuffer.h index 030484ce32..3c68094dbc 100644 --- a/esphome/components/bytebuffer/bytebuffer.h +++ b/esphome/components/bytebuffer/bytebuffer.h @@ -263,7 +263,7 @@ class ByteBuffer { void put_uint8(uint8_t value, size_t offset) { this->data_[offset] = value; } void put_uint16(uint16_t value, size_t offset) { this->put(value, offset); } - void put_uint24(uint32_t value, size_t offset) { this->put(value, offset); } + void put_uint24(uint32_t value, size_t offset) { this->put_uint32_(value, offset, 3); } void put_uint32(uint32_t value, size_t offset) { this->put(value, offset); } void put_uint64(uint64_t value, size_t offset) { this->put(value, offset); } // Signed versions of the put functions diff --git a/esphome/components/camera/buffer_impl.cpp b/esphome/components/camera/buffer_impl.cpp index d17a4e2707..97cdd2e104 100644 --- a/esphome/components/camera/buffer_impl.cpp +++ b/esphome/components/camera/buffer_impl.cpp @@ -3,18 +3,22 @@ namespace esphome::camera { BufferImpl::BufferImpl(size_t size) { - this->data_ = this->allocator_.allocate(size); + RAMAllocator allocator; + this->data_ = allocator.allocate(size); this->size_ = size; } BufferImpl::BufferImpl(CameraImageSpec *spec) { - this->data_ = this->allocator_.allocate(spec->bytes_per_image()); + RAMAllocator allocator; + this->data_ = allocator.allocate(spec->bytes_per_image()); this->size_ = spec->bytes_per_image(); } BufferImpl::~BufferImpl() { - if (this->data_ != nullptr) - this->allocator_.deallocate(this->data_, this->size_); + if (this->data_ != nullptr) { + RAMAllocator allocator; + allocator.deallocate(this->data_, this->size_); + } } } // namespace esphome::camera diff --git a/esphome/components/camera/buffer_impl.h b/esphome/components/camera/buffer_impl.h index 46398295fa..5e42df7957 100644 --- a/esphome/components/camera/buffer_impl.h +++ b/esphome/components/camera/buffer_impl.h @@ -18,7 +18,6 @@ class BufferImpl : public Buffer { ~BufferImpl() override; protected: - RAMAllocator allocator_; size_t size_{}; uint8_t *data_{}; }; diff --git a/esphome/components/camera_encoder/__init__.py b/esphome/components/camera_encoder/__init__.py index 89181d27b4..3bbeae7835 100644 --- a/esphome/components/camera_encoder/__init__.py +++ b/esphome/components/camera_encoder/__init__.py @@ -50,7 +50,7 @@ async def to_code(config: ConfigType) -> None: buffer = cg.new_Pvariable(config[CONF_ENCODER_BUFFER_ID]) cg.add(buffer.set_buffer_size(config[CONF_BUFFER_SIZE])) if config[CONF_TYPE] == ESP32_CAMERA_ENCODER: - add_idf_component(name="espressif/esp32-camera", ref="2.1.1") + add_idf_component(name="espressif/esp32-camera", ref="2.1.6") cg.add_define("USE_ESP32_CAMERA_JPEG_ENCODER") var = cg.new_Pvariable( config[CONF_ID], diff --git a/esphome/components/camera_encoder/encoder_buffer_impl.cpp b/esphome/components/camera_encoder/encoder_buffer_impl.cpp index db84026496..f12c66f203 100644 --- a/esphome/components/camera_encoder/encoder_buffer_impl.cpp +++ b/esphome/components/camera_encoder/encoder_buffer_impl.cpp @@ -4,7 +4,8 @@ namespace esphome::camera_encoder { bool EncoderBufferImpl::set_buffer_size(size_t size) { if (size > this->capacity_) { - uint8_t *p = this->allocator_.reallocate(this->data_, size); + RAMAllocator allocator; + uint8_t *p = allocator.reallocate(this->data_, size); if (p == nullptr) return false; @@ -16,8 +17,10 @@ bool EncoderBufferImpl::set_buffer_size(size_t size) { } EncoderBufferImpl::~EncoderBufferImpl() { - if (this->data_ != nullptr) - this->allocator_.deallocate(this->data_, this->capacity_); + if (this->data_ != nullptr) { + RAMAllocator allocator; + allocator.deallocate(this->data_, this->capacity_); + } } } // namespace esphome::camera_encoder diff --git a/esphome/components/camera_encoder/encoder_buffer_impl.h b/esphome/components/camera_encoder/encoder_buffer_impl.h index 13eccb7d56..d394daff14 100644 --- a/esphome/components/camera_encoder/encoder_buffer_impl.h +++ b/esphome/components/camera_encoder/encoder_buffer_impl.h @@ -16,7 +16,6 @@ class EncoderBufferImpl : public camera::EncoderBuffer { ~EncoderBufferImpl() override; protected: - RAMAllocator allocator_; size_t capacity_{}; size_t size_{}; uint8_t *data_{}; diff --git a/esphome/components/canbus/__init__.py b/esphome/components/canbus/__init__.py index 7b51c2c45c..7d3bf78f49 100644 --- a/esphome/components/canbus/__init__.py +++ b/esphome/components/canbus/__init__.py @@ -155,12 +155,13 @@ async def register_canbus(var, config): validate_id, key=CONF_DATA, ), + synchronous=True, ) async def canbus_action_to_code(config, action_id, template_arg, args): var = cg.new_Pvariable(action_id, template_arg) await cg.register_parented(var, config[CONF_CANBUS_ID]) - if can_id := config.get(CONF_CAN_ID): + if (can_id := config.get(CONF_CAN_ID)) is not None: can_id = await cg.templatable(can_id, args, cg.uint32) cg.add(var.set_can_id(can_id)) cg.add(var.set_use_extended_id(config[CONF_USE_EXTENDED_ID])) diff --git a/esphome/components/canbus/canbus.cpp b/esphome/components/canbus/canbus.cpp index e208b0fd66..ce48bfbba5 100644 --- a/esphome/components/canbus/canbus.cpp +++ b/esphome/components/canbus/canbus.cpp @@ -1,4 +1,5 @@ #include "canbus.h" +#include #include "esphome/core/log.h" namespace esphome { @@ -82,7 +83,7 @@ void Canbus::loop() { std::vector data; // show data received - for (int i = 0; i < can_message.can_data_length_code; i++) { + for (int i = 0; i < std::min(can_message.can_data_length_code, CAN_MAX_DATA_LENGTH); i++) { ESP_LOGV(TAG, " can_message.data[%d]=%02x", i, can_message.data[i]); data.push_back(can_message.data[i]); } diff --git a/esphome/components/canbus/canbus.h b/esphome/components/canbus/canbus.h index f7b84111bd..420125e1d3 100644 --- a/esphome/components/canbus/canbus.h +++ b/esphome/components/canbus/canbus.h @@ -91,10 +91,7 @@ class Canbus : public Component { * - rtr If this is a remote transmission request * - data The message data */ - void add_callback( - std::function &data)> callback) { - this->callback_manager_.add(std::move(callback)); - } + template void add_callback(F &&callback) { this->callback_manager_.add(std::forward(callback)); } protected: template friend class CanbusSendAction; diff --git a/esphome/components/cap1188/cap1188.cpp b/esphome/components/cap1188/cap1188.cpp index 9e8c87d147..64bdc620cd 100644 --- a/esphome/components/cap1188/cap1188.cpp +++ b/esphome/components/cap1188/cap1188.cpp @@ -92,7 +92,7 @@ void CAP1188Component::loop() { this->read_register(CAP1188_MAIN, &data, 1); data = data & ~CAP1188_MAIN_INT; - this->write_register(CAP1188_MAIN, &data, 2); + this->write_register(CAP1188_MAIN, &data, 1); } for (auto *channel : this->channels_) { diff --git a/esphome/components/captive_portal/__init__.py b/esphome/components/captive_portal/__init__.py index 049618219e..cd877fc879 100644 --- a/esphome/components/captive_portal/__init__.py +++ b/esphome/components/captive_portal/__init__.py @@ -13,6 +13,7 @@ from esphome.const import ( PLATFORM_ESP32, PLATFORM_ESP8266, PLATFORM_LN882X, + PLATFORM_RP2040, PLATFORM_RTL87XX, PlatformFramework, ) @@ -53,6 +54,7 @@ CONFIG_SCHEMA = cv.All( PLATFORM_ESP8266, PLATFORM_BK72XX, PLATFORM_LN882X, + PLATFORM_RP2040, PLATFORM_RTL87XX, ] ), @@ -76,13 +78,15 @@ def _final_validate(config: ConfigType) -> ConfigType: # Register socket needs for DNS server and additional HTTP connections # - 1 UDP socket for DNS server - # - 3 additional TCP sockets for captive portal detection probes + configuration requests + # - 3 TCP sockets for captive portal detection probes + configuration requests # OS captive portal detection makes multiple probe requests that stay in TIME_WAIT. # Need headroom for actual user configuration requests. # LRU purging will reclaim idle sockets to prevent exhaustion from repeated attempts. + # The listening socket is registered by web_server_base (shared HTTP server). from esphome.components import socket - socket.consume_sockets(4, "captive_portal")(config) + socket.consume_sockets(3, "captive_portal")(config) + socket.consume_sockets(1, "captive_portal", socket.SocketType.UDP)(config) return config @@ -101,11 +105,8 @@ async def to_code(config): if config[CONF_COMPRESSION] == "gzip": cg.add_define("USE_CAPTIVE_PORTAL_GZIP") - if CORE.using_arduino: - if CORE.is_esp8266: - cg.add_library("DNSServer", None) - if CORE.is_libretiny: - cg.add_library("DNSServer", None) + if CORE.using_arduino and (CORE.is_esp8266 or CORE.is_libretiny or CORE.is_rp2040): + cg.add_library("DNSServer", None) # Only compile the ESP-IDF DNS server when using ESP-IDF framework diff --git a/esphome/components/captive_portal/captive_index.h b/esphome/components/captive_portal/captive_index.h index 645ebb7a2f..a81edc1900 100644 --- a/esphome/components/captive_portal/captive_index.h +++ b/esphome/components/captive_portal/captive_index.h @@ -6,7 +6,7 @@ namespace esphome::captive_portal { #ifdef USE_CAPTIVE_PORTAL_GZIP -const uint8_t INDEX_GZ[] PROGMEM = { +constexpr uint8_t INDEX_GZ[] PROGMEM = { 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0x95, 0x16, 0x6b, 0x8f, 0xdb, 0x36, 0xf2, 0x7b, 0x7e, 0x05, 0x8f, 0x49, 0xbb, 0x52, 0xb3, 0x7a, 0x7a, 0xed, 0x6c, 0x24, 0x51, 0x45, 0x9a, 0xbb, 0xa2, 0x05, 0x9a, 0x36, 0xc0, 0x6e, 0x73, 0x1f, 0x82, 0x00, 0x4b, 0x53, 0x23, 0x8b, 0x31, 0x45, 0xea, 0x48, 0xca, 0x8f, 0x18, 0xbe, 0xdf, @@ -86,7 +86,7 @@ const uint8_t INDEX_GZ[] PROGMEM = { 0xfc, 0xda, 0xd1, 0xf8, 0xe9, 0xa3, 0xe1, 0xa6, 0xfb, 0x1f, 0x53, 0x58, 0x46, 0xb2, 0xf9, 0x0a, 0x00, 0x00}; #else // Brotli (default, smaller) -const uint8_t INDEX_BR[] PROGMEM = { +constexpr uint8_t INDEX_BR[] PROGMEM = { 0x1b, 0xf8, 0x0a, 0x00, 0x64, 0x5a, 0xd3, 0xfa, 0xe7, 0xf3, 0x62, 0xd8, 0x06, 0x1b, 0xe9, 0x6a, 0x8a, 0x81, 0x2b, 0xb5, 0x49, 0x14, 0x37, 0xdc, 0x9e, 0x1a, 0xcb, 0x56, 0x87, 0xfb, 0xff, 0xf7, 0x73, 0x75, 0x12, 0x0a, 0xd6, 0x48, 0x84, 0xc6, 0x21, 0xa4, 0x6d, 0xb5, 0x71, 0xef, 0x13, 0xbe, 0x4e, 0x54, 0xf1, 0x64, 0x8f, 0x3f, 0xcc, 0x9a, 0x78, diff --git a/esphome/components/captive_portal/captive_portal.cpp b/esphome/components/captive_portal/captive_portal.cpp index 8d88a10b27..183f16c5f8 100644 --- a/esphome/components/captive_portal/captive_portal.cpp +++ b/esphome/components/captive_portal/captive_portal.cpp @@ -47,8 +47,8 @@ void CaptivePortal::handle_config(AsyncWebServerRequest *request) { request->send(stream); } void CaptivePortal::handle_wifisave(AsyncWebServerRequest *request) { - std::string ssid = request->arg("ssid").c_str(); // NOLINT(readability-redundant-string-cstr) - std::string psk = request->arg("psk").c_str(); // NOLINT(readability-redundant-string-cstr) + const auto &ssid = request->arg("ssid"); + const auto &psk = request->arg("psk"); ESP_LOGI(TAG, "Requested WiFi Settings Change:\n" " SSID='%s'\n" @@ -56,12 +56,12 @@ void CaptivePortal::handle_wifisave(AsyncWebServerRequest *request) { ssid.c_str(), psk.c_str()); #ifdef USE_ESP8266 // ESP8266 is single-threaded, call directly - wifi::global_wifi_component->save_wifi_sta(ssid, psk); + wifi::global_wifi_component->save_wifi_sta(ssid.c_str(), psk.c_str()); #else // Defer save to main loop thread to avoid NVS operations from HTTP thread - this->defer([ssid, psk]() { wifi::global_wifi_component->save_wifi_sta(ssid, psk); }); + this->defer([ssid, psk]() { wifi::global_wifi_component->save_wifi_sta(ssid.c_str(), psk.c_str()); }); #endif - request->redirect(ESPHOME_F("/?save")); + request->send(200, ESPHOME_F("text/plain"), ESPHOME_F("Saved. Connecting...")); } void CaptivePortal::setup() { @@ -71,7 +71,7 @@ void CaptivePortal::setup() { void CaptivePortal::start() { this->base_->init(); if (!this->initialized_) { - this->base_->add_handler(this); + this->base_->add_handler_without_auth(this); } network::IPAddress ip = wifi::global_wifi_component->wifi_soft_ap_ip(); diff --git a/esphome/components/captive_portal/dns_server_esp32_idf.cpp b/esphome/components/captive_portal/dns_server_esp32_idf.cpp index 5743cbd671..56ad9f7176 100644 --- a/esphome/components/captive_portal/dns_server_esp32_idf.cpp +++ b/esphome/components/captive_portal/dns_server_esp32_idf.cpp @@ -14,6 +14,7 @@ static const char *const TAG = "captive_portal.dns"; // DNS constants static constexpr uint16_t DNS_PORT = 53; static constexpr uint16_t DNS_QR_FLAG = 1 << 15; +static constexpr uint16_t DNS_AA_FLAG = 1 << 10; static constexpr uint16_t DNS_OPCODE_MASK = 0x7800; static constexpr uint16_t DNS_QTYPE_A = 0x0001; static constexpr uint16_t DNS_QCLASS_IN = 0x0001; @@ -53,7 +54,7 @@ void DNSServer::start(const network::IPAddress &ip) { #endif // Create loop-monitored UDP socket - this->socket_ = socket::socket_ip_loop_monitored(SOCK_DGRAM, IPPROTO_UDP); + this->socket_ = socket::socket_ip_loop_monitored(SOCK_DGRAM, IPPROTO_UDP).release(); if (this->socket_ == nullptr) { ESP_LOGE(TAG, "Socket create failed"); return; @@ -70,17 +71,14 @@ void DNSServer::start(const network::IPAddress &ip) { int err = this->socket_->bind((struct sockaddr *) &server_addr, addr_len); if (err != 0) { ESP_LOGE(TAG, "Bind failed: %d", errno); - this->socket_ = nullptr; + this->destroy_socket_(); return; } ESP_LOGV(TAG, "Bound to port %d", DNS_PORT); } void DNSServer::stop() { - if (this->socket_ != nullptr) { - this->socket_->close(); - this->socket_ = nullptr; - } + this->destroy_socket_(); ESP_LOGV(TAG, "Stopped"); } @@ -102,8 +100,9 @@ void DNSServer::process_next_request() { &client_addr_len); if (len < 0) { - if (errno != EAGAIN && errno != EWOULDBLOCK && errno != EINTR) { - ESP_LOGE(TAG, "recvfrom failed: %d", errno); + const int err = errno; + if (err != EAGAIN && err != EWOULDBLOCK && err != EINTR) { + ESP_LOGE(TAG, "recvfrom failed: %d", err); } return; } @@ -165,8 +164,8 @@ void DNSServer::process_next_request() { } // Build DNS response by modifying the request in-place - header->flags = htons(DNS_QR_FLAG | 0x8000); // Response + Authoritative - header->an_count = htons(1); // One answer + header->flags = htons(DNS_QR_FLAG | DNS_AA_FLAG); // Response + Authoritative + header->an_count = htons(1); // One answer // Add answer section after the question size_t question_len = (ptr + sizeof(DNSQuestion)) - this->buffer_ - sizeof(DNSHeader); diff --git a/esphome/components/captive_portal/dns_server_esp32_idf.h b/esphome/components/captive_portal/dns_server_esp32_idf.h index 3e0ac07373..b30856c204 100644 --- a/esphome/components/captive_portal/dns_server_esp32_idf.h +++ b/esphome/components/captive_portal/dns_server_esp32_idf.h @@ -1,7 +1,6 @@ #pragma once #ifdef USE_ESP32 -#include #include "esphome/core/helpers.h" #include "esphome/components/network/ip_address.h" #include "esphome/components/socket/socket.h" @@ -15,9 +14,15 @@ class DNSServer { void process_next_request(); protected: + // No explicit close() needed — listen sockets have no active connections on + // failure/shutdown. Destructor handles fd cleanup (close or abort per platform). + inline void destroy_socket_() { + delete this->socket_; + this->socket_ = nullptr; + } static constexpr size_t DNS_BUFFER_SIZE = 192; - std::unique_ptr socket_{nullptr}; + socket::ListenSocket *socket_{nullptr}; network::IPAddress server_ip_; uint8_t buffer_[DNS_BUFFER_SIZE]; }; diff --git a/esphome/components/cc1101/__init__.py b/esphome/components/cc1101/__init__.py index fbdd7010b4..2709290862 100644 --- a/esphome/components/cc1101/__init__.py +++ b/esphome/components/cc1101/__init__.py @@ -9,6 +9,8 @@ from esphome.const import ( CONF_DATA, CONF_FREQUENCY, CONF_ID, + CONF_OUTPUT_POWER, + CONF_VALUE, CONF_WAIT_TIME, ) from esphome.core import ID @@ -21,7 +23,6 @@ ns = cg.esphome_ns.namespace("cc1101") CC1101Component = ns.class_("CC1101Component", cg.Component, spi.SPIDevice) # Config keys -CONF_OUTPUT_POWER = "output_power" CONF_RX_ATTENUATION = "rx_attenuation" CONF_DC_BLOCKING_FILTER = "dc_blocking_filter" CONF_IF_FREQUENCY = "if_frequency" @@ -286,10 +287,18 @@ CC1101_ACTION_SCHEMA = cv.Schema( ) -@automation.register_action("cc1101.begin_tx", BeginTxAction, CC1101_ACTION_SCHEMA) -@automation.register_action("cc1101.begin_rx", BeginRxAction, CC1101_ACTION_SCHEMA) -@automation.register_action("cc1101.reset", ResetAction, CC1101_ACTION_SCHEMA) -@automation.register_action("cc1101.set_idle", SetIdleAction, CC1101_ACTION_SCHEMA) +@automation.register_action( + "cc1101.begin_tx", BeginTxAction, CC1101_ACTION_SCHEMA, synchronous=True +) +@automation.register_action( + "cc1101.begin_rx", BeginRxAction, CC1101_ACTION_SCHEMA, synchronous=True +) +@automation.register_action( + "cc1101.reset", ResetAction, CC1101_ACTION_SCHEMA, synchronous=True +) +@automation.register_action( + "cc1101.set_idle", SetIdleAction, CC1101_ACTION_SCHEMA, synchronous=True +) async def cc1101_action_to_code(config, action_id, template_arg, args): var = cg.new_Pvariable(action_id, template_arg) await cg.register_parented(var, config[CONF_ID]) @@ -316,7 +325,10 @@ SEND_PACKET_ACTION_SCHEMA = cv.maybe_simple_value( @automation.register_action( - "cc1101.send_packet", SendPacketAction, SEND_PACKET_ACTION_SCHEMA + "cc1101.send_packet", + SendPacketAction, + SEND_PACKET_ACTION_SCHEMA, + synchronous=True, ) async def send_packet_action_to_code(config, action_id, template_arg, args): var = cg.new_Pvariable(action_id, template_arg) @@ -333,3 +345,94 @@ async def send_packet_action_to_code(config, action_id, template_arg, args): arr = cg.static_const_array(arr_id, cg.ArrayInitializer(*data)) cg.add(var.set_data_static(arr, len(data))) return var + + +# Setter action definitions: (setter_name, validator, template_type, enum_map) +_SETTER_ACTIONS = [ + ( + "set_frequency", + cv.All(cv.frequency, cv.float_range(min=300.0e6, max=928.0e6)), + float, + None, + ), + ("set_output_power", cv.float_range(min=-30.0, max=11.0), float, None), + ("set_modulation_type", cv.enum(MODULATION, upper=False), Modulation, MODULATION), + ("set_symbol_rate", cv.float_range(min=600, max=500000), float, None), + ( + "set_rx_attenuation", + cv.enum(RX_ATTENUATION, upper=False), + RxAttenuation, + RX_ATTENUATION, + ), + ("set_dc_blocking_filter", cv.boolean, bool, None), + ("set_manchester", cv.boolean, bool, None), + ( + "set_filter_bandwidth", + cv.All(cv.frequency, cv.float_range(min=58000, max=812000)), + float, + None, + ), + ( + "set_fsk_deviation", + cv.All(cv.frequency, cv.float_range(min=1500, max=381000)), + float, + None, + ), + ("set_msk_deviation", cv.int_range(min=1, max=8), cg.uint8, None), + ("set_channel", cv.uint8_t, cg.uint8, None), + ( + "set_channel_spacing", + cv.All(cv.frequency, cv.float_range(min=25000, max=405000)), + float, + None, + ), + ( + "set_if_frequency", + cv.All(cv.frequency, cv.float_range(min=25000, max=788000)), + float, + None, + ), +] + + +def _register_setter_actions(): + for setter_name, validator, templ_type, enum_map in _SETTER_ACTIONS: + class_name = ( + "".join(word.capitalize() for word in setter_name.split("_")) + "Action" + ) + action_cls = ns.class_( + class_name, automation.Action, cg.Parented.template(CC1101Component) + ) + schema = cv.maybe_simple_value( + { + cv.GenerateID(): cv.use_id(CC1101Component), + cv.Required(CONF_VALUE): cv.templatable(validator), + }, + key=CONF_VALUE, + ) + + async def _setter_action_to_code( + config, + action_id, + template_arg, + args, + _setter=setter_name, + _type=templ_type, + _map=enum_map, + ): + var = cg.new_Pvariable(action_id, template_arg) + await cg.register_parented(var, config[CONF_ID]) + data = config[CONF_VALUE] + if cg.is_template(data): + templ_ = await cg.templatable(data, args, _type) + cg.add(getattr(var, _setter)(templ_)) + else: + cg.add(getattr(var, _setter)(_map[data] if _map else data)) + return var + + automation.register_action( + f"cc1101.{setter_name}", action_cls, schema, synchronous=True + )(_setter_action_to_code) + + +_register_setter_actions() diff --git a/esphome/components/cc1101/cc1101.cpp b/esphome/components/cc1101/cc1101.cpp index 46cd89e0e8..51aa88b8f7 100644 --- a/esphome/components/cc1101/cc1101.cpp +++ b/esphome/components/cc1101/cc1101.cpp @@ -156,7 +156,7 @@ void CC1101Component::call_listeners_(const std::vector &packet, float for (auto &listener : this->listeners_) { listener->on_packet(packet, freq_offset, rssi, lqi); } - this->packet_trigger_->trigger(packet, freq_offset, rssi, lqi); + this->packet_trigger_.trigger(packet, freq_offset, rssi, lqi); } void CC1101Component::loop() { @@ -242,6 +242,9 @@ void CC1101Component::begin_tx() { if (this->gdo0_pin_ != nullptr) { this->gdo0_pin_->pin_mode(gpio::FLAG_OUTPUT); } + // Transition through IDLE to bypass CCA (Clear Channel Assessment) which can + // block TX entry when strobing from RX, and to ensure FS_AUTOCAL calibration + this->enter_idle_(); if (!this->enter_tx_()) { ESP_LOGW(TAG, "Failed to enter TX state!"); } @@ -252,6 +255,8 @@ void CC1101Component::begin_rx() { if (this->gdo0_pin_ != nullptr) { this->gdo0_pin_->pin_mode(gpio::FLAG_INPUT); } + // Transition through IDLE to ensure FS_AUTOCAL calibration occurs + this->enter_idle_(); if (!this->enter_rx_()) { ESP_LOGW(TAG, "Failed to enter RX state!"); } diff --git a/esphome/components/cc1101/cc1101.h b/esphome/components/cc1101/cc1101.h index 6e3f01af90..2efd9e082d 100644 --- a/esphome/components/cc1101/cc1101.h +++ b/esphome/components/cc1101/cc1101.h @@ -79,7 +79,7 @@ class CC1101Component : public Component, // Packet mode operations CC1101Error transmit_packet(const std::vector &packet); void register_listener(CC1101Listener *listener) { this->listeners_.push_back(listener); } - Trigger, float, float, uint8_t> *get_packet_trigger() const { return this->packet_trigger_; } + Trigger, float, float, uint8_t> *get_packet_trigger() { return &this->packet_trigger_; } protected: uint16_t chip_id_{0}; @@ -96,8 +96,7 @@ class CC1101Component : public Component, // Packet handling void call_listeners_(const std::vector &packet, float freq_offset, float rssi, uint8_t lqi); - Trigger, float, float, uint8_t> *packet_trigger_{ - new Trigger, float, float, uint8_t>()}; + Trigger, float, float, uint8_t> packet_trigger_; std::vector packet_; std::vector listeners_; @@ -162,4 +161,82 @@ template class SendPacketAction : public Action, public P size_t data_static_len_{0}; }; +template class SetSymbolRateAction : public Action, public Parented { + public: + TEMPLATABLE_VALUE(float, symbol_rate) + void play(const Ts &...x) override { this->parent_->set_symbol_rate(this->symbol_rate_.value(x...)); } +}; + +template class SetFrequencyAction : public Action, public Parented { + public: + TEMPLATABLE_VALUE(float, frequency) + void play(const Ts &...x) override { this->parent_->set_frequency(this->frequency_.value(x...)); } +}; + +template class SetOutputPowerAction : public Action, public Parented { + public: + TEMPLATABLE_VALUE(float, output_power) + void play(const Ts &...x) override { this->parent_->set_output_power(this->output_power_.value(x...)); } +}; + +template class SetModulationTypeAction : public Action, public Parented { + public: + TEMPLATABLE_VALUE(Modulation, modulation_type) + void play(const Ts &...x) override { this->parent_->set_modulation_type(this->modulation_type_.value(x...)); } +}; + +template class SetRxAttenuationAction : public Action, public Parented { + public: + TEMPLATABLE_VALUE(RxAttenuation, rx_attenuation) + void play(const Ts &...x) override { this->parent_->set_rx_attenuation(this->rx_attenuation_.value(x...)); } +}; + +template class SetDcBlockingFilterAction : public Action, public Parented { + public: + TEMPLATABLE_VALUE(bool, dc_blocking_filter) + void play(const Ts &...x) override { this->parent_->set_dc_blocking_filter(this->dc_blocking_filter_.value(x...)); } +}; + +template class SetManchesterAction : public Action, public Parented { + public: + TEMPLATABLE_VALUE(bool, manchester) + void play(const Ts &...x) override { this->parent_->set_manchester(this->manchester_.value(x...)); } +}; + +template class SetFilterBandwidthAction : public Action, public Parented { + public: + TEMPLATABLE_VALUE(float, filter_bandwidth) + void play(const Ts &...x) override { this->parent_->set_filter_bandwidth(this->filter_bandwidth_.value(x...)); } +}; + +template class SetFskDeviationAction : public Action, public Parented { + public: + TEMPLATABLE_VALUE(float, fsk_deviation) + void play(const Ts &...x) override { this->parent_->set_fsk_deviation(this->fsk_deviation_.value(x...)); } +}; + +template class SetMskDeviationAction : public Action, public Parented { + public: + TEMPLATABLE_VALUE(uint8_t, msk_deviation) + void play(const Ts &...x) override { this->parent_->set_msk_deviation(this->msk_deviation_.value(x...)); } +}; + +template class SetChannelAction : public Action, public Parented { + public: + TEMPLATABLE_VALUE(uint8_t, channel) + void play(const Ts &...x) override { this->parent_->set_channel(this->channel_.value(x...)); } +}; + +template class SetChannelSpacingAction : public Action, public Parented { + public: + TEMPLATABLE_VALUE(float, channel_spacing) + void play(const Ts &...x) override { this->parent_->set_channel_spacing(this->channel_spacing_.value(x...)); } +}; + +template class SetIfFrequencyAction : public Action, public Parented { + public: + TEMPLATABLE_VALUE(float, if_frequency) + void play(const Ts &...x) override { this->parent_->set_if_frequency(this->if_frequency_.value(x...)); } +}; + } // namespace esphome::cc1101 diff --git a/esphome/components/cd74hc4067/cd74hc4067.cpp b/esphome/components/cd74hc4067/cd74hc4067.cpp index 4293d7af07..302c83d7d3 100644 --- a/esphome/components/cd74hc4067/cd74hc4067.cpp +++ b/esphome/components/cd74hc4067/cd74hc4067.cpp @@ -7,8 +7,6 @@ namespace cd74hc4067 { static const char *const TAG = "cd74hc4067"; -float CD74HC4067Component::get_setup_priority() const { return setup_priority::DATA; } - void CD74HC4067Component::setup() { this->pin_s0_->setup(); this->pin_s1_->setup(); diff --git a/esphome/components/cd74hc4067/cd74hc4067.h b/esphome/components/cd74hc4067/cd74hc4067.h index 6193513575..76ebc1ebbe 100644 --- a/esphome/components/cd74hc4067/cd74hc4067.h +++ b/esphome/components/cd74hc4067/cd74hc4067.h @@ -13,7 +13,6 @@ class CD74HC4067Component : public Component { /// Set up the internal sensor array. void setup() override; void dump_config() override; - float get_setup_priority() const override; /// setting pin active by setting the right combination of the four multiplexer input pins void activate_pin(uint8_t pin); diff --git a/esphome/components/ch422g/ch422g.cpp b/esphome/components/ch422g/ch422g.cpp index eef95b9ba2..fc856cd563 100644 --- a/esphome/components/ch422g/ch422g.cpp +++ b/esphome/components/ch422g/ch422g.cpp @@ -124,10 +124,6 @@ bool CH422GComponent::write_outputs_() { float CH422GComponent::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 CH422GComponent::get_loop_priority() const { return 9.0f; } // Just after WIFI - void CH422GGPIOPin::pin_mode(gpio::Flags flags) { this->parent_->pin_mode(this->pin_, flags); } bool CH422GGPIOPin::digital_read() { return this->parent_->digital_read(this->pin_) ^ this->inverted_; } diff --git a/esphome/components/ch422g/ch422g.h b/esphome/components/ch422g/ch422g.h index 8ed63db90a..6e6bdad64a 100644 --- a/esphome/components/ch422g/ch422g.h +++ b/esphome/components/ch422g/ch422g.h @@ -23,7 +23,6 @@ class CH422GComponent : public Component, public i2c::I2CDevice { 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: diff --git a/esphome/components/ch423/__init__.py b/esphome/components/ch423/__init__.py new file mode 100644 index 0000000000..e3990ee631 --- /dev/null +++ b/esphome/components/ch423/__init__.py @@ -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 diff --git a/esphome/components/ch423/ch423.cpp b/esphome/components/ch423/ch423.cpp new file mode 100644 index 0000000000..8424d130b4 --- /dev/null +++ b/esphome/components/ch423/ch423.cpp @@ -0,0 +1,144 @@ +#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(this->output_bits_)); + // Write OC7-OC0 + success &= this->write_reg_(CH423_REG_OCL, static_cast(this->output_bits_ >> 8)); + // Write OC15-OC8 + success &= this->write_reg_(CH423_REG_OCH, static_cast(this->output_bits_ >> 16)); + return success; +} + +float CH423Component::get_setup_priority() const { return setup_priority::IO; } + +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 diff --git a/esphome/components/ch423/ch423.h b/esphome/components/ch423/ch423.h new file mode 100644 index 0000000000..d384971a72 --- /dev/null +++ b/esphome/components/ch423/ch423.h @@ -0,0 +1,66 @@ +#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; + 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 diff --git a/esphome/components/climate/__init__.py b/esphome/components/climate/__init__.py index 2150a30c3e..13dd7aa007 100644 --- a/esphome/components/climate/__init__.py +++ b/esphome/components/climate/__init__.py @@ -8,6 +8,7 @@ from esphome.const import ( CONF_AWAY_COMMAND_TOPIC, CONF_AWAY_STATE_TOPIC, CONF_CURRENT_HUMIDITY_STATE_TOPIC, + CONF_CURRENT_TEMPERATURE, CONF_CURRENT_TEMPERATURE_STATE_TOPIC, CONF_CUSTOM_FAN_MODE, CONF_CUSTOM_PRESET, @@ -112,7 +113,6 @@ CLIMATE_SWING_MODES = { validate_climate_swing_mode = cv.enum(CLIMATE_SWING_MODES, upper=True) -CONF_CURRENT_TEMPERATURE = "current_temperature" CONF_MIN_HUMIDITY = "min_humidity" CONF_MAX_HUMIDITY = "max_humidity" CONF_TARGET_HUMIDITY = "target_humidity" @@ -268,9 +268,8 @@ def climate_schema( return _CLIMATE_SCHEMA.extend(schema) +@setup_entity("climate") async def setup_climate_core_(var, config): - await setup_entity(var, config, "climate") - visual = config[CONF_VISUAL] if (min_temp := visual.get(CONF_MIN_TEMPERATURE)) is not None: cg.add_define("USE_CLIMATE_VISUAL_OVERRIDES") @@ -401,7 +400,7 @@ async def setup_climate_core_(var, config): ) ) is not None: cg.add( - mqtt_.set_custom_target_temperature_state_topic( + mqtt_.set_custom_target_temperature_low_state_topic( target_temperature_low_state_topic ) ) @@ -477,7 +476,10 @@ CLIMATE_CONTROL_ACTION_SCHEMA = cv.Schema( @automation.register_action( - "climate.control", ControlAction, CLIMATE_CONTROL_ACTION_SCHEMA + "climate.control", + ControlAction, + CLIMATE_CONTROL_ACTION_SCHEMA, + synchronous=True, ) async def climate_control_to_code(config, action_id, template_arg, args): paren = await cg.get_variable(config[CONF_ID]) diff --git a/esphome/components/climate/climate.cpp b/esphome/components/climate/climate.cpp index ba6de4ff61..32cac0961c 100644 --- a/esphome/components/climate/climate.cpp +++ b/esphome/components/climate/climate.cpp @@ -46,45 +46,45 @@ constexpr StringToUint8 CLIMATE_SWING_MODES_BY_STR[] = { void ClimateCall::perform() { this->parent_->control_callback_.call(*this); - ESP_LOGD(TAG, "'%s' - Setting", this->parent_->get_name().c_str()); + ESP_LOGV(TAG, "'%s' - Setting", this->parent_->get_name().c_str()); this->validate_(); if (this->mode_.has_value()) { const LogString *mode_s = climate_mode_to_string(*this->mode_); - ESP_LOGD(TAG, " Mode: %s", LOG_STR_ARG(mode_s)); + ESP_LOGV(TAG, " Mode: %s", LOG_STR_ARG(mode_s)); } if (this->custom_fan_mode_ != nullptr) { this->fan_mode_.reset(); - ESP_LOGD(TAG, " Custom Fan: %s", this->custom_fan_mode_); + ESP_LOGV(TAG, " Custom Fan: %s", this->custom_fan_mode_); } if (this->fan_mode_.has_value()) { this->custom_fan_mode_ = nullptr; const LogString *fan_mode_s = climate_fan_mode_to_string(*this->fan_mode_); - ESP_LOGD(TAG, " Fan: %s", LOG_STR_ARG(fan_mode_s)); + ESP_LOGV(TAG, " Fan: %s", LOG_STR_ARG(fan_mode_s)); } if (this->custom_preset_ != nullptr) { this->preset_.reset(); - ESP_LOGD(TAG, " Custom Preset: %s", this->custom_preset_); + ESP_LOGV(TAG, " Custom Preset: %s", this->custom_preset_); } if (this->preset_.has_value()) { this->custom_preset_ = nullptr; const LogString *preset_s = climate_preset_to_string(*this->preset_); - ESP_LOGD(TAG, " Preset: %s", LOG_STR_ARG(preset_s)); + ESP_LOGV(TAG, " Preset: %s", LOG_STR_ARG(preset_s)); } if (this->swing_mode_.has_value()) { const LogString *swing_mode_s = climate_swing_mode_to_string(*this->swing_mode_); - ESP_LOGD(TAG, " Swing: %s", LOG_STR_ARG(swing_mode_s)); + ESP_LOGV(TAG, " Swing: %s", LOG_STR_ARG(swing_mode_s)); } if (this->target_temperature_.has_value()) { - ESP_LOGD(TAG, " Target Temperature: %.2f", *this->target_temperature_); + ESP_LOGV(TAG, " Target Temperature: %.2f", *this->target_temperature_); } if (this->target_temperature_low_.has_value()) { - ESP_LOGD(TAG, " Target Temperature Low: %.2f", *this->target_temperature_low_); + ESP_LOGV(TAG, " Target Temperature Low: %.2f", *this->target_temperature_low_); } if (this->target_temperature_high_.has_value()) { - ESP_LOGD(TAG, " Target Temperature High: %.2f", *this->target_temperature_high_); + ESP_LOGV(TAG, " Target Temperature High: %.2f", *this->target_temperature_high_); } if (this->target_humidity_.has_value()) { - ESP_LOGD(TAG, " Target Humidity: %.0f", *this->target_humidity_); + ESP_LOGV(TAG, " Target Humidity: %.0f", *this->target_humidity_); } this->parent_->control(*this); } @@ -173,14 +173,17 @@ ClimateCall &ClimateCall::set_mode(ClimateMode mode) { return *this; } -ClimateCall &ClimateCall::set_mode(const std::string &mode) { +ClimateCall &ClimateCall::set_mode(const std::string &mode) { return this->set_mode(mode.c_str(), mode.size()); } + +ClimateCall &ClimateCall::set_mode(const char *mode, size_t len) { + StringRef mode_ref(mode, len); for (const auto &mode_entry : CLIMATE_MODES_BY_STR) { - if (str_equals_case_insensitive(mode, mode_entry.str)) { + if (str_equals_case_insensitive(mode_ref, mode_entry.str)) { this->set_mode(static_cast(mode_entry.value)); return *this; } } - ESP_LOGW(TAG, "'%s' - Unrecognized mode %s", this->parent_->get_name().c_str(), mode.c_str()); + ESP_LOGW(TAG, "'%s' - Unrecognized mode %.*s", this->parent_->get_name().c_str(), (int) len, mode); return *this; } @@ -266,13 +269,18 @@ ClimateCall &ClimateCall::set_swing_mode(ClimateSwingMode swing_mode) { } ClimateCall &ClimateCall::set_swing_mode(const std::string &swing_mode) { + return this->set_swing_mode(swing_mode.c_str(), swing_mode.size()); +} + +ClimateCall &ClimateCall::set_swing_mode(const char *swing_mode, size_t len) { + StringRef mode_ref(swing_mode, len); for (const auto &mode_entry : CLIMATE_SWING_MODES_BY_STR) { - if (str_equals_case_insensitive(swing_mode, mode_entry.str)) { + if (str_equals_case_insensitive(mode_ref, mode_entry.str)) { this->set_swing_mode(static_cast(mode_entry.value)); return *this; } } - ESP_LOGW(TAG, "'%s' - Unrecognized swing mode %s", this->parent_->get_name().c_str(), swing_mode.c_str()); + ESP_LOGW(TAG, "'%s' - Unrecognized swing mode %.*s", this->parent_->get_name().c_str(), (int) len, swing_mode); return *this; } @@ -348,14 +356,6 @@ ClimateCall &ClimateCall::set_swing_mode(optional swing_mode) return *this; } -void Climate::add_on_state_callback(std::function &&callback) { - this->state_callback_.add(std::move(callback)); -} - -void Climate::add_on_control_callback(std::function &&callback) { - this->control_callback_.add(std::move(callback)); -} - // Random 32bit value; If this changes existing restore preferences are invalidated static const uint32_t RESTORE_STATE_VERSION = 0x848EA6ADUL; @@ -367,7 +367,7 @@ optional Climate::restore_state_() { return recovered; } -void Climate::save_state_() { +void Climate::save_state_(const ClimateTraits &traits) { #if (defined(USE_ESP32) || (defined(USE_ESP8266) && USE_ARDUINO_VERSION_CODE >= VERSION_CODE(3, 0, 0))) && \ !defined(CLANG_TIDY) #pragma GCC diagnostic ignored "-Wclass-memaccess" @@ -382,7 +382,6 @@ void Climate::save_state_() { #endif state.mode = this->mode; - auto traits = this->get_traits(); if (traits.has_feature_flags(CLIMATE_SUPPORTS_TWO_POINT_TARGET_TEMPERATURE | CLIMATE_REQUIRES_TWO_POINT_TARGET_TEMPERATURE)) { state.target_temperature_low = this->target_temperature_low; @@ -435,43 +434,43 @@ void Climate::save_state_() { } void Climate::publish_state() { - ESP_LOGD(TAG, "'%s' >>", this->name_.c_str()); + ESP_LOGV(TAG, "'%s' >>", this->name_.c_str()); auto traits = this->get_traits(); - ESP_LOGD(TAG, " Mode: %s", LOG_STR_ARG(climate_mode_to_string(this->mode))); + ESP_LOGV(TAG, " Mode: %s", LOG_STR_ARG(climate_mode_to_string(this->mode))); if (traits.has_feature_flags(climate::CLIMATE_SUPPORTS_ACTION)) { - ESP_LOGD(TAG, " Action: %s", LOG_STR_ARG(climate_action_to_string(this->action))); + ESP_LOGV(TAG, " Action: %s", LOG_STR_ARG(climate_action_to_string(this->action))); } if (traits.get_supports_fan_modes() && this->fan_mode.has_value()) { - ESP_LOGD(TAG, " Fan Mode: %s", LOG_STR_ARG(climate_fan_mode_to_string(this->fan_mode.value()))); + ESP_LOGV(TAG, " Fan Mode: %s", LOG_STR_ARG(climate_fan_mode_to_string(this->fan_mode.value()))); } if (!traits.get_supported_custom_fan_modes().empty() && this->has_custom_fan_mode()) { - ESP_LOGD(TAG, " Custom Fan Mode: %s", this->custom_fan_mode_); + ESP_LOGV(TAG, " Custom Fan Mode: %s", this->custom_fan_mode_); } if (traits.get_supports_presets() && this->preset.has_value()) { - ESP_LOGD(TAG, " Preset: %s", LOG_STR_ARG(climate_preset_to_string(this->preset.value()))); + ESP_LOGV(TAG, " Preset: %s", LOG_STR_ARG(climate_preset_to_string(this->preset.value()))); } if (!traits.get_supported_custom_presets().empty() && this->has_custom_preset()) { - ESP_LOGD(TAG, " Custom Preset: %s", this->custom_preset_); + ESP_LOGV(TAG, " Custom Preset: %s", this->custom_preset_); } if (traits.get_supports_swing_modes()) { - ESP_LOGD(TAG, " Swing Mode: %s", LOG_STR_ARG(climate_swing_mode_to_string(this->swing_mode))); + ESP_LOGV(TAG, " Swing Mode: %s", LOG_STR_ARG(climate_swing_mode_to_string(this->swing_mode))); } if (traits.has_feature_flags(climate::CLIMATE_SUPPORTS_CURRENT_TEMPERATURE)) { - ESP_LOGD(TAG, " Current Temperature: %.2f°C", this->current_temperature); + ESP_LOGV(TAG, " Current Temperature: %.2f°C", this->current_temperature); } if (traits.has_feature_flags(CLIMATE_SUPPORTS_TWO_POINT_TARGET_TEMPERATURE | CLIMATE_REQUIRES_TWO_POINT_TARGET_TEMPERATURE)) { - ESP_LOGD(TAG, " Target Temperature: Low: %.2f°C High: %.2f°C", this->target_temperature_low, + ESP_LOGV(TAG, " Target Temperature: Low: %.2f°C High: %.2f°C", this->target_temperature_low, this->target_temperature_high); } else { - ESP_LOGD(TAG, " Target Temperature: %.2f°C", this->target_temperature); + ESP_LOGV(TAG, " Target Temperature: %.2f°C", this->target_temperature); } if (traits.has_feature_flags(climate::CLIMATE_SUPPORTS_CURRENT_HUMIDITY)) { - ESP_LOGD(TAG, " Current Humidity: %.0f%%", this->current_humidity); + ESP_LOGV(TAG, " Current Humidity: %.0f%%", this->current_humidity); } if (traits.has_feature_flags(climate::CLIMATE_SUPPORTS_TARGET_HUMIDITY)) { - ESP_LOGD(TAG, " Target Humidity: %.0f%%", this->target_humidity); + ESP_LOGV(TAG, " Target Humidity: %.0f%%", this->target_humidity); } // Send state to frontend @@ -480,7 +479,7 @@ void Climate::publish_state() { ControllerRegistry::notify_climate_update(this); #endif // Save state - this->save_state_(); + this->save_state_(traits); } ClimateTraits Climate::get_traits() { diff --git a/esphome/components/climate/climate.h b/esphome/components/climate/climate.h index 6fac254502..0251365dd8 100644 --- a/esphome/components/climate/climate.h +++ b/esphome/components/climate/climate.h @@ -41,6 +41,8 @@ class ClimateCall { ClimateCall &set_mode(optional mode); /// Set the mode of the climate device based on a string. ClimateCall &set_mode(const std::string &mode); + /// Set the mode of the climate device based on a C string. + ClimateCall &set_mode(const char *mode, size_t len); /// Set the target temperature of the climate device. ClimateCall &set_target_temperature(float target_temperature); /// Set the target temperature of the climate device. @@ -87,6 +89,8 @@ class ClimateCall { ClimateCall &set_swing_mode(optional swing_mode); /// Set the swing mode of the climate device based on a string. ClimateCall &set_swing_mode(const std::string &swing_mode); + /// Set the swing mode of the climate device based on a C string. + ClimateCall &set_swing_mode(const char *swing_mode, size_t len); /// Set the preset of the climate device. ClimateCall &set_preset(ClimatePreset preset); /// Set the preset of the climate device. @@ -188,7 +192,9 @@ class Climate : public EntityBase { * * @param callback The callback to call. */ - void add_on_state_callback(std::function &&callback); + template void add_on_state_callback(F &&callback) { + this->state_callback_.add(std::forward(callback)); + } /** * Add a callback for the climate device configuration; each time the configuration parameters of a climate device @@ -196,7 +202,9 @@ class Climate : public EntityBase { * * @param callback The callback to call. */ - void add_on_control_callback(std::function &&callback); + template void add_on_control_callback(F &&callback) { + this->control_callback_.add(std::forward(callback)); + } /** Make a climate device control call, this is used to control the climate device, see the ClimateCall description * for more info. @@ -327,7 +335,8 @@ class Climate : public EntityBase { /** Internal method to save the state of the climate device to recover memory. This is automatically * called from publish_state() */ - void save_state_(); + void save_state_(const ClimateTraits &traits); + void save_state_() { this->save_state_(this->traits()); } void dump_traits_(const char *tag); diff --git a/esphome/components/climate/climate_mode.cpp b/esphome/components/climate/climate_mode.cpp index b153ee0424..8e443f4146 100644 --- a/esphome/components/climate/climate_mode.cpp +++ b/esphome/components/climate/climate_mode.cpp @@ -1,109 +1,46 @@ #include "climate_mode.h" +#include "esphome/core/progmem.h" namespace esphome::climate { +// Climate mode strings indexed by ClimateMode enum (0-6): OFF, HEAT_COOL, COOL, HEAT, FAN_ONLY, DRY, AUTO +PROGMEM_STRING_TABLE(ClimateModeStrings, "OFF", "HEAT_COOL", "COOL", "HEAT", "FAN_ONLY", "DRY", "AUTO", "UNKNOWN"); + const LogString *climate_mode_to_string(ClimateMode mode) { - switch (mode) { - case CLIMATE_MODE_OFF: - return LOG_STR("OFF"); - case CLIMATE_MODE_HEAT_COOL: - return LOG_STR("HEAT_COOL"); - case CLIMATE_MODE_AUTO: - return LOG_STR("AUTO"); - case CLIMATE_MODE_COOL: - return LOG_STR("COOL"); - case CLIMATE_MODE_HEAT: - return LOG_STR("HEAT"); - case CLIMATE_MODE_FAN_ONLY: - return LOG_STR("FAN_ONLY"); - case CLIMATE_MODE_DRY: - return LOG_STR("DRY"); - default: - return LOG_STR("UNKNOWN"); - } + return ClimateModeStrings::get_log_str(static_cast(mode), ClimateModeStrings::LAST_INDEX); } + +// Climate action strings indexed by ClimateAction enum (0,2-7): OFF, (gap), COOLING, HEATING, IDLE, DRYING, FAN, +// DEFROSTING +PROGMEM_STRING_TABLE(ClimateActionStrings, "OFF", "UNKNOWN", "COOLING", "HEATING", "IDLE", "DRYING", "FAN", + "DEFROSTING", "UNKNOWN"); + const LogString *climate_action_to_string(ClimateAction action) { - switch (action) { - case CLIMATE_ACTION_OFF: - return LOG_STR("OFF"); - case CLIMATE_ACTION_COOLING: - return LOG_STR("COOLING"); - case CLIMATE_ACTION_HEATING: - return LOG_STR("HEATING"); - case CLIMATE_ACTION_IDLE: - return LOG_STR("IDLE"); - case CLIMATE_ACTION_DRYING: - return LOG_STR("DRYING"); - case CLIMATE_ACTION_FAN: - return LOG_STR("FAN"); - default: - return LOG_STR("UNKNOWN"); - } + return ClimateActionStrings::get_log_str(static_cast(action), ClimateActionStrings::LAST_INDEX); } +// Climate fan mode strings indexed by ClimateFanMode enum (0-9): ON, OFF, AUTO, LOW, MEDIUM, HIGH, MIDDLE, FOCUS, +// DIFFUSE, QUIET +PROGMEM_STRING_TABLE(ClimateFanModeStrings, "ON", "OFF", "AUTO", "LOW", "MEDIUM", "HIGH", "MIDDLE", "FOCUS", "DIFFUSE", + "QUIET", "UNKNOWN"); + const LogString *climate_fan_mode_to_string(ClimateFanMode fan_mode) { - switch (fan_mode) { - case climate::CLIMATE_FAN_ON: - return LOG_STR("ON"); - case climate::CLIMATE_FAN_OFF: - return LOG_STR("OFF"); - case climate::CLIMATE_FAN_AUTO: - return LOG_STR("AUTO"); - case climate::CLIMATE_FAN_LOW: - return LOG_STR("LOW"); - case climate::CLIMATE_FAN_MEDIUM: - return LOG_STR("MEDIUM"); - case climate::CLIMATE_FAN_HIGH: - return LOG_STR("HIGH"); - case climate::CLIMATE_FAN_MIDDLE: - return LOG_STR("MIDDLE"); - case climate::CLIMATE_FAN_FOCUS: - return LOG_STR("FOCUS"); - case climate::CLIMATE_FAN_DIFFUSE: - return LOG_STR("DIFFUSE"); - case climate::CLIMATE_FAN_QUIET: - return LOG_STR("QUIET"); - default: - return LOG_STR("UNKNOWN"); - } + return ClimateFanModeStrings::get_log_str(static_cast(fan_mode), ClimateFanModeStrings::LAST_INDEX); } +// Climate swing mode strings indexed by ClimateSwingMode enum (0-3): OFF, BOTH, VERTICAL, HORIZONTAL +PROGMEM_STRING_TABLE(ClimateSwingModeStrings, "OFF", "BOTH", "VERTICAL", "HORIZONTAL", "UNKNOWN"); + const LogString *climate_swing_mode_to_string(ClimateSwingMode swing_mode) { - switch (swing_mode) { - case climate::CLIMATE_SWING_OFF: - return LOG_STR("OFF"); - case climate::CLIMATE_SWING_BOTH: - return LOG_STR("BOTH"); - case climate::CLIMATE_SWING_VERTICAL: - return LOG_STR("VERTICAL"); - case climate::CLIMATE_SWING_HORIZONTAL: - return LOG_STR("HORIZONTAL"); - default: - return LOG_STR("UNKNOWN"); - } + return ClimateSwingModeStrings::get_log_str(static_cast(swing_mode), ClimateSwingModeStrings::LAST_INDEX); } +// Climate preset strings indexed by ClimatePreset enum (0-7): NONE, HOME, AWAY, BOOST, COMFORT, ECO, SLEEP, ACTIVITY +PROGMEM_STRING_TABLE(ClimatePresetStrings, "NONE", "HOME", "AWAY", "BOOST", "COMFORT", "ECO", "SLEEP", "ACTIVITY", + "UNKNOWN"); + const LogString *climate_preset_to_string(ClimatePreset preset) { - switch (preset) { - case climate::CLIMATE_PRESET_NONE: - return LOG_STR("NONE"); - case climate::CLIMATE_PRESET_HOME: - return LOG_STR("HOME"); - case climate::CLIMATE_PRESET_ECO: - return LOG_STR("ECO"); - case climate::CLIMATE_PRESET_AWAY: - return LOG_STR("AWAY"); - case climate::CLIMATE_PRESET_BOOST: - return LOG_STR("BOOST"); - case climate::CLIMATE_PRESET_COMFORT: - return LOG_STR("COMFORT"); - case climate::CLIMATE_PRESET_SLEEP: - return LOG_STR("SLEEP"); - case climate::CLIMATE_PRESET_ACTIVITY: - return LOG_STR("ACTIVITY"); - default: - return LOG_STR("UNKNOWN"); - } + return ClimatePresetStrings::get_log_str(static_cast(preset), ClimatePresetStrings::LAST_INDEX); } } // namespace esphome::climate diff --git a/esphome/components/climate/climate_mode.h b/esphome/components/climate/climate_mode.h index c961c44248..014b1a9e64 100644 --- a/esphome/components/climate/climate_mode.h +++ b/esphome/components/climate/climate_mode.h @@ -41,6 +41,8 @@ enum ClimateAction : uint8_t { CLIMATE_ACTION_DRYING = 5, /// The climate device is in fan only mode CLIMATE_ACTION_FAN = 6, + /// The climate device is defrosting + CLIMATE_ACTION_DEFROSTING = 7, }; /// NOTE: If adding values, update ClimateFanModeMask in climate_traits.h to use the new last value diff --git a/esphome/components/climate_ir/climate_ir.cpp b/esphome/components/climate_ir/climate_ir.cpp index 50c8d459b0..cc291ff17c 100644 --- a/esphome/components/climate_ir/climate_ir.cpp +++ b/esphome/components/climate_ir/climate_ir.cpp @@ -71,16 +71,21 @@ void ClimateIR::setup() { } void ClimateIR::control(const climate::ClimateCall &call) { - if (call.get_mode().has_value()) - this->mode = *call.get_mode(); - if (call.get_target_temperature().has_value()) - this->target_temperature = *call.get_target_temperature(); - if (call.get_fan_mode().has_value()) - this->fan_mode = *call.get_fan_mode(); - if (call.get_swing_mode().has_value()) - this->swing_mode = *call.get_swing_mode(); - if (call.get_preset().has_value()) - this->preset = *call.get_preset(); + auto mode = call.get_mode(); + if (mode.has_value()) + this->mode = *mode; + auto target_temperature = call.get_target_temperature(); + if (target_temperature.has_value()) + this->target_temperature = *target_temperature; + auto fan_mode = call.get_fan_mode(); + if (fan_mode.has_value()) + this->fan_mode = fan_mode; + auto swing_mode = call.get_swing_mode(); + if (swing_mode.has_value()) + this->swing_mode = *swing_mode; + auto preset = call.get_preset(); + if (preset.has_value()) + this->preset = preset; this->transmit_state(); this->publish_state(); } diff --git a/esphome/components/climate_ir_lg/climate_ir_lg.cpp b/esphome/components/climate_ir_lg/climate_ir_lg.cpp index 7fe0646230..90e3d006a8 100644 --- a/esphome/components/climate_ir_lg/climate_ir_lg.cpp +++ b/esphome/components/climate_ir_lg/climate_ir_lg.cpp @@ -79,7 +79,7 @@ void LgIrClimate::transmit_state() { if (this->mode == climate::CLIMATE_MODE_OFF) { remote_state |= FAN_AUTO; } else { - switch (this->fan_mode.value()) { + switch (this->fan_mode.value_or(climate::CLIMATE_FAN_ON)) { case climate::CLIMATE_FAN_HIGH: remote_state |= FAN_MAX; break; diff --git a/esphome/components/climate_ir_lg/climate_ir_lg.h b/esphome/components/climate_ir_lg/climate_ir_lg.h index 00fc99ae73..958245279f 100644 --- a/esphome/components/climate_ir_lg/climate_ir_lg.h +++ b/esphome/components/climate_ir_lg/climate_ir_lg.h @@ -23,7 +23,8 @@ class LgIrClimate : public climate_ir::ClimateIR { void control(const climate::ClimateCall &call) override { this->send_swing_cmd_ = call.get_swing_mode().has_value(); // swing resets after unit powered off - if (call.get_mode().has_value() && *call.get_mode() == climate::CLIMATE_MODE_OFF) + auto mode = call.get_mode(); + if (mode.has_value() && *mode == climate::CLIMATE_MODE_OFF) this->swing_mode = climate::CLIMATE_SWING_OFF; climate_ir::ClimateIR::control(call); } diff --git a/esphome/components/cm1106/cm1106.h b/esphome/components/cm1106/cm1106.h index ad089bbe7d..8c33e56457 100644 --- a/esphome/components/cm1106/cm1106.h +++ b/esphome/components/cm1106/cm1106.h @@ -10,8 +10,6 @@ namespace cm1106 { class CM1106Component : public PollingComponent, public uart::UARTDevice { public: - float get_setup_priority() const override { return esphome::setup_priority::DATA; } - void setup() override; void update() override; void dump_config() override; diff --git a/esphome/components/cm1106/sensor.py b/esphome/components/cm1106/sensor.py index 1d95bcc666..3c82fac977 100644 --- a/esphome/components/cm1106/sensor.py +++ b/esphome/components/cm1106/sensor.py @@ -65,6 +65,7 @@ CALIBRATION_ACTION_SCHEMA = maybe_simple_id( "cm1106.calibrate_zero", CM1106CalibrateZeroAction, CALIBRATION_ACTION_SCHEMA, + synchronous=True, ) async def cm1106_calibration_to_code(config, action_id, template_arg, args) -> None: """Service code generation entry point.""" diff --git a/esphome/components/combination/combination.cpp b/esphome/components/combination/combination.cpp index 716d270390..b858eee4ee 100644 --- a/esphome/components/combination/combination.cpp +++ b/esphome/components/combination/combination.cpp @@ -4,8 +4,6 @@ #include "esphome/core/hal.h" #include -#include -#include namespace esphome { namespace combination { @@ -20,12 +18,12 @@ void CombinationComponent::log_config_(const LogString *combo_type) { void CombinationNoParameterComponent::add_source(Sensor *sensor) { this->sensors_.emplace_back(sensor); } -void CombinationOneParameterComponent::add_source(Sensor *sensor, std::function const &stddev) { - this->sensor_pairs_.emplace_back(sensor, stddev); +void CombinationOneParameterComponent::add_source(Sensor *sensor, std::function const &compute) { + this->sensor_sources_.push_back({sensor, compute, this}); } -void CombinationOneParameterComponent::add_source(Sensor *sensor, float stddev) { - this->add_source(sensor, std::function{[stddev](float x) -> float { return stddev; }}); +void CombinationOneParameterComponent::add_source(Sensor *sensor, float value) { + this->add_source(sensor, std::function{[value](float x) -> float { return value; }}); } void CombinationNoParameterComponent::log_source_sensors() { @@ -37,9 +35,8 @@ void CombinationNoParameterComponent::log_source_sensors() { void CombinationOneParameterComponent::log_source_sensors() { ESP_LOGCONFIG(TAG, " Source Sensors:"); - for (const auto &sensor : this->sensor_pairs_) { - auto &entity = *sensor.first; - ESP_LOGCONFIG(TAG, " - %s", entity.get_name().c_str()); + for (const auto &source : this->sensor_sources_) { + ESP_LOGCONFIG(TAG, " - %s", source.sensor->get_name().c_str()); } } @@ -62,9 +59,12 @@ void KalmanCombinationComponent::dump_config() { } void KalmanCombinationComponent::setup() { - for (const auto &sensor : this->sensor_pairs_) { - const auto stddev = sensor.second; - sensor.first->add_on_state_callback([this, stddev](float x) -> void { this->correct_(x, stddev(x)); }); + for (auto &source : this->sensor_sources_) { + // [&source] is safe: source refers to a FixedVector element that never reallocates, + // so the reference remains valid for the component's lifetime. + source.sensor->add_on_state_callback([&source](float x) -> void { + static_cast(source.parent)->correct_(x, source.compute(x)); + }); } } @@ -117,26 +117,26 @@ void KalmanCombinationComponent::correct_(float value, float stddev) { } void LinearCombinationComponent::setup() { - for (const auto &sensor : this->sensor_pairs_) { + for (auto &source : this->sensor_sources_) { // All sensor updates are deferred until the next loop. This avoids publishing the combined sensor's result // repeatedly in the same loop if multiple source senors update. - sensor.first->add_on_state_callback( + source.sensor->add_on_state_callback( [this](float value) -> void { this->defer("update", [this, value]() { this->handle_new_value(value); }); }); } } void LinearCombinationComponent::handle_new_value(float value) { - // Multiplies each sensor state by a configured coeffecient and then sums + // Multiplies each sensor state by a configured coefficient and then sums if (!std::isfinite(value)) return; float sum = 0.0; - for (const auto &sensor : this->sensor_pairs_) { - const float sensor_state = sensor.first->state; + for (const auto &source : this->sensor_sources_) { + const float sensor_state = source.sensor->state; if (std::isfinite(sensor_state)) { - sum += sensor_state * sensor.second(sensor_state); + sum += sensor_state * source.compute(sensor_state); } } @@ -163,7 +163,7 @@ void MeanCombinationComponent::handle_new_value(float value) { return; float sum = 0.0; - size_t count = 0.0; + size_t count = 0; for (const auto &sensor : this->sensors_) { if (std::isfinite(sensor->state)) { @@ -172,6 +172,10 @@ void MeanCombinationComponent::handle_new_value(float value) { } } + if (count == 0) { + this->publish_state(NAN); + return; + } float mean = sum / count; this->publish_state(mean); @@ -238,6 +242,11 @@ void RangeCombinationComponent::handle_new_value(float value) { } } + if (sensor_states.empty()) { + this->publish_state(NAN); + return; + } + sort(sensor_states.begin(), sensor_states.end()); float range = sensor_states.back() - sensor_states.front(); diff --git a/esphome/components/combination/combination.h b/esphome/components/combination/combination.h index 901aeaf259..463eedc564 100644 --- a/esphome/components/combination/combination.h +++ b/esphome/components/combination/combination.h @@ -1,17 +1,16 @@ #pragma once #include "esphome/core/component.h" +#include "esphome/core/helpers.h" #include "esphome/components/sensor/sensor.h" -#include +#include namespace esphome { namespace combination { class CombinationComponent : public Component, public sensor::Sensor { public: - float get_setup_priority() const override { return esphome::setup_priority::DATA; } - /// @brief Logs all source sensor's names virtual void log_source_sensors() = 0; @@ -43,14 +42,21 @@ class CombinationNoParameterComponent : public CombinationComponent { // Base class for opertions that require one parameter to compute the combination class CombinationOneParameterComponent : public CombinationComponent { public: - void add_source(Sensor *sensor, std::function const &stddev); - void add_source(Sensor *sensor, float stddev); + void set_source_count(size_t count) { this->sensor_sources_.init(count); } + void add_source(Sensor *sensor, std::function const &compute); + void add_source(Sensor *sensor, float value); - /// @brief Logs all source sensor's names in sensor_pairs_ + /// @brief Logs all source sensors' names in sensor_sources_ void log_source_sensors() override; protected: - std::vector>> sensor_pairs_; + struct SensorSource { + sensor::Sensor *sensor; + std::function compute; + CombinationOneParameterComponent *parent; + }; + + FixedVector sensor_sources_; }; class KalmanCombinationComponent : public CombinationOneParameterComponent { diff --git a/esphome/components/combination/sensor.py b/esphome/components/combination/sensor.py index f5255fec03..327cedee1e 100644 --- a/esphome/components/combination/sensor.py +++ b/esphome/components/combination/sensor.py @@ -1,3 +1,5 @@ +import logging + import esphome.codegen as cg from esphome.components import sensor import esphome.config_validation as cv @@ -15,6 +17,8 @@ from esphome.const import ( ) from esphome.core.entity_helpers import inherit_property_from +_LOGGER = logging.getLogger(__name__) + CODEOWNERS = ["@Cat-Ion", "@kahrendt"] combination_ns = cg.esphome_ns.namespace("combination") @@ -47,7 +51,8 @@ SumCombinationComponent = combination_ns.class_( "SumCombinationComponent", cg.Component, sensor.Sensor ) -CONF_COEFFECIENT = "coeffecient" +CONF_COEFFICIENT = "coefficient" +CONF_COEFFECIENT = "coeffecient" # Deprecated, remove before 2026.12.0 CONF_ERROR = "error" CONF_KALMAN = "kalman" CONF_LINEAR = "linear" @@ -68,11 +73,34 @@ KALMAN_SOURCE_SCHEMA = cv.Schema( } ) -LINEAR_SOURCE_SCHEMA = cv.Schema( - { - cv.Required(CONF_SOURCE): cv.use_id(sensor.Sensor), - cv.Required(CONF_COEFFECIENT): cv.templatable(cv.float_), - } + +def _migrate_coeffecient(config): + """Migrate deprecated 'coeffecient' spelling to 'coefficient'.""" + if CONF_COEFFECIENT in config: + if CONF_COEFFICIENT in config: + raise cv.Invalid( + f"Cannot specify both '{CONF_COEFFICIENT}' and '{CONF_COEFFECIENT}'" + ) + _LOGGER.warning( + "'%s' is deprecated, use '%s' instead. Will be removed in 2026.12.0", + CONF_COEFFECIENT, + CONF_COEFFICIENT, + ) + config[CONF_COEFFICIENT] = config.pop(CONF_COEFFECIENT) + elif CONF_COEFFICIENT not in config: + raise cv.Invalid(f"'{CONF_COEFFICIENT}' is a required option") + return config + + +LINEAR_SOURCE_SCHEMA = cv.All( + cv.Schema( + { + cv.Required(CONF_SOURCE): cv.use_id(sensor.Sensor), + cv.Optional(CONF_COEFFICIENT): cv.templatable(cv.float_), + cv.Optional(CONF_COEFFECIENT): cv.templatable(cv.float_), + } + ), + _migrate_coeffecient, ) SENSOR_ONLY_SOURCE_SCHEMA = cv.Schema( @@ -152,6 +180,9 @@ async def to_code(config): if proces_std_dev := config.get(CONF_PROCESS_STD_DEV): cg.add(var.set_process_std_dev(proces_std_dev)) + if config[CONF_TYPE] in (CONF_KALMAN, CONF_LINEAR): + cg.add(var.set_source_count(len(config[CONF_SOURCES]))) + for source_conf in config[CONF_SOURCES]: source = await cg.get_variable(source_conf[CONF_SOURCE]) if config[CONF_TYPE] == CONF_KALMAN: @@ -162,12 +193,12 @@ async def to_code(config): ) cg.add(var.add_source(source, error)) elif config[CONF_TYPE] == CONF_LINEAR: - coeffecient = await cg.templatable( - source_conf[CONF_COEFFECIENT], + coefficient = await cg.templatable( + source_conf[CONF_COEFFICIENT], [(float, "x")], cg.float_, ) - cg.add(var.add_source(source, coeffecient)) + cg.add(var.add_source(source, coefficient)) else: cg.add(var.add_source(source)) diff --git a/esphome/components/const/__init__.py b/esphome/components/const/__init__.py index fcfafa0c1a..846d3fd883 100644 --- a/esphome/components/const/__init__.py +++ b/esphome/components/const/__init__.py @@ -3,17 +3,35 @@ CODEOWNERS = ["@esphome/core"] CONF_BYTE_ORDER = "byte_order" +CONF_CLIMATE_ID = "climate_id" BYTE_ORDER_LITTLE = "little_endian" BYTE_ORDER_BIG = "big_endian" CONF_COLOR_DEPTH = "color_depth" CONF_CRC_ENABLE = "crc_enable" +CONF_DATA_BITS = "data_bits" CONF_DRAW_ROUNDING = "draw_rounding" CONF_ENABLED = "enabled" CONF_IGNORE_NOT_FOUND = "ignore_not_found" +CONF_LIBRETINY = "libretiny" CONF_ON_PACKET = "on_packet" CONF_ON_RECEIVE = "on_receive" CONF_ON_STATE_CHANGE = "on_state_change" +CONF_PARITY = "parity" +CONF_RECEIVER_FREQUENCY = "receiver_frequency" CONF_REQUEST_HEADERS = "request_headers" CONF_ROWS = "rows" +CONF_STOP_BITS = "stop_bits" CONF_USE_PSRAM = "use_psram" +CONF_VOLUME_INCREMENT = "volume_increment" +CONF_VOLUME_INITIAL = "volume_initial" +CONF_VOLUME_MAX = "volume_max" +CONF_VOLUME_MIN = "volume_min" + +ICON_CURRENT_DC = "mdi:current-dc" +ICON_SOLAR_PANEL = "mdi:solar-panel" +ICON_SOLAR_POWER = "mdi:solar-power" + +KEY_METADATA = "metadata" + +UNIT_AMPERE_HOUR = "Ah" diff --git a/esphome/components/coolix/coolix.cpp b/esphome/components/coolix/coolix.cpp index 5c6bfd7740..d8ea676478 100644 --- a/esphome/components/coolix/coolix.cpp +++ b/esphome/components/coolix/coolix.cpp @@ -83,7 +83,7 @@ void CoolixClimate::transmit_state() { this->fan_mode = climate::CLIMATE_FAN_AUTO; remote_state |= COOLIX_FAN_MODE_AUTO_DRY; } else { - switch (this->fan_mode.value()) { + switch (this->fan_mode.value_or(climate::CLIMATE_FAN_ON)) { case climate::CLIMATE_FAN_HIGH: remote_state |= COOLIX_FAN_MAX; break; diff --git a/esphome/components/coolix/coolix.h b/esphome/components/coolix/coolix.h index f4b4ff8e0e..51ddcdf8f2 100644 --- a/esphome/components/coolix/coolix.h +++ b/esphome/components/coolix/coolix.h @@ -23,7 +23,8 @@ class CoolixClimate : public climate_ir::ClimateIR { void control(const climate::ClimateCall &call) override { send_swing_cmd_ = call.get_swing_mode().has_value(); // swing resets after unit powered off - if (call.get_mode().has_value() && *call.get_mode() == climate::CLIMATE_MODE_OFF) + auto mode = call.get_mode(); + if (mode.has_value() && *mode == climate::CLIMATE_MODE_OFF) this->swing_mode = climate::CLIMATE_SWING_OFF; climate_ir::ClimateIR::control(call); } diff --git a/esphome/components/copy/cover/copy_cover.cpp b/esphome/components/copy/cover/copy_cover.cpp index 28f8c9877c..c139869d8f 100644 --- a/esphome/components/copy/cover/copy_cover.cpp +++ b/esphome/components/copy/cover/copy_cover.cpp @@ -38,12 +38,15 @@ cover::CoverTraits CopyCover::get_traits() { void CopyCover::control(const cover::CoverCall &call) { auto call2 = source_->make_call(); call2.set_stop(call.get_stop()); - if (call.get_tilt().has_value()) - call2.set_tilt(*call.get_tilt()); - if (call.get_position().has_value()) - call2.set_position(*call.get_position()); - if (call.get_tilt().has_value()) - call2.set_tilt(*call.get_tilt()); + auto tilt = call.get_tilt(); + if (tilt.has_value()) + call2.set_tilt(*tilt); + auto position = call.get_position(); + if (position.has_value()) + call2.set_position(*position); + auto tilt2 = call.get_tilt(); + if (tilt2.has_value()) + call2.set_tilt(*tilt2); call2.perform(); } diff --git a/esphome/components/copy/fan/copy_fan.cpp b/esphome/components/copy/fan/copy_fan.cpp index b4a43cf2f1..14c600d71f 100644 --- a/esphome/components/copy/fan/copy_fan.cpp +++ b/esphome/components/copy/fan/copy_fan.cpp @@ -45,14 +45,18 @@ fan::FanTraits CopyFan::get_traits() { void CopyFan::control(const fan::FanCall &call) { auto call2 = source_->make_call(); - if (call.get_state().has_value()) - call2.set_state(*call.get_state()); - if (call.get_oscillating().has_value()) - call2.set_oscillating(*call.get_oscillating()); - if (call.get_speed().has_value()) - call2.set_speed(*call.get_speed()); - if (call.get_direction().has_value()) - call2.set_direction(*call.get_direction()); + auto state = call.get_state(); + if (state.has_value()) + call2.set_state(*state); + auto oscillating = call.get_oscillating(); + if (oscillating.has_value()) + call2.set_oscillating(*oscillating); + auto speed = call.get_speed(); + if (speed.has_value()) + call2.set_speed(*speed); + auto direction = call.get_direction(); + if (direction.has_value()) + call2.set_direction(*direction); if (call.has_preset_mode()) call2.set_preset_mode(call.get_preset_mode()); call2.perform(); diff --git a/esphome/components/copy/lock/copy_lock.cpp b/esphome/components/copy/lock/copy_lock.cpp index 25bd8c33ef..c846954510 100644 --- a/esphome/components/copy/lock/copy_lock.cpp +++ b/esphome/components/copy/lock/copy_lock.cpp @@ -7,7 +7,7 @@ namespace copy { static const char *const TAG = "copy.lock"; void CopyLock::setup() { - source_->add_on_state_callback([this]() { this->publish_state(source_->state); }); + source_->add_on_state_callback([this](lock::LockState state) { this->publish_state(state); }); traits.set_assumed_state(source_->traits.get_assumed_state()); traits.set_requires_code(source_->traits.get_requires_code()); diff --git a/esphome/components/copy/select/copy_select.cpp b/esphome/components/copy/select/copy_select.cpp index e85e08e353..227fe33182 100644 --- a/esphome/components/copy/select/copy_select.cpp +++ b/esphome/components/copy/select/copy_select.cpp @@ -11,8 +11,9 @@ void CopySelect::setup() { traits.set_options(source_->traits.get_options()); - if (source_->has_state()) - this->publish_state(source_->active_index().value()); + auto idx = this->source_->active_index(); + if (idx.has_value()) + this->publish_state(*idx); } void CopySelect::dump_config() { LOG_SELECT("", "Copy Select", this); } diff --git a/esphome/components/cover/__init__.py b/esphome/components/cover/__init__.py index 383daee083..c330241f4d 100644 --- a/esphome/components/cover/__init__.py +++ b/esphome/components/cover/__init__.py @@ -1,3 +1,5 @@ +import logging + from esphome import automation from esphome.automation import Condition, maybe_simple_id import esphome.codegen as cg @@ -9,6 +11,8 @@ from esphome.const import ( CONF_ICON, CONF_ID, CONF_MQTT_ID, + CONF_MQTT_JSON_STATE_PAYLOAD, + CONF_ON_IDLE, CONF_ON_OPEN, CONF_POSITION, CONF_POSITION_COMMAND_TOPIC, @@ -32,9 +36,14 @@ from esphome.const import ( DEVICE_CLASS_SHUTTER, DEVICE_CLASS_WINDOW, ) -from esphome.core import CORE, CoroPriority, coroutine_with_priority -from esphome.core.entity_helpers import entity_duplicate_validator, setup_entity -from esphome.cpp_generator import MockObjClass +from esphome.core import CORE, ID, CoroPriority, coroutine_with_priority +from esphome.core.entity_helpers import ( + entity_duplicate_validator, + setup_device_class, + setup_entity, +) +from esphome.cpp_generator import MockObj, MockObjClass +from esphome.types import ConfigType, TemplateArgsType IS_PLATFORM_COMPONENT = True @@ -53,6 +62,8 @@ DEVICE_CLASSES = [ DEVICE_CLASS_WINDOW, ] +_LOGGER = logging.getLogger(__name__) + cover_ns = cg.esphome_ns.namespace("cover") Cover = cover_ns.class_("Cover", cg.EntityBase) @@ -83,14 +94,29 @@ ControlAction = cover_ns.class_("ControlAction", automation.Action) CoverPublishAction = cover_ns.class_("CoverPublishAction", automation.Action) CoverIsOpenCondition = cover_ns.class_("CoverIsOpenCondition", Condition) CoverIsClosedCondition = cover_ns.class_("CoverIsClosedCondition", Condition) - -# Triggers -CoverOpenTrigger = cover_ns.class_("CoverOpenTrigger", automation.Trigger.template()) +CoverOpenedTrigger = cover_ns.class_( + "CoverOpenedTrigger", automation.Trigger.template() +) CoverClosedTrigger = cover_ns.class_( "CoverClosedTrigger", automation.Trigger.template() ) +CoverTrigger = cover_ns.class_("CoverTrigger", automation.Trigger.template()) +# Cover-specific constants CONF_ON_CLOSED = "on_closed" +CONF_ON_OPENED = "on_opened" +CONF_ON_OPENING = "on_opening" +CONF_ON_CLOSING = "on_closing" + +TRIGGERS = { + CONF_ON_OPEN: CoverOpenedTrigger, # Deprecated, use on_opened + CONF_ON_OPENED: CoverOpenedTrigger, + CONF_ON_CLOSED: CoverClosedTrigger, + CONF_ON_CLOSING: CoverTrigger.template(CoverOperation.COVER_OPERATION_CLOSING), + CONF_ON_OPENING: CoverTrigger.template(CoverOperation.COVER_OPERATION_OPENING), + CONF_ON_IDLE: CoverTrigger.template(CoverOperation.COVER_OPERATION_IDLE), +} + _COVER_SCHEMA = ( cv.ENTITY_BASE_SCHEMA.extend(web_server.WEBSERVER_SORTING_SCHEMA) @@ -98,6 +124,9 @@ _COVER_SCHEMA = ( .extend( { cv.OnlyWith(CONF_MQTT_ID, "mqtt"): cv.declare_id(mqtt.MQTTCoverComponent), + cv.Optional(CONF_MQTT_JSON_STATE_PAYLOAD): cv.All( + cv.requires_component("mqtt"), cv.boolean + ), cv.Optional(CONF_DEVICE_CLASS): cv.one_of(*DEVICE_CLASSES, lower=True), cv.Optional(CONF_POSITION_COMMAND_TOPIC): cv.All( cv.requires_component("mqtt"), cv.subscribe_topic @@ -111,16 +140,14 @@ _COVER_SCHEMA = ( cv.Optional(CONF_TILT_STATE_TOPIC): cv.All( cv.requires_component("mqtt"), cv.subscribe_topic ), - cv.Optional(CONF_ON_OPEN): automation.validate_automation( - { - cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(CoverOpenTrigger), - } - ), - cv.Optional(CONF_ON_CLOSED): automation.validate_automation( - { - cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(CoverClosedTrigger), - } - ), + **{ + cv.Optional(conf): automation.validate_automation( + { + cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(trigger_class), + } + ) + for conf, trigger_class in TRIGGERS.items() + }, } ) ) @@ -129,6 +156,22 @@ _COVER_SCHEMA = ( _COVER_SCHEMA.add_extra(entity_duplicate_validator("cover")) +def _validate_mqtt_state_topics(config): + if config.get(CONF_MQTT_JSON_STATE_PAYLOAD): + if CONF_POSITION_STATE_TOPIC in config: + raise cv.Invalid( + f"'{CONF_POSITION_STATE_TOPIC}' cannot be used with '{CONF_MQTT_JSON_STATE_PAYLOAD}: true'" + ) + if CONF_TILT_STATE_TOPIC in config: + raise cv.Invalid( + f"'{CONF_TILT_STATE_TOPIC}' cannot be used with '{CONF_MQTT_JSON_STATE_PAYLOAD}: true'" + ) + return config + + +_COVER_SCHEMA.add_extra(_validate_mqtt_state_topics) + + def cover_schema( class_: MockObjClass, *, @@ -151,18 +194,18 @@ def cover_schema( return _COVER_SCHEMA.extend(schema) +@setup_entity("cover") async def setup_cover_core_(var, config): - await setup_entity(var, config, "cover") + setup_device_class(config) - if (device_class := config.get(CONF_DEVICE_CLASS)) is not None: - cg.add(var.set_device_class(device_class)) - - for conf in config.get(CONF_ON_OPEN, []): - trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) - await automation.build_automation(trigger, [], conf) - for conf in config.get(CONF_ON_CLOSED, []): - trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) - await automation.build_automation(trigger, [], conf) + if CONF_ON_OPEN in config: + _LOGGER.warning( + "'on_open' is deprecated, use 'on_opened'. Will be removed in 2026.8.0" + ) + for trigger_conf in TRIGGERS: + for conf in config.get(trigger_conf, []): + trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) + await automation.build_automation(trigger, [], conf) if (mqtt_id := config.get(CONF_MQTT_ID)) is not None: mqtt_ = cg.new_Pvariable(mqtt_id, var) @@ -174,6 +217,9 @@ async def setup_cover_core_(var, config): position_command_topic := config.get(CONF_POSITION_COMMAND_TOPIC) ) is not None: cg.add(mqtt_.set_custom_position_command_topic(position_command_topic)) + if config.get(CONF_MQTT_JSON_STATE_PAYLOAD): + cg.add_define("USE_MQTT_COVER_JSON") + cg.add(mqtt_.set_use_json_format(True)) if (tilt_state_topic := config.get(CONF_TILT_STATE_TOPIC)) is not None: cg.add(mqtt_.set_custom_tilt_state_topic(tilt_state_topic)) if (tilt_command_topic := config.get(CONF_TILT_COMMAND_TOPIC)) is not None: @@ -204,25 +250,33 @@ COVER_ACTION_SCHEMA = maybe_simple_id( ) -@automation.register_action("cover.open", OpenAction, COVER_ACTION_SCHEMA) +@automation.register_action( + "cover.open", OpenAction, COVER_ACTION_SCHEMA, synchronous=True +) async def cover_open_to_code(config, action_id, template_arg, args): paren = await cg.get_variable(config[CONF_ID]) return cg.new_Pvariable(action_id, template_arg, paren) -@automation.register_action("cover.close", CloseAction, COVER_ACTION_SCHEMA) +@automation.register_action( + "cover.close", CloseAction, COVER_ACTION_SCHEMA, synchronous=True +) async def cover_close_to_code(config, action_id, template_arg, args): paren = await cg.get_variable(config[CONF_ID]) return cg.new_Pvariable(action_id, template_arg, paren) -@automation.register_action("cover.stop", StopAction, COVER_ACTION_SCHEMA) +@automation.register_action( + "cover.stop", StopAction, COVER_ACTION_SCHEMA, synchronous=True +) async def cover_stop_to_code(config, action_id, template_arg, args): paren = await cg.get_variable(config[CONF_ID]) return cg.new_Pvariable(action_id, template_arg, paren) -@automation.register_action("cover.toggle", ToggleAction, COVER_ACTION_SCHEMA) +@automation.register_action( + "cover.toggle", ToggleAction, COVER_ACTION_SCHEMA, synchronous=True +) async def cover_toggle_to_code(config, action_id, template_arg, args): paren = await cg.get_variable(config[CONF_ID]) return cg.new_Pvariable(action_id, template_arg, paren) @@ -239,7 +293,9 @@ COVER_CONTROL_ACTION_SCHEMA = cv.Schema( ) -@automation.register_action("cover.control", ControlAction, COVER_CONTROL_ACTION_SCHEMA) +@automation.register_action( + "cover.control", ControlAction, COVER_CONTROL_ACTION_SCHEMA, synchronous=True +) async def cover_control_to_code(config, action_id, template_arg, args): paren = await cg.get_variable(config[CONF_ID]) var = cg.new_Pvariable(action_id, template_arg, paren) @@ -258,6 +314,26 @@ async def cover_control_to_code(config, action_id, template_arg, args): return var +COVER_CONDITION_SCHEMA = cv.maybe_simple_value( + {cv.Required(CONF_ID): cv.use_id(Cover)}, key=CONF_ID +) + + +async def cover_condition_to_code( + config: ConfigType, condition_id: ID, template_arg: MockObj, args: TemplateArgsType +) -> MockObj: + paren = await cg.get_variable(config[CONF_ID]) + return cg.new_Pvariable(condition_id, template_arg, paren) + + +automation.register_condition( + "cover.is_open", CoverIsOpenCondition, COVER_CONDITION_SCHEMA +)(cover_condition_to_code) +automation.register_condition( + "cover.is_closed", CoverIsClosedCondition, COVER_CONDITION_SCHEMA +)(cover_condition_to_code) + + @coroutine_with_priority(CoroPriority.CORE) async def to_code(config): cg.add_global(cover_ns.using) diff --git a/esphome/components/cover/automation.h b/esphome/components/cover/automation.h index c0345a7cc6..f121e5c2d6 100644 --- a/esphome/components/cover/automation.h +++ b/esphome/components/cover/automation.h @@ -90,44 +90,55 @@ template class CoverPublishAction : public Action { Cover *cover_; }; -template class CoverIsOpenCondition : public Condition { +template class CoverPositionCondition : public Condition { public: - CoverIsOpenCondition(Cover *cover) : cover_(cover) {} - bool check(const Ts &...x) override { return this->cover_->is_fully_open(); } + CoverPositionCondition(Cover *cover) : cover_(cover) {} + + bool check(const Ts &...x) override { return this->cover_->position == (OPEN ? COVER_OPEN : COVER_CLOSED); } protected: Cover *cover_; }; -template class CoverIsClosedCondition : public Condition { +template using CoverIsOpenCondition = CoverPositionCondition; +template using CoverIsClosedCondition = CoverPositionCondition; + +template class CoverPositionTrigger : public Trigger<> { public: - CoverIsClosedCondition(Cover *cover) : cover_(cover) {} - bool check(const Ts &...x) override { return this->cover_->is_fully_closed(); } + CoverPositionTrigger(Cover *a_cover) : cover_(a_cover) { + a_cover->add_on_state_callback([this]() { + if (this->cover_->position != this->last_position_) { + this->last_position_ = this->cover_->position; + if (this->cover_->position == (OPEN ? COVER_OPEN : COVER_CLOSED)) + this->trigger(); + } + }); + } protected: Cover *cover_; + float last_position_{NAN}; }; -class CoverOpenTrigger : public Trigger<> { +using CoverOpenedTrigger = CoverPositionTrigger; +using CoverClosedTrigger = CoverPositionTrigger; + +template class CoverTrigger : public Trigger<> { public: - CoverOpenTrigger(Cover *a_cover) { - a_cover->add_on_state_callback([this, a_cover]() { - if (a_cover->is_fully_open()) { - this->trigger(); + CoverTrigger(Cover *a_cover) : cover_(a_cover) { + a_cover->add_on_state_callback([this]() { + auto current_op = this->cover_->current_operation; + if (current_op == OP) { + if (!this->last_operation_.has_value() || this->last_operation_.value() != OP) { + this->trigger(); + } } + this->last_operation_ = current_op; }); } -}; -class CoverClosedTrigger : public Trigger<> { - public: - CoverClosedTrigger(Cover *a_cover) { - a_cover->add_on_state_callback([this, a_cover]() { - if (a_cover->is_fully_closed()) { - this->trigger(); - } - }); - } + protected: + Cover *cover_; + optional last_operation_{}; }; - } // namespace esphome::cover diff --git a/esphome/components/cover/cover.cpp b/esphome/components/cover/cover.cpp index 0d9e7e8ffb..e98a555fe5 100644 --- a/esphome/components/cover/cover.cpp +++ b/esphome/components/cover/cover.cpp @@ -10,9 +10,6 @@ namespace esphome::cover { static const char *const TAG = "cover"; -const float COVER_OPEN = 1.0f; -const float COVER_CLOSED = 0.0f; - const LogString *cover_command_to_str(float pos) { if (pos == COVER_OPEN) { return LOG_STR("OPEN"); @@ -22,17 +19,11 @@ const LogString *cover_command_to_str(float pos) { return LOG_STR("UNKNOWN"); } } +// Cover operation strings indexed by CoverOperation enum (0-2): IDLE, OPENING, CLOSING, plus UNKNOWN +PROGMEM_STRING_TABLE(CoverOperationStrings, "IDLE", "OPENING", "CLOSING", "UNKNOWN"); + const LogString *cover_operation_to_str(CoverOperation op) { - switch (op) { - case COVER_OPERATION_IDLE: - return LOG_STR("IDLE"); - case COVER_OPERATION_OPENING: - return LOG_STR("OPENING"); - case COVER_OPERATION_CLOSING: - return LOG_STR("CLOSING"); - default: - return LOG_STR("UNKNOWN"); - } + return CoverOperationStrings::get_log_str(static_cast(op), CoverOperationStrings::LAST_INDEX); } Cover::Cover() : position{COVER_OPEN} {} @@ -77,24 +68,24 @@ CoverCall &CoverCall::set_tilt(float tilt) { return *this; } void CoverCall::perform() { - ESP_LOGD(TAG, "'%s' - Setting", this->parent_->get_name().c_str()); + ESP_LOGV(TAG, "'%s' - Setting", this->parent_->get_name().c_str()); auto traits = this->parent_->get_traits(); this->validate_(); if (this->stop_) { - ESP_LOGD(TAG, " Command: STOP"); + ESP_LOGV(TAG, " Command: STOP"); } if (this->position_.has_value()) { if (traits.get_supports_position()) { - ESP_LOGD(TAG, " Position: %.0f%%", *this->position_ * 100.0f); + ESP_LOGV(TAG, " Position: %.0f%%", *this->position_ * 100.0f); } else { - ESP_LOGD(TAG, " Command: %s", LOG_STR_ARG(cover_command_to_str(*this->position_))); + ESP_LOGV(TAG, " Command: %s", LOG_STR_ARG(cover_command_to_str(*this->position_))); } } if (this->tilt_.has_value()) { - ESP_LOGD(TAG, " Tilt: %.0f%%", *this->tilt_ * 100.0f); + ESP_LOGV(TAG, " Tilt: %.0f%%", *this->tilt_ * 100.0f); } if (this->toggle_.has_value()) { - ESP_LOGD(TAG, " Command: TOGGLE"); + ESP_LOGV(TAG, " Command: TOGGLE"); } this->parent_->control(*this); } @@ -148,28 +139,27 @@ bool CoverCall::get_stop() const { return this->stop_; } CoverCall Cover::make_call() { return {this}; } -void Cover::add_on_state_callback(std::function &&f) { this->state_callback_.add(std::move(f)); } void Cover::publish_state(bool save) { this->position = clamp(this->position, 0.0f, 1.0f); this->tilt = clamp(this->tilt, 0.0f, 1.0f); - ESP_LOGD(TAG, "'%s' >>", this->name_.c_str()); + ESP_LOGV(TAG, "'%s' >>", this->name_.c_str()); auto traits = this->get_traits(); if (traits.get_supports_position()) { - ESP_LOGD(TAG, " Position: %.0f%%", this->position * 100.0f); + ESP_LOGV(TAG, " Position: %.0f%%", this->position * 100.0f); } else { if (this->position == COVER_OPEN) { - ESP_LOGD(TAG, " State: OPEN"); + ESP_LOGV(TAG, " State: OPEN"); } else if (this->position == COVER_CLOSED) { - ESP_LOGD(TAG, " State: CLOSED"); + ESP_LOGV(TAG, " State: CLOSED"); } else { - ESP_LOGD(TAG, " State: UNKNOWN"); + ESP_LOGV(TAG, " State: UNKNOWN"); } } if (traits.get_supports_tilt()) { - ESP_LOGD(TAG, " Tilt: %.0f%%", this->tilt * 100.0f); + ESP_LOGV(TAG, " Tilt: %.0f%%", this->tilt * 100.0f); } - ESP_LOGD(TAG, " Current Operation: %s", LOG_STR_ARG(cover_operation_to_str(this->current_operation))); + ESP_LOGV(TAG, " Current Operation: %s", LOG_STR_ARG(cover_operation_to_str(this->current_operation))); this->state_callback_.call(); #if defined(USE_COVER) && defined(USE_CONTROLLER_REGISTRY) diff --git a/esphome/components/cover/cover.h b/esphome/components/cover/cover.h index e5427ceaa8..9a75e68487 100644 --- a/esphome/components/cover/cover.h +++ b/esphome/components/cover/cover.h @@ -10,8 +10,8 @@ namespace esphome::cover { -const extern float COVER_OPEN; -const extern float COVER_CLOSED; +static constexpr float COVER_OPEN = 1.0f; +static constexpr float COVER_CLOSED = 0.0f; #define LOG_COVER(prefix, type, obj) \ if ((obj) != nullptr) { \ @@ -107,7 +107,7 @@ const LogString *cover_operation_to_str(CoverOperation op); * to control all values of the cover. Also implement get_traits() to return what operations * the cover supports. */ -class Cover : public EntityBase, public EntityBase_DeviceClass { +class Cover : public EntityBase { public: explicit Cover(); @@ -125,7 +125,7 @@ class Cover : public EntityBase, public EntityBase_DeviceClass { /// Construct a new cover call used to control the cover. CoverCall make_call(); - void add_on_state_callback(std::function &&f); + template void add_on_state_callback(F &&f) { this->state_callback_.add(std::forward(f)); } /** Publish the current state of the cover. * diff --git a/esphome/components/cs5460a/sensor.py b/esphome/components/cs5460a/sensor.py index 07b5ea1c63..d2383bd01b 100644 --- a/esphome/components/cs5460a/sensor.py +++ b/esphome/components/cs5460a/sensor.py @@ -132,6 +132,7 @@ async def to_code(config): cv.Required(CONF_ID): cv.use_id(CS5460AComponent), } ), + synchronous=True, ) async def restart_action_to_code(config, action_id, template_arg, args): paren = await cg.get_variable(config[CONF_ID]) diff --git a/esphome/components/cse7761/cse7761.cpp b/esphome/components/cse7761/cse7761.cpp index 482636dd81..7525b901f8 100644 --- a/esphome/components/cse7761/cse7761.cpp +++ b/esphome/components/cse7761/cse7761.cpp @@ -15,29 +15,29 @@ static const char *const TAG = "cse7761"; * https://github.com/arendst/Tasmota/blob/development/tasmota/xnrg_19_cse7761.ino \*********************************************************************************************/ -static const int CSE7761_UREF = 42563; // RmsUc -static const int CSE7761_IREF = 52241; // RmsIAC -static const int CSE7761_PREF = 44513; // PowerPAC +static constexpr int CSE7761_UREF = 42563; // RmsUc +static constexpr int CSE7761_IREF = 52241; // RmsIAC +static constexpr int CSE7761_PREF = 44513; // PowerPAC -static const uint8_t CSE7761_REG_SYSCON = 0x00; // (2) System Control Register (0x0A04) -static const uint8_t CSE7761_REG_EMUCON = 0x01; // (2) Metering control register (0x0000) -static const uint8_t CSE7761_REG_EMUCON2 = 0x13; // (2) Metering control register 2 (0x0001) -static const uint8_t CSE7761_REG_PULSE1SEL = 0x1D; // (2) Pin function output select register (0x3210) +static constexpr uint8_t CSE7761_REG_SYSCON = 0x00; // (2) System Control Register (0x0A04) +static constexpr uint8_t CSE7761_REG_EMUCON = 0x01; // (2) Metering control register (0x0000) +static constexpr uint8_t CSE7761_REG_EMUCON2 = 0x13; // (2) Metering control register 2 (0x0001) +static constexpr uint8_t CSE7761_REG_PULSE1SEL = 0x1D; // (2) Pin function output select register (0x3210) -static const uint8_t CSE7761_REG_RMSIA = 0x24; // (3) The effective value of channel A current (0x000000) -static const uint8_t CSE7761_REG_RMSIB = 0x25; // (3) The effective value of channel B current (0x000000) -static const uint8_t CSE7761_REG_RMSU = 0x26; // (3) Voltage RMS (0x000000) -static const uint8_t CSE7761_REG_POWERPA = 0x2C; // (4) Channel A active power, update rate 27.2Hz (0x00000000) -static const uint8_t CSE7761_REG_POWERPB = 0x2D; // (4) Channel B active power, update rate 27.2Hz (0x00000000) -static const uint8_t CSE7761_REG_SYSSTATUS = 0x43; // (1) System status register +static constexpr uint8_t CSE7761_REG_RMSIA = 0x24; // (3) The effective value of channel A current (0x000000) +static constexpr uint8_t CSE7761_REG_RMSIB = 0x25; // (3) The effective value of channel B current (0x000000) +static constexpr uint8_t CSE7761_REG_RMSU = 0x26; // (3) Voltage RMS (0x000000) +static constexpr uint8_t CSE7761_REG_POWERPA = 0x2C; // (4) Channel A active power, update rate 27.2Hz (0x00000000) +static constexpr uint8_t CSE7761_REG_POWERPB = 0x2D; // (4) Channel B active power, update rate 27.2Hz (0x00000000) +static constexpr uint8_t CSE7761_REG_SYSSTATUS = 0x43; // (1) System status register -static const uint8_t CSE7761_REG_COEFFCHKSUM = 0x6F; // (2) Coefficient checksum -static const uint8_t CSE7761_REG_RMSIAC = 0x70; // (2) Channel A effective current conversion coefficient +static constexpr uint8_t CSE7761_REG_COEFFCHKSUM = 0x6F; // (2) Coefficient checksum +static constexpr uint8_t CSE7761_REG_RMSIAC = 0x70; // (2) Channel A effective current conversion coefficient -static const uint8_t CSE7761_SPECIAL_COMMAND = 0xEA; // Start special command -static const uint8_t CSE7761_CMD_RESET = 0x96; // Reset command, after receiving the command, the chip resets -static const uint8_t CSE7761_CMD_CLOSE_WRITE = 0xDC; // Close write operation -static const uint8_t CSE7761_CMD_ENABLE_WRITE = 0xE5; // Enable write operation +static constexpr uint8_t CSE7761_SPECIAL_COMMAND = 0xEA; // Start special command +static constexpr uint8_t CSE7761_CMD_RESET = 0x96; // Reset command, after receiving the command, the chip resets +static constexpr uint8_t CSE7761_CMD_CLOSE_WRITE = 0xDC; // Close write operation +static constexpr uint8_t CSE7761_CMD_ENABLE_WRITE = 0xE5; // Enable write operation enum CSE7761 { RMS_IAC, RMS_IBC, RMS_UC, POWER_PAC, POWER_PBC, POWER_SC, ENERGY_AC, ENERGY_BC }; @@ -62,8 +62,6 @@ void CSE7761Component::dump_config() { this->check_uart_settings(38400, 1, uart::UART_CONFIG_PARITY_EVEN, 8); } -float CSE7761Component::get_setup_priority() const { return setup_priority::DATA; } - void CSE7761Component::update() { if (this->data_.ready) { this->get_data_(); @@ -149,13 +147,17 @@ uint32_t CSE7761Component::read_(uint8_t reg, uint8_t size) { } uint32_t CSE7761Component::coefficient_by_unit_(uint32_t unit) { + uint32_t coeff = 0; switch (unit) { case RMS_UC: - return 0x400000 * 100 / this->data_.coefficient[RMS_UC]; + coeff = this->data_.coefficient[RMS_UC]; + return coeff ? 0x400000 * 100 / coeff : 0; case RMS_IAC: - return (0x800000 * 100 / this->data_.coefficient[RMS_IAC]) * 10; // Stay within 32 bits + coeff = this->data_.coefficient[RMS_IAC]; + return coeff ? (0x800000 * 100 / coeff) * 10 : 0; // Stay within 32 bits case POWER_PAC: - return 0x80000000 / this->data_.coefficient[POWER_PAC]; + coeff = this->data_.coefficient[POWER_PAC]; + return coeff ? 0x80000000 / coeff : 0; } return 0; } diff --git a/esphome/components/cse7761/cse7761.h b/esphome/components/cse7761/cse7761.h index 71846cdcab..289c5e7e19 100644 --- a/esphome/components/cse7761/cse7761.h +++ b/esphome/components/cse7761/cse7761.h @@ -28,7 +28,6 @@ class CSE7761Component : public PollingComponent, public uart::UARTDevice { void set_current_2_sensor(sensor::Sensor *current_sensor_2) { current_sensor_2_ = current_sensor_2; } void setup() override; void dump_config() override; - float get_setup_priority() const override; void update() override; protected: diff --git a/esphome/components/cse7766/cse7766.cpp b/esphome/components/cse7766/cse7766.cpp index 4432195365..ce77b62b7b 100644 --- a/esphome/components/cse7766/cse7766.cpp +++ b/esphome/components/cse7766/cse7766.cpp @@ -2,12 +2,11 @@ #include "esphome/core/application.h" #include "esphome/core/helpers.h" #include "esphome/core/log.h" +#include -namespace esphome { -namespace cse7766 { +namespace esphome::cse7766 { static const char *const TAG = "cse7766"; -static constexpr size_t CSE7766_RAW_DATA_SIZE = 24; void CSE7766Component::loop() { const uint32_t now = App.get_loop_component_start_time(); @@ -16,28 +15,41 @@ void CSE7766Component::loop() { this->raw_data_index_ = 0; } - if (this->available() == 0) { + // Early return prevents updating last_transmission_ when no data is available. + size_t avail = this->available(); + if (avail == 0) { return; } this->last_transmission_ = now; - while (this->available() != 0) { - this->read_byte(&this->raw_data_[this->raw_data_index_]); - if (!this->check_byte_()) { - this->raw_data_index_ = 0; - this->status_set_warning(); - continue; - } - if (this->raw_data_index_ == 23) { - this->parse_data_(); - this->status_clear_warning(); + // Read all available bytes in batches to reduce UART call overhead. + // At 4800 baud (~480 bytes/sec) with ~122 Hz loop rate, typically ~4 bytes per call. + uint8_t buf[CSE7766_RAW_DATA_SIZE]; + while (avail > 0) { + size_t to_read = std::min(avail, sizeof(buf)); + if (!this->read_array(buf, to_read)) { + break; } + avail -= to_read; - this->raw_data_index_ = (this->raw_data_index_ + 1) % 24; + for (size_t i = 0; i < to_read; i++) { + this->raw_data_[this->raw_data_index_] = buf[i]; + if (!this->check_byte_()) { + this->raw_data_index_ = 0; + this->status_set_warning(); + continue; + } + + if (this->raw_data_index_ == CSE7766_RAW_DATA_SIZE - 1) { + this->parse_data_(); + this->status_clear_warning(); + } + + this->raw_data_index_ = (this->raw_data_index_ + 1) % CSE7766_RAW_DATA_SIZE; + } } } -float CSE7766Component::get_setup_priority() const { return setup_priority::DATA; } bool CSE7766Component::check_byte_() { uint8_t index = this->raw_data_index_; @@ -54,14 +66,15 @@ bool CSE7766Component::check_byte_() { return true; } - if (index == 23) { + if (index == CSE7766_RAW_DATA_SIZE - 1) { uint8_t checksum = 0; - for (uint8_t i = 2; i < 23; i++) { + for (uint8_t i = 2; i < CSE7766_RAW_DATA_SIZE - 1; i++) { checksum += this->raw_data_[i]; } - if (checksum != this->raw_data_[23]) { - ESP_LOGW(TAG, "Invalid checksum from CSE7766: 0x%02X != 0x%02X", checksum, this->raw_data_[23]); + if (checksum != this->raw_data_[CSE7766_RAW_DATA_SIZE - 1]) { + ESP_LOGW(TAG, "Invalid checksum from CSE7766: 0x%02X != 0x%02X", checksum, + this->raw_data_[CSE7766_RAW_DATA_SIZE - 1]); return false; } return true; @@ -152,6 +165,10 @@ void CSE7766Component::parse_data_() { if (this->power_sensor_ != nullptr) { this->power_sensor_->publish_state(power); } + } else if (this->power_sensor_ != nullptr) { + // No valid power measurement from chip - publish 0W to avoid stale readings + // This typically happens when current is below the measurable threshold (~50mA) + this->power_sensor_->publish_state(0.0f); } float current = 0.0f; @@ -176,12 +193,12 @@ void CSE7766Component::parse_data_() { this->apparent_power_sensor_->publish_state(apparent_power); } if (have_power && this->reactive_power_sensor_ != nullptr) { - const float reactive_power = apparent_power - power; - if (reactive_power < 0.0f) { - ESP_LOGD(TAG, "Impossible reactive power: %.4f is negative", reactive_power); + const float q_squared = apparent_power * apparent_power - power * power; + if (q_squared < 0.0f) { + ESP_LOGD(TAG, "Impossible reactive power: S^2-P^2 is negative (%.4f)", q_squared); this->reactive_power_sensor_->publish_state(0.0f); } else { - this->reactive_power_sensor_->publish_state(reactive_power); + this->reactive_power_sensor_->publish_state(std::sqrt(q_squared)); } } if (this->power_factor_sensor_ != nullptr && (have_power || power_cycle_exceeds_range)) { @@ -241,5 +258,4 @@ void CSE7766Component::dump_config() { this->check_uart_settings(4800, 1, uart::UART_CONFIG_PARITY_EVEN); } -} // namespace cse7766 -} // namespace esphome +} // namespace esphome::cse7766 diff --git a/esphome/components/cse7766/cse7766.h b/esphome/components/cse7766/cse7766.h index 8902eafe3c..77b80dd824 100644 --- a/esphome/components/cse7766/cse7766.h +++ b/esphome/components/cse7766/cse7766.h @@ -5,8 +5,9 @@ #include "esphome/components/sensor/sensor.h" #include "esphome/components/uart/uart.h" -namespace esphome { -namespace cse7766 { +namespace esphome::cse7766 { + +static constexpr size_t CSE7766_RAW_DATA_SIZE = 24; class CSE7766Component : public Component, public uart::UARTDevice { public: @@ -23,7 +24,6 @@ class CSE7766Component : public Component, public uart::UARTDevice { void set_power_factor_sensor(sensor::Sensor *power_factor_sensor) { power_factor_sensor_ = power_factor_sensor; } void loop() override; - float get_setup_priority() const override; void dump_config() override; protected: @@ -34,7 +34,7 @@ class CSE7766Component : public Component, public uart::UARTDevice { this->raw_data_[start_index + 2]); } - uint8_t raw_data_[24]; + uint8_t raw_data_[CSE7766_RAW_DATA_SIZE]; uint8_t raw_data_index_{0}; uint32_t last_transmission_{0}; sensor::Sensor *voltage_sensor_{nullptr}; @@ -48,5 +48,4 @@ class CSE7766Component : public Component, public uart::UARTDevice { uint16_t cf_pulses_last_{0}; }; -} // namespace cse7766 -} // namespace esphome +} // namespace esphome::cse7766 diff --git a/esphome/components/current_based/current_based_cover.cpp b/esphome/components/current_based/current_based_cover.cpp index cb3f65c9cd..13bf11b991 100644 --- a/esphome/components/current_based/current_based_cover.cpp +++ b/esphome/components/current_based/current_based_cover.cpp @@ -37,8 +37,9 @@ void CurrentBasedCover::control(const CoverCall &call) { } } } - if (call.get_position().has_value()) { - auto pos = *call.get_position(); + auto opt_pos = call.get_position(); + if (opt_pos.has_value()) { + auto pos = *opt_pos; if (fabsf(this->position - pos) < 0.01) { // already at target } else { @@ -66,7 +67,7 @@ void CurrentBasedCover::loop() { if (this->current_operation == COVER_OPERATION_OPENING) { if (this->malfunction_detection_ && this->is_closing_()) { // Malfunction this->direction_idle_(); - this->malfunction_trigger_->trigger(); + this->malfunction_trigger_.trigger(); ESP_LOGI(TAG, "'%s' - Malfunction detected during opening. Current flow detected in close circuit", this->name_.c_str()); } else if (this->is_opening_blocked_()) { // Blocked @@ -87,7 +88,7 @@ void CurrentBasedCover::loop() { } else if (this->current_operation == COVER_OPERATION_CLOSING) { if (this->malfunction_detection_ && this->is_opening_()) { // Malfunction this->direction_idle_(); - this->malfunction_trigger_->trigger(); + this->malfunction_trigger_.trigger(); ESP_LOGI(TAG, "'%s' - Malfunction detected during closing. Current flow detected in open circuit", this->name_.c_str()); } else if (this->is_closing_blocked_()) { // Blocked @@ -148,18 +149,17 @@ void CurrentBasedCover::dump_config() { } ESP_LOGCONFIG(TAG, " Close Duration: %.1fs\n" - "Obstacle Rollback: %.1f%%", + " Obstacle Rollback: %.1f%%", this->close_duration_ / 1e3f, this->obstacle_rollback_ * 100); if (this->max_duration_ != UINT32_MAX) { - ESP_LOGCONFIG(TAG, "Maximum duration: %.1fs", this->max_duration_ / 1e3f); + ESP_LOGCONFIG(TAG, " Maximum duration: %.1fs", this->max_duration_ / 1e3f); } ESP_LOGCONFIG(TAG, - "Start sensing delay: %.1fs\n" - "Malfunction detection: %s", + " Start sensing delay: %.1fs\n" + " Malfunction detection: %s", this->start_sensing_delay_ / 1e3f, YESNO(this->malfunction_detection_)); } -float CurrentBasedCover::get_setup_priority() const { return setup_priority::DATA; } void CurrentBasedCover::stop_prev_trigger_() { if (this->prev_command_trigger_ != nullptr) { this->prev_command_trigger_->stop_action(); @@ -221,15 +221,15 @@ void CurrentBasedCover::start_direction_(CoverOperation dir) { Trigger<> *trig; switch (dir) { case COVER_OPERATION_IDLE: - trig = this->stop_trigger_; + trig = &this->stop_trigger_; break; case COVER_OPERATION_OPENING: this->last_operation_ = dir; - trig = this->open_trigger_; + trig = &this->open_trigger_; break; case COVER_OPERATION_CLOSING: this->last_operation_ = dir; - trig = this->close_trigger_; + trig = &this->close_trigger_; break; default: return; diff --git a/esphome/components/current_based/current_based_cover.h b/esphome/components/current_based/current_based_cover.h index b172e762b0..40b39517e4 100644 --- a/esphome/components/current_based/current_based_cover.h +++ b/esphome/components/current_based/current_based_cover.h @@ -14,11 +14,10 @@ class CurrentBasedCover : public cover::Cover, public Component { void setup() override; void loop() override; void dump_config() override; - float get_setup_priority() const override; - Trigger<> *get_stop_trigger() const { return this->stop_trigger_; } + Trigger<> *get_stop_trigger() { return &this->stop_trigger_; } - Trigger<> *get_open_trigger() const { return this->open_trigger_; } + Trigger<> *get_open_trigger() { return &this->open_trigger_; } void set_open_sensor(sensor::Sensor *open_sensor) { this->open_sensor_ = open_sensor; } void set_open_moving_current_threshold(float open_moving_current_threshold) { this->open_moving_current_threshold_ = open_moving_current_threshold; @@ -28,7 +27,7 @@ class CurrentBasedCover : public cover::Cover, public Component { } void set_open_duration(uint32_t open_duration) { this->open_duration_ = open_duration; } - Trigger<> *get_close_trigger() const { return this->close_trigger_; } + Trigger<> *get_close_trigger() { return &this->close_trigger_; } void set_close_sensor(sensor::Sensor *close_sensor) { this->close_sensor_ = close_sensor; } void set_close_moving_current_threshold(float close_moving_current_threshold) { this->close_moving_current_threshold_ = close_moving_current_threshold; @@ -44,7 +43,7 @@ class CurrentBasedCover : public cover::Cover, public Component { void set_malfunction_detection(bool malfunction_detection) { this->malfunction_detection_ = malfunction_detection; } void set_start_sensing_delay(uint32_t start_sensing_delay) { this->start_sensing_delay_ = start_sensing_delay; } - Trigger<> *get_malfunction_trigger() const { return this->malfunction_trigger_; } + Trigger<> *get_malfunction_trigger() { return &this->malfunction_trigger_; } cover::CoverTraits get_traits() override; @@ -64,25 +63,25 @@ class CurrentBasedCover : public cover::Cover, public Component { void recompute_position_(); - Trigger<> *stop_trigger_{new Trigger<>()}; + Trigger<> stop_trigger_; sensor::Sensor *open_sensor_{nullptr}; - Trigger<> *open_trigger_{new Trigger<>()}; - float open_moving_current_threshold_; + Trigger<> open_trigger_; + float open_moving_current_threshold_{0.0f}; float open_obstacle_current_threshold_{FLT_MAX}; - uint32_t open_duration_; + uint32_t open_duration_{0}; sensor::Sensor *close_sensor_{nullptr}; - Trigger<> *close_trigger_{new Trigger<>()}; - float close_moving_current_threshold_; + Trigger<> close_trigger_; + float close_moving_current_threshold_{0.0f}; float close_obstacle_current_threshold_{FLT_MAX}; - uint32_t close_duration_; + uint32_t close_duration_{0}; uint32_t max_duration_{UINT32_MAX}; bool malfunction_detection_{true}; - Trigger<> *malfunction_trigger_{new Trigger<>()}; - uint32_t start_sensing_delay_; - float obstacle_rollback_; + Trigger<> malfunction_trigger_; + uint32_t start_sensing_delay_{0}; + float obstacle_rollback_{0.0f}; Trigger<> *prev_command_trigger_{nullptr}; uint32_t last_recompute_time_{0}; diff --git a/esphome/components/dac7678/dac7678_output.cpp b/esphome/components/dac7678/dac7678_output.cpp index 83f8722e7f..27ab54f0be 100644 --- a/esphome/components/dac7678/dac7678_output.cpp +++ b/esphome/components/dac7678/dac7678_output.cpp @@ -62,6 +62,8 @@ void DAC7678Output::register_channel(DAC7678Channel *channel) { } void DAC7678Output::set_channel_value_(uint8_t channel, uint16_t value) { + if (channel >= std::size(this->dac_input_reg_)) + return; if (this->dac_input_reg_[channel] != value) { ESP_LOGV(TAG, "Channel %01u: input_reg=%04u ", channel, value); diff --git a/esphome/components/daikin/daikin.cpp b/esphome/components/daikin/daikin.cpp index 359c63aeca..a285f3613d 100644 --- a/esphome/components/daikin/daikin.cpp +++ b/esphome/components/daikin/daikin.cpp @@ -94,7 +94,7 @@ uint8_t DaikinClimate::operation_mode_() const { uint16_t DaikinClimate::fan_speed_() const { uint16_t fan_speed; - switch (this->fan_mode.value()) { + switch (this->fan_mode.value_or(climate::CLIMATE_FAN_ON)) { case climate::CLIMATE_FAN_QUIET: fan_speed = DAIKIN_FAN_SILENT << 8; break; diff --git a/esphome/components/daikin_arc/daikin_arc.cpp b/esphome/components/daikin_arc/daikin_arc.cpp index 4726310806..18f12dbfc6 100644 --- a/esphome/components/daikin_arc/daikin_arc.cpp +++ b/esphome/components/daikin_arc/daikin_arc.cpp @@ -91,11 +91,10 @@ void DaikinArcClimate::transmit_state() { remote_state[5] = this->operation_mode_() | 0x08; remote_state[6] = this->temperature_(); remote_state[7] = this->humidity_(); - static uint8_t last_humidity = 0x66; - if (remote_state[7] != last_humidity && this->mode != climate::CLIMATE_MODE_OFF) { + if (remote_state[7] != this->last_humidity_ && this->mode != climate::CLIMATE_MODE_OFF) { ESP_LOGD(TAG, "Set Humditiy: %d, %d\n", (int) this->target_humidity, (int) remote_state[7]); remote_header[9] |= 0x10; - last_humidity = remote_state[7]; + this->last_humidity_ = remote_state[7]; } uint16_t fan_speed = this->fan_speed_(); remote_state[8] = fan_speed >> 8; @@ -176,7 +175,7 @@ uint8_t DaikinArcClimate::operation_mode_() { uint16_t DaikinArcClimate::fan_speed_() { uint16_t fan_speed; - switch (this->fan_mode.value()) { + switch (this->fan_mode.value_or(climate::CLIMATE_FAN_ON)) { case climate::CLIMATE_FAN_LOW: fan_speed = DAIKIN_FAN_1 << 8; break; @@ -350,8 +349,9 @@ bool DaikinArcClimate::on_receive(remote_base::RemoteReceiveData data) { if (data.expect_item(DAIKIN_HEADER_MARK, DAIKIN_HEADER_SPACE)) { valid_daikin_frame = true; size_t bytes_count = data.size() / 2 / 8; - size_t buf_size = bytes_count * 3 + 1; - std::unique_ptr buf(new char[buf_size]()); // value-initialize (zero-fill) + // Header (20) + state (19) = 39 bytes max; truncates gracefully via buf_append_printf + char buf[40 * 3 + 1] = {}; + constexpr size_t buf_size = sizeof(buf); size_t buf_pos = 0; for (size_t i = 0; i < bytes_count; i++) { uint8_t byte = 0; @@ -363,9 +363,9 @@ bool DaikinArcClimate::on_receive(remote_base::RemoteReceiveData data) { break; } } - buf_pos = buf_append_printf(buf.get(), buf_size, buf_pos, "%02x ", byte); + buf_pos = buf_append_printf(buf, buf_size, buf_pos, "%02x ", byte); } - ESP_LOGD(TAG, "WHOLE FRAME %s size: %d", buf.get(), data.size()); + ESP_LOGD(TAG, "WHOLE FRAME %s size: %d", buf, data.size()); } if (!valid_daikin_frame) { char sbuf[16 * 10 + 1] = {0}; @@ -485,8 +485,9 @@ bool DaikinArcClimate::on_receive(remote_base::RemoteReceiveData data) { } void DaikinArcClimate::control(const climate::ClimateCall &call) { - if (call.get_target_humidity().has_value()) { - this->target_humidity = *call.get_target_humidity(); + auto target_humidity = call.get_target_humidity(); + if (target_humidity.has_value()) { + this->target_humidity = *target_humidity; } climate_ir::ClimateIR::control(call); } diff --git a/esphome/components/daikin_arc/daikin_arc.h b/esphome/components/daikin_arc/daikin_arc.h index 6cfffd4725..2b4d4375aa 100644 --- a/esphome/components/daikin_arc/daikin_arc.h +++ b/esphome/components/daikin_arc/daikin_arc.h @@ -70,6 +70,7 @@ class DaikinArcClimate : public climate_ir::ClimateIR { // Handle received IR Buffer bool on_receive(remote_base::RemoteReceiveData data) override; bool parse_state_frame_(const uint8_t frame[]); + uint8_t last_humidity_{0x66}; }; } // namespace daikin_arc diff --git a/esphome/components/daikin_brc/daikin_brc.cpp b/esphome/components/daikin_brc/daikin_brc.cpp index 6683d70f80..1179cb07d7 100644 --- a/esphome/components/daikin_brc/daikin_brc.cpp +++ b/esphome/components/daikin_brc/daikin_brc.cpp @@ -111,7 +111,7 @@ uint8_t DaikinBrcClimate::operation_mode_() { uint8_t DaikinBrcClimate::fan_speed_swing_() { uint16_t fan_speed; - switch (this->fan_mode.value()) { + switch (this->fan_mode.value_or(climate::CLIMATE_FAN_ON)) { case climate::CLIMATE_FAN_LOW: fan_speed = DAIKIN_BRC_FAN_1; break; diff --git a/esphome/components/dallas_temp/dallas_temp.cpp b/esphome/components/dallas_temp/dallas_temp.cpp index 13f2fa59bd..f119e28e78 100644 --- a/esphome/components/dallas_temp/dallas_temp.cpp +++ b/esphome/components/dallas_temp/dallas_temp.cpp @@ -136,6 +136,9 @@ bool DallasTemperatureSensor::check_scratch_pad_() { float DallasTemperatureSensor::get_temp_c_() { int16_t temp = (this->scratch_pad_[1] << 8) | this->scratch_pad_[0]; if ((this->address_ & 0xff) == DALLAS_MODEL_DS18S20) { + if (this->scratch_pad_[7] == 0) { + return NAN; + } return (temp >> 1) + (this->scratch_pad_[7] - this->scratch_pad_[6]) / float(this->scratch_pad_[7]) - 0.25; } switch (this->resolution_) { diff --git a/esphome/components/daly_bms/daly_bms.cpp b/esphome/components/daly_bms/daly_bms.cpp index 2d270cc56e..90ccee78f8 100644 --- a/esphome/components/daly_bms/daly_bms.cpp +++ b/esphome/components/daly_bms/daly_bms.cpp @@ -104,8 +104,6 @@ void DalyBmsComponent::loop() { } } -float DalyBmsComponent::get_setup_priority() const { return setup_priority::DATA; } - void DalyBmsComponent::request_data_(uint8_t data_id) { uint8_t request_message[DALY_FRAME_SIZE]; diff --git a/esphome/components/daly_bms/daly_bms.h b/esphome/components/daly_bms/daly_bms.h index e6d476bcdd..1983ba0ef1 100644 --- a/esphome/components/daly_bms/daly_bms.h +++ b/esphome/components/daly_bms/daly_bms.h @@ -72,7 +72,6 @@ class DalyBmsComponent : public PollingComponent, public uart::UARTDevice { void update() override; void loop() override; - float get_setup_priority() const override; void set_address(uint8_t address) { this->addr_ = address; } protected: diff --git a/esphome/components/daly_bms/sensor.py b/esphome/components/daly_bms/sensor.py index 560bcef64e..aa92cfa86a 100644 --- a/esphome/components/daly_bms/sensor.py +++ b/esphome/components/daly_bms/sensor.py @@ -1,5 +1,6 @@ import esphome.codegen as cg from esphome.components import sensor +from esphome.components.const import ICON_CURRENT_DC, UNIT_AMPERE_HOUR import esphome.config_validation as cv from esphome.const import ( CONF_BATTERY_LEVEL, @@ -55,14 +56,11 @@ CONF_CELL_15_VOLTAGE = "cell_15_voltage" CONF_CELL_16_VOLTAGE = "cell_16_voltage" CONF_CELL_17_VOLTAGE = "cell_17_voltage" CONF_CELL_18_VOLTAGE = "cell_18_voltage" -ICON_CURRENT_DC = "mdi:current-dc" ICON_BATTERY_OUTLINE = "mdi:battery-outline" ICON_THERMOMETER_CHEVRON_UP = "mdi:thermometer-chevron-up" ICON_THERMOMETER_CHEVRON_DOWN = "mdi:thermometer-chevron-down" ICON_CAR_BATTERY = "mdi:car-battery" -UNIT_AMPERE_HOUR = "Ah" - TYPES = [ CONF_VOLTAGE, CONF_CURRENT, diff --git a/esphome/components/datetime/__init__.py b/esphome/components/datetime/__init__.py index 602db3827a..90835624bf 100644 --- a/esphome/components/datetime/__init__.py +++ b/esphome/components/datetime/__init__.py @@ -134,9 +134,8 @@ def datetime_schema(class_: MockObjClass) -> cv.Schema: return _DATETIME_SCHEMA.extend(schema) +@setup_entity("datetime") async def setup_datetime_core_(var, config): - await setup_entity(var, config, "datetime") - if (mqtt_id := config.get(CONF_MQTT_ID)) is not None: mqtt_ = cg.new_Pvariable(mqtt_id, var) await mqtt.register_mqtt_component(mqtt_, config) @@ -188,6 +187,7 @@ async def to_code(config): ), } ), + synchronous=True, ) async def datetime_date_set_to_code(config, action_id, template_arg, args): action_var = cg.new_Pvariable(action_id, template_arg) @@ -219,6 +219,7 @@ async def datetime_date_set_to_code(config, action_id, template_arg, args): ), } ), + synchronous=True, ) async def datetime_time_set_to_code(config, action_id, template_arg, args): action_var = cg.new_Pvariable(action_id, template_arg) @@ -250,6 +251,7 @@ async def datetime_time_set_to_code(config, action_id, template_arg, args): ), }, ), + synchronous=True, ) async def datetime_datetime_set_to_code(config, action_id, template_arg, args): action_var = cg.new_Pvariable(action_id, template_arg) diff --git a/esphome/components/datetime/date_entity.cpp b/esphome/components/datetime/date_entity.cpp index 3ba488c0aa..997aec3f69 100644 --- a/esphome/components/datetime/date_entity.cpp +++ b/esphome/components/datetime/date_entity.cpp @@ -30,7 +30,7 @@ void DateEntity::publish_state() { return; } this->set_has_state(true); - ESP_LOGD(TAG, "'%s' >> %d-%d-%d", this->get_name().c_str(), this->year_, this->month_, this->day_); + ESP_LOGV(TAG, "'%s' >> %d-%d-%d", this->get_name().c_str(), this->year_, this->month_, this->day_); this->state_callback_.call(); #if defined(USE_DATETIME_DATE) && defined(USE_CONTROLLER_REGISTRY) ControllerRegistry::notify_date_update(this); @@ -83,16 +83,16 @@ void DateCall::validate_() { void DateCall::perform() { this->validate_(); - ESP_LOGD(TAG, "'%s' - Setting", this->parent_->get_name().c_str()); + ESP_LOGV(TAG, "'%s' - Setting", this->parent_->get_name().c_str()); if (this->year_.has_value()) { - ESP_LOGD(TAG, " Year: %d", *this->year_); + ESP_LOGV(TAG, " Year: %d", *this->year_); } if (this->month_.has_value()) { - ESP_LOGD(TAG, " Month: %d", *this->month_); + ESP_LOGV(TAG, " Month: %d", *this->month_); } if (this->day_.has_value()) { - ESP_LOGD(TAG, " Day: %d", *this->day_); + ESP_LOGV(TAG, " Day: %d", *this->day_); } this->parent_->control(*this); } diff --git a/esphome/components/datetime/date_entity.h b/esphome/components/datetime/date_entity.h index cbf2b85506..8233e809a1 100644 --- a/esphome/components/datetime/date_entity.h +++ b/esphome/components/datetime/date_entity.h @@ -91,7 +91,7 @@ class DateCall { DateEntity *parent_; - optional year_; + optional year_; optional month_; optional day_; }; diff --git a/esphome/components/datetime/datetime_base.h b/esphome/components/datetime/datetime_base.h index 1b0b3d5463..6c0a33c842 100644 --- a/esphome/components/datetime/datetime_base.h +++ b/esphome/components/datetime/datetime_base.h @@ -14,7 +14,9 @@ class DateTimeBase : public EntityBase { public: virtual ESPTime state_as_esptime() const = 0; - void add_on_state_callback(std::function &&callback) { this->state_callback_.add(std::move(callback)); } + template void add_on_state_callback(F &&callback) { + this->state_callback_.add(std::forward(callback)); + } #ifdef USE_TIME void set_rtc(time::RealTimeClock *rtc) { this->rtc_ = rtc; } @@ -31,9 +33,12 @@ class DateTimeBase : public EntityBase { class DateTimeStateTrigger : public Trigger { public: - explicit DateTimeStateTrigger(DateTimeBase *parent) { - parent->add_on_state_callback([this, parent]() { this->trigger(parent->state_as_esptime()); }); + explicit DateTimeStateTrigger(DateTimeBase *parent) : parent_(parent) { + parent->add_on_state_callback([this]() { this->trigger(this->parent_->state_as_esptime()); }); } + + protected: + DateTimeBase *parent_; }; } // namespace esphome::datetime diff --git a/esphome/components/datetime/datetime_entity.cpp b/esphome/components/datetime/datetime_entity.cpp index 730abb3ca8..a8e00d6eb3 100644 --- a/esphome/components/datetime/datetime_entity.cpp +++ b/esphome/components/datetime/datetime_entity.cpp @@ -45,7 +45,7 @@ void DateTimeEntity::publish_state() { return; } this->set_has_state(true); - ESP_LOGD(TAG, "'%s' >> %04u-%02u-%02u %02d:%02d:%02d", this->get_name().c_str(), this->year_, this->month_, + ESP_LOGV(TAG, "'%s' >> %04u-%02u-%02u %02d:%02d:%02d", this->get_name().c_str(), this->year_, this->month_, this->day_, this->hour_, this->minute_, this->second_); this->state_callback_.call(); #if defined(USE_DATETIME_DATETIME) && defined(USE_CONTROLLER_REGISTRY) @@ -60,6 +60,9 @@ ESPTime DateTimeEntity::state_as_esptime() const { obj.year = this->year_; obj.month = this->month_; obj.day_of_month = this->day_; + obj.day_of_week = 0; + obj.day_of_year = 0; + obj.is_dst = false; obj.hour = this->hour_; obj.minute = this->minute_; obj.second = this->second_; @@ -124,25 +127,25 @@ void DateTimeCall::validate_() { void DateTimeCall::perform() { this->validate_(); - ESP_LOGD(TAG, "'%s' - Setting", this->parent_->get_name().c_str()); + ESP_LOGV(TAG, "'%s' - Setting", this->parent_->get_name().c_str()); if (this->year_.has_value()) { - ESP_LOGD(TAG, " Year: %d", *this->year_); + ESP_LOGV(TAG, " Year: %d", *this->year_); } if (this->month_.has_value()) { - ESP_LOGD(TAG, " Month: %d", *this->month_); + ESP_LOGV(TAG, " Month: %d", *this->month_); } if (this->day_.has_value()) { - ESP_LOGD(TAG, " Day: %d", *this->day_); + ESP_LOGV(TAG, " Day: %d", *this->day_); } if (this->hour_.has_value()) { - ESP_LOGD(TAG, " Hour: %d", *this->hour_); + ESP_LOGV(TAG, " Hour: %d", *this->hour_); } if (this->minute_.has_value()) { - ESP_LOGD(TAG, " Minute: %d", *this->minute_); + ESP_LOGV(TAG, " Minute: %d", *this->minute_); } if (this->second_.has_value()) { - ESP_LOGD(TAG, " Second: %d", *this->second_); + ESP_LOGV(TAG, " Second: %d", *this->second_); } this->parent_->control(*this); } diff --git a/esphome/components/datetime/time_entity.cpp b/esphome/components/datetime/time_entity.cpp index 74e43fbbe7..1cc9eaf2fb 100644 --- a/esphome/components/datetime/time_entity.cpp +++ b/esphome/components/datetime/time_entity.cpp @@ -26,7 +26,7 @@ void TimeEntity::publish_state() { return; } this->set_has_state(true); - ESP_LOGD(TAG, "'%s' >> %02d:%02d:%02d", this->get_name().c_str(), this->hour_, this->minute_, this->second_); + ESP_LOGV(TAG, "'%s' >> %02d:%02d:%02d", this->get_name().c_str(), this->hour_, this->minute_, this->second_); this->state_callback_.call(); #if defined(USE_DATETIME_TIME) && defined(USE_CONTROLLER_REGISTRY) ControllerRegistry::notify_time_update(this); @@ -52,15 +52,15 @@ void TimeCall::validate_() { void TimeCall::perform() { this->validate_(); - ESP_LOGD(TAG, "'%s' - Setting", this->parent_->get_name().c_str()); + ESP_LOGV(TAG, "'%s' - Setting", this->parent_->get_name().c_str()); if (this->hour_.has_value()) { - ESP_LOGD(TAG, " Hour: %d", *this->hour_); + ESP_LOGV(TAG, " Hour: %d", *this->hour_); } if (this->minute_.has_value()) { - ESP_LOGD(TAG, " Minute: %d", *this->minute_); + ESP_LOGV(TAG, " Minute: %d", *this->minute_); } if (this->second_.has_value()) { - ESP_LOGD(TAG, " Second: %d", *this->second_); + ESP_LOGV(TAG, " Second: %d", *this->second_); } this->parent_->control(*this); } diff --git a/esphome/components/debug/debug_component.h b/esphome/components/debug/debug_component.h index e4f4bb36eb..3da6b800c6 100644 --- a/esphome/components/debug/debug_component.h +++ b/esphome/components/debug/debug_component.h @@ -18,6 +18,7 @@ namespace debug { static constexpr size_t DEVICE_INFO_BUFFER_SIZE = 256; static constexpr size_t RESET_REASON_BUFFER_SIZE = 128; +static constexpr size_t WAKEUP_CAUSE_BUFFER_SIZE = 128; // buf_append_printf is now provided by esphome/core/helpers.h @@ -94,7 +95,7 @@ class DebugComponent : public PollingComponent { #endif // USE_TEXT_SENSOR const char *get_reset_reason_(std::span buffer); - const char *get_wakeup_cause_(std::span buffer); + const char *get_wakeup_cause_(std::span buffer); uint32_t get_free_heap_(); size_t get_device_info_(std::span buffer, size_t pos); void update_platform_(); diff --git a/esphome/components/debug/debug_esp32.cpp b/esphome/components/debug/debug_esp32.cpp index aad4c7426c..2e04090749 100644 --- a/esphome/components/debug/debug_esp32.cpp +++ b/esphome/components/debug/debug_esp32.cpp @@ -5,6 +5,7 @@ #include "esphome/core/log.h" #include "esphome/core/hal.h" #include +#include #include #include @@ -48,7 +49,8 @@ static const size_t REBOOT_MAX_LEN = 24; void DebugComponent::on_shutdown() { auto *component = App.get_current_component(); char buffer[REBOOT_MAX_LEN]{}; - auto pref = global_preferences->make_preference(REBOOT_MAX_LEN, fnv1_hash(REBOOT_KEY + App.get_name())); + auto pref = global_preferences->make_preference(REBOOT_MAX_LEN, + fnv1_hash_extend(fnv1_hash(REBOOT_KEY), App.get_name().c_str())); if (component != nullptr) { strncpy(buffer, LOG_STR_ARG(component->get_component_log_str()), REBOOT_MAX_LEN - 1); buffer[REBOOT_MAX_LEN - 1] = '\0'; @@ -65,7 +67,8 @@ const char *DebugComponent::get_reset_reason_(std::spanmake_preference(REBOOT_MAX_LEN, fnv1_hash(REBOOT_KEY + App.get_name())); + auto pref = global_preferences->make_preference(REBOOT_MAX_LEN, + fnv1_hash_extend(fnv1_hash(REBOOT_KEY), App.get_name().c_str())); char reboot_source[REBOOT_MAX_LEN]{}; if (pref.load(&reboot_source)) { reboot_source[REBOOT_MAX_LEN - 1] = '\0'; @@ -79,37 +82,77 @@ const char *DebugComponent::get_reset_reason_(std::span= ESP_IDF_VERSION_VAL(6, 0, 0) static const char *const WAKEUP_CAUSES[] = { - "undefined", - "undefined", - "external signal using RTC_IO", - "external signal using RTC_CNTL", - "timer", - "touchpad", - "ULP program", - "GPIO", - "UART", - "WIFI", - "COCPU int", - "COCPU crash", - "BT", + "undefined", // ESP_SLEEP_WAKEUP_UNDEFINED (0) + "undefined", // ESP_SLEEP_WAKEUP_ALL (1) + "external signal using RTC_IO", // ESP_SLEEP_WAKEUP_EXT0 (2) + "external signal using RTC_CNTL", // ESP_SLEEP_WAKEUP_EXT1 (3) + "timer", // ESP_SLEEP_WAKEUP_TIMER (4) + "touchpad", // ESP_SLEEP_WAKEUP_TOUCHPAD (5) + "ULP program", // ESP_SLEEP_WAKEUP_ULP (6) + "GPIO", // ESP_SLEEP_WAKEUP_GPIO (7) + "UART", // ESP_SLEEP_WAKEUP_UART (8) + "UART1", // ESP_SLEEP_WAKEUP_UART1 (9) + "UART2", // ESP_SLEEP_WAKEUP_UART2 (10) + "WIFI", // ESP_SLEEP_WAKEUP_WIFI (11) + "COCPU int", // ESP_SLEEP_WAKEUP_COCPU (12) + "COCPU crash", // ESP_SLEEP_WAKEUP_COCPU_TRAP_TRIG (13) + "BT", // ESP_SLEEP_WAKEUP_BT (14) + "VAD", // ESP_SLEEP_WAKEUP_VAD (15) + "VBAT under voltage", // ESP_SLEEP_WAKEUP_VBAT_UNDER_VOLT (16) }; +#else +static const char *const WAKEUP_CAUSES[] = { + "undefined", // ESP_SLEEP_WAKEUP_UNDEFINED (0) + "undefined", // ESP_SLEEP_WAKEUP_ALL (1) + "external signal using RTC_IO", // ESP_SLEEP_WAKEUP_EXT0 (2) + "external signal using RTC_CNTL", // ESP_SLEEP_WAKEUP_EXT1 (3) + "timer", // ESP_SLEEP_WAKEUP_TIMER (4) + "touchpad", // ESP_SLEEP_WAKEUP_TOUCHPAD (5) + "ULP program", // ESP_SLEEP_WAKEUP_ULP (6) + "GPIO", // ESP_SLEEP_WAKEUP_GPIO (7) + "UART", // ESP_SLEEP_WAKEUP_UART (8) + "WIFI", // ESP_SLEEP_WAKEUP_WIFI (9) + "COCPU int", // ESP_SLEEP_WAKEUP_COCPU (10) + "COCPU crash", // ESP_SLEEP_WAKEUP_COCPU_TRAP_TRIG (11) + "BT", // ESP_SLEEP_WAKEUP_BT (12) +}; +#endif -const char *DebugComponent::get_wakeup_cause_(std::span buffer) { - const char *wake_reason; - unsigned reason = esp_sleep_get_wakeup_cause(); - if (reason < sizeof(WAKEUP_CAUSES) / sizeof(WAKEUP_CAUSES[0])) { - wake_reason = WAKEUP_CAUSES[reason]; - } else { - wake_reason = "unknown source"; +const char *DebugComponent::get_wakeup_cause_(std::span buffer) { + static constexpr auto NUM_CAUSES = sizeof(WAKEUP_CAUSES) / sizeof(WAKEUP_CAUSES[0]); +#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(6, 0, 0) + // IDF 6.0+ returns a bitmap of all wakeup sources + uint32_t causes = esp_sleep_get_wakeup_causes(); + if (causes == 0) { + return WAKEUP_CAUSES[0]; // "undefined" } - ESP_LOGD(TAG, "Wakeup Reason: %s", wake_reason); - // Return the static string directly - no need to copy to buffer - return wake_reason; + char *p = buffer.data(); + char *end = p + buffer.size(); + *p = '\0'; + const char *sep = ""; + for (unsigned i = 0; i < NUM_CAUSES && p < end; i++) { + if (causes & (1U << i)) { + size_t needed = strlen(sep) + strlen(WAKEUP_CAUSES[i]); + if (p + needed >= end) { + break; + } + p += snprintf(p, end - p, "%s%s", sep, WAKEUP_CAUSES[i]); + sep = ", "; + } + } + return buffer.data(); +#else + unsigned reason = esp_sleep_get_wakeup_cause(); + if (reason < NUM_CAUSES) { + return WAKEUP_CAUSES[reason]; + } + return "unknown source"; +#endif } void DebugComponent::log_partition_info_() { @@ -172,7 +215,6 @@ size_t DebugComponent::get_device_info_(std::span } uint32_t flash_size = ESP.getFlashChipSize() / 1024; // NOLINT uint32_t flash_speed = ESP.getFlashChipSpeed() / 1000000; // NOLINT - ESP_LOGD(TAG, "Flash Chip: Size=%" PRIu32 "kB Speed=%" PRIu32 "MHz Mode=%s", flash_size, flash_speed, flash_mode); pos = buf_append_printf(buf, size, pos, "|Flash: %" PRIu32 "kB Speed:%" PRIu32 "MHz Mode:%s", flash_size, flash_speed, flash_mode); #endif @@ -194,39 +236,47 @@ size_t DebugComponent::get_device_info_(std::span if (info.features != 0) { pos = buf_append_printf(buf, size, pos, "%sOther:0x%" PRIx32, first_feature ? "" : ", ", info.features); } - ESP_LOGD(TAG, "Chip: Model=%s, Cores=%u, Revision=%u", model, info.cores, info.revision); pos = buf_append_printf(buf, size, pos, " Cores:%u Revision:%u", info.cores, info.revision); uint32_t cpu_freq_mhz = arch_get_cpu_freq_hz() / 1000000; - ESP_LOGD(TAG, "CPU Frequency: %" PRIu32 " MHz", cpu_freq_mhz); pos = buf_append_printf(buf, size, pos, "|CPU Frequency: %" PRIu32 " MHz", cpu_freq_mhz); - // Framework detection -#ifdef USE_ARDUINO - ESP_LOGD(TAG, "Framework: Arduino"); - pos = buf_append_printf(buf, size, pos, "|Framework: Arduino"); -#elif defined(USE_ESP32) - ESP_LOGD(TAG, "Framework: ESP-IDF"); - pos = buf_append_printf(buf, size, pos, "|Framework: ESP-IDF"); -#else - ESP_LOGW(TAG, "Framework: UNKNOWN"); - pos = buf_append_printf(buf, size, pos, "|Framework: UNKNOWN"); -#endif - - ESP_LOGD(TAG, "ESP-IDF Version: %s", esp_get_idf_version()); - pos = buf_append_printf(buf, size, pos, "|ESP-IDF: %s", esp_get_idf_version()); + char reset_buffer[RESET_REASON_BUFFER_SIZE]; + char wakeup_buffer[WAKEUP_CAUSE_BUFFER_SIZE]; + const char *reset_reason = get_reset_reason_(std::span(reset_buffer)); + const char *wakeup_cause = get_wakeup_cause_(std::span(wakeup_buffer)); uint8_t mac[6]; get_mac_address_raw(mac); - ESP_LOGD(TAG, "EFuse MAC: %02X:%02X:%02X:%02X:%02X:%02X", mac[0], mac[1], mac[2], mac[3], mac[4], mac[5]); + + ESP_LOGD(TAG, + "ESP32 debug info:\n" + " Chip: %s\n" + " Cores: %u\n" + " Revision: %u\n" + " CPU Frequency: %" PRIu32 " MHz\n" + " ESP-IDF Version: %s\n" + " EFuse MAC: %02X:%02X:%02X:%02X:%02X:%02X\n" + " Reset Reason: %s\n" + " Wakeup Cause: %s", + model, info.cores, info.revision, cpu_freq_mhz, esp_get_idf_version(), mac[0], mac[1], mac[2], mac[3], + mac[4], mac[5], reset_reason, wakeup_cause); +#if defined(USE_ARDUINO) + ESP_LOGD(TAG, " Flash: Size=%" PRIu32 "kB Speed=%" PRIu32 "MHz Mode=%s", flash_size, flash_speed, flash_mode); +#endif + // Framework detection +#ifdef USE_ARDUINO + ESP_LOGD(TAG, " Framework: Arduino"); + pos = buf_append_printf(buf, size, pos, "|Framework: Arduino"); +#else + ESP_LOGD(TAG, " Framework: ESP-IDF"); + pos = buf_append_printf(buf, size, pos, "|Framework: ESP-IDF"); +#endif + + pos = buf_append_printf(buf, size, pos, "|ESP-IDF: %s", esp_get_idf_version()); pos = buf_append_printf(buf, size, pos, "|EFuse MAC: %02X:%02X:%02X:%02X:%02X:%02X", mac[0], mac[1], mac[2], mac[3], mac[4], mac[5]); - - char reason_buffer[RESET_REASON_BUFFER_SIZE]; - const char *reset_reason = get_reset_reason_(std::span(reason_buffer)); pos = buf_append_printf(buf, size, pos, "|Reset: %s", reset_reason); - - const char *wakeup_cause = get_wakeup_cause_(std::span(reason_buffer)); pos = buf_append_printf(buf, size, pos, "|Wakeup: %s", wakeup_cause); return pos; diff --git a/esphome/components/debug/debug_esp8266.cpp b/esphome/components/debug/debug_esp8266.cpp index a4b6468b49..0519ab72fe 100644 --- a/esphome/components/debug/debug_esp8266.cpp +++ b/esphome/components/debug/debug_esp8266.cpp @@ -1,6 +1,7 @@ #include "debug_component.h" #ifdef USE_ESP8266 #include "esphome/core/log.h" +#include "esphome/core/progmem.h" #include extern "C" { @@ -19,27 +20,38 @@ namespace debug { static const char *const TAG = "debug"; +// PROGMEM string table for reset reasons, indexed by reason code (0-6), with "Unknown" as fallback +// clang-format off +PROGMEM_STRING_TABLE(ResetReasonStrings, + "Power On", // 0 = REASON_DEFAULT_RST + "Hardware Watchdog", // 1 = REASON_WDT_RST + "Exception", // 2 = REASON_EXCEPTION_RST + "Software Watchdog", // 3 = REASON_SOFT_WDT_RST + "Software/System restart", // 4 = REASON_SOFT_RESTART + "Deep-Sleep Wake", // 5 = REASON_DEEP_SLEEP_AWAKE + "External System", // 6 = REASON_EXT_SYS_RST + "Unknown" // 7 = fallback +); +// clang-format on +static_assert(REASON_DEFAULT_RST == 0, "Reset reason enum values must match table indices"); +static_assert(REASON_WDT_RST == 1, "Reset reason enum values must match table indices"); +static_assert(REASON_EXCEPTION_RST == 2, "Reset reason enum values must match table indices"); +static_assert(REASON_SOFT_WDT_RST == 3, "Reset reason enum values must match table indices"); +static_assert(REASON_SOFT_RESTART == 4, "Reset reason enum values must match table indices"); +static_assert(REASON_DEEP_SLEEP_AWAKE == 5, "Reset reason enum values must match table indices"); +static_assert(REASON_EXT_SYS_RST == 6, "Reset reason enum values must match table indices"); + +// PROGMEM string table for flash chip modes, indexed by mode code (0-3), with "UNKNOWN" as fallback +PROGMEM_STRING_TABLE(FlashModeStrings, "QIO", "QOUT", "DIO", "DOUT", "UNKNOWN"); +static_assert(FM_QIO == 0, "Flash mode enum values must match table indices"); +static_assert(FM_QOUT == 1, "Flash mode enum values must match table indices"); +static_assert(FM_DIO == 2, "Flash mode enum values must match table indices"); +static_assert(FM_DOUT == 3, "Flash mode enum values must match table indices"); + // Get reset reason string from reason code (no heap allocation) // Returns LogString* pointing to flash (PROGMEM) on ESP8266 static const LogString *get_reset_reason_str(uint32_t reason) { - switch (reason) { - case REASON_DEFAULT_RST: - return LOG_STR("Power On"); - case REASON_WDT_RST: - return LOG_STR("Hardware Watchdog"); - case REASON_EXCEPTION_RST: - return LOG_STR("Exception"); - case REASON_SOFT_WDT_RST: - return LOG_STR("Software Watchdog"); - case REASON_SOFT_RESTART: - return LOG_STR("Software/System restart"); - case REASON_DEEP_SLEEP_AWAKE: - return LOG_STR("Deep-Sleep Wake"); - case REASON_EXT_SYS_RST: - return LOG_STR("External System"); - default: - return LOG_STR("Unknown"); - } + return ResetReasonStrings::get_log_str(static_cast(reason), ResetReasonStrings::LAST_INDEX); } // Size for core version hex buffer @@ -79,7 +91,7 @@ const char *DebugComponent::get_reset_reason_(std::span buffer) { +const char *DebugComponent::get_wakeup_cause_(std::span buffer) { // ESP8266 doesn't have detailed wakeup cause like ESP32 return ""; } @@ -92,23 +104,9 @@ size_t DebugComponent::get_device_info_(std::span constexpr size_t size = DEVICE_INFO_BUFFER_SIZE; char *buf = buffer.data(); - const LogString *flash_mode; - switch (ESP.getFlashChipMode()) { // NOLINT(readability-static-accessed-through-instance) - case FM_QIO: - flash_mode = LOG_STR("QIO"); - break; - case FM_QOUT: - flash_mode = LOG_STR("QOUT"); - break; - case FM_DIO: - flash_mode = LOG_STR("DIO"); - break; - case FM_DOUT: - flash_mode = LOG_STR("DOUT"); - break; - default: - flash_mode = LOG_STR("UNKNOWN"); - } + const LogString *flash_mode = FlashModeStrings::get_log_str( + static_cast(ESP.getFlashChipMode()), // NOLINT(readability-static-accessed-through-instance) + FlashModeStrings::LAST_INDEX); uint32_t flash_size = ESP.getFlashChipSize() / 1024; // NOLINT(readability-static-accessed-through-instance) uint32_t flash_speed = ESP.getFlashChipSpeed() / 1000000; // NOLINT(readability-static-accessed-through-instance) ESP_LOGD(TAG, "Flash Chip: Size=%" PRIu32 "kB Speed=%" PRIu32 "MHz Mode=%s", flash_size, flash_speed, @@ -130,14 +128,16 @@ size_t DebugComponent::get_device_info_(std::span // NOLINTEND(readability-static-accessed-through-instance) ESP_LOGD(TAG, - "Chip ID: 0x%08" PRIX32 "\n" - "SDK Version: %s\n" - "Core Version: %s\n" - "Boot Version=%u Mode=%u\n" - "CPU Frequency: %u\n" - "Flash Chip ID=0x%08" PRIX32 "\n" - "Reset Reason: %s\n" - "Reset Info: %s", + "ESP8266 debug info:\n" + " Chip ID: 0x%08" PRIX32 "\n" + " SDK Version: %s\n" + " Core Version: %s\n" + " Boot Version: %u\n" + " Boot Mode: %u\n" + " CPU Frequency: %u\n" + " Flash Chip ID: 0x%08" PRIX32 "\n" + " Reset Reason: %s\n" + " Reset Info: %s", chip_id, sdk_version, get_core_version_str(core_version_buffer), boot_version, boot_mode, cpu_freq, flash_chip_id, reset_reason, get_reset_info_str(reset_info_buffer, resetInfo.reason)); diff --git a/esphome/components/debug/debug_host.cpp b/esphome/components/debug/debug_host.cpp index 2fa88f0909..0dfab86e4c 100644 --- a/esphome/components/debug/debug_host.cpp +++ b/esphome/components/debug/debug_host.cpp @@ -7,7 +7,7 @@ namespace debug { const char *DebugComponent::get_reset_reason_(std::span buffer) { return ""; } -const char *DebugComponent::get_wakeup_cause_(std::span buffer) { return ""; } +const char *DebugComponent::get_wakeup_cause_(std::span buffer) { return ""; } uint32_t DebugComponent::get_free_heap_() { return INT_MAX; } diff --git a/esphome/components/debug/debug_libretiny.cpp b/esphome/components/debug/debug_libretiny.cpp index 14bbdb945a..1d458c602a 100644 --- a/esphome/components/debug/debug_libretiny.cpp +++ b/esphome/components/debug/debug_libretiny.cpp @@ -12,7 +12,7 @@ const char *DebugComponent::get_reset_reason_(std::span buffer) { return ""; } +const char *DebugComponent::get_wakeup_cause_(std::span buffer) { return ""; } uint32_t DebugComponent::get_free_heap_() { return lt_heap_get_free(); } @@ -27,12 +27,14 @@ size_t DebugComponent::get_device_info_(std::span uint32_t mac_id = lt_cpu_get_mac_id(); ESP_LOGD(TAG, - "LibreTiny Version: %s\n" - "Chip: %s (%04x) @ %u MHz\n" - "Chip ID: 0x%06" PRIX32 "\n" - "Board: %s\n" - "Flash: %" PRIu32 " KiB / RAM: %" PRIu32 " KiB\n" - "Reset Reason: %s", + "LibreTiny debug info:\n" + " Version: %s\n" + " Chip: %s (%04x) @ %u MHz\n" + " Chip ID: 0x%06" PRIX32 "\n" + " Board: %s\n" + " Flash: %" PRIu32 " KiB\n" + " RAM: %" PRIu32 " KiB\n" + " Reset Reason: %s", lt_get_version(), lt_cpu_get_model_name(), lt_cpu_get_model(), lt_cpu_get_freq_mhz(), mac_id, lt_get_board_code(), flash_kib, ram_kib, reset_reason); diff --git a/esphome/components/debug/debug_rp2040.cpp b/esphome/components/debug/debug_rp2040.cpp index c9d41942db..73f08492c8 100644 --- a/esphome/components/debug/debug_rp2040.cpp +++ b/esphome/components/debug/debug_rp2040.cpp @@ -1,23 +1,81 @@ #include "debug_component.h" #ifdef USE_RP2040 +#include "esphome/core/defines.h" #include "esphome/core/log.h" #include +#include +#if defined(PICO_RP2350) +#include +#else +#include +#endif +#ifdef USE_RP2040_CRASH_HANDLER +#include "esphome/components/rp2040/crash_handler.h" +#endif namespace esphome { namespace debug { static const char *const TAG = "debug"; -const char *DebugComponent::get_reset_reason_(std::span buffer) { return ""; } +const char *DebugComponent::get_reset_reason_(std::span buffer) { + char *buf = buffer.data(); + const size_t size = RESET_REASON_BUFFER_SIZE; + size_t pos = 0; -const char *DebugComponent::get_wakeup_cause_(std::span buffer) { return ""; } +#if defined(PICO_RP2350) + uint32_t chip_reset = powman_hw->chip_reset; + if (chip_reset & 0x04000000) // HAD_GLITCH_DETECT + pos = buf_append_str(buf, size, pos, "Power supply glitch|"); + if (chip_reset & 0x00040000) // HAD_RUN_LOW + pos = buf_append_str(buf, size, pos, "RUN pin|"); + if (chip_reset & 0x00020000) // HAD_BOR + pos = buf_append_str(buf, size, pos, "Brown-out|"); + if (chip_reset & 0x00010000) // HAD_POR + pos = buf_append_str(buf, size, pos, "Power-on reset|"); +#else + uint32_t chip_reset = vreg_and_chip_reset_hw->chip_reset; + if (chip_reset & 0x00010000) // HAD_RUN + pos = buf_append_str(buf, size, pos, "RUN pin|"); + if (chip_reset & 0x00000100) // HAD_POR + pos = buf_append_str(buf, size, pos, "Power-on reset|"); +#endif -uint32_t DebugComponent::get_free_heap_() { return rp2040.getFreeHeap(); } + if (watchdog_caused_reboot()) { + bool handled = false; +#ifdef USE_RP2040_CRASH_HANDLER + if (rp2040::crash_handler_has_data()) { + pos = buf_append_str(buf, size, pos, "Crash (HardFault)|"); + handled = true; + } +#endif + if (!handled) { + if (watchdog_enable_caused_reboot()) { + pos = buf_append_str(buf, size, pos, "Watchdog timeout|"); + } else { + pos = buf_append_str(buf, size, pos, "Software reset|"); + } + } + } + + // Remove trailing '|' + if (pos > 0 && buf[pos - 1] == '|') { + buf[pos - 1] = '\0'; + } else if (pos == 0) { + return "Unknown"; + } + + return buf; +} + +const char *DebugComponent::get_wakeup_cause_(std::span buffer) { return ""; } + +uint32_t DebugComponent::get_free_heap_() { return ::rp2040.getFreeHeap(); } size_t DebugComponent::get_device_info_(std::span buffer, size_t pos) { constexpr size_t size = DEVICE_INFO_BUFFER_SIZE; char *buf = buffer.data(); - uint32_t cpu_freq = rp2040.f_cpu(); + uint32_t cpu_freq = ::rp2040.f_cpu(); ESP_LOGD(TAG, "CPU Frequency: %" PRIu32, cpu_freq); pos = buf_append_printf(buf, size, pos, "|CPU Frequency: %" PRIu32, cpu_freq); diff --git a/esphome/components/debug/debug_zephyr.cpp b/esphome/components/debug/debug_zephyr.cpp index 0291cc3061..d1580dae80 100644 --- a/esphome/components/debug/debug_zephyr.cpp +++ b/esphome/components/debug/debug_zephyr.cpp @@ -2,6 +2,7 @@ #ifdef USE_ZEPHYR #include #include "esphome/core/log.h" +#include #include #include #include @@ -15,16 +16,6 @@ static const char *const TAG = "debug"; constexpr std::uintptr_t MBR_PARAM_PAGE_ADDR = 0xFFC; constexpr std::uintptr_t MBR_BOOTLOADER_ADDR = 0xFF8; -static size_t append_reset_reason(char *buf, size_t size, size_t pos, bool set, const char *reason) { - if (!set) { - return pos; - } - if (pos > 0) { - pos = buf_append_printf(buf, size, pos, ", "); - } - return buf_append_printf(buf, size, pos, "%s", reason); -} - static inline uint32_t read_mem_u32(uintptr_t addr) { return *reinterpret_cast(addr); // NOLINT(performance-no-int-to-ptr) } @@ -57,44 +48,12 @@ static inline uint32_t sd_version_get() { } const char *DebugComponent::get_reset_reason_(std::span buffer) { - char *buf = buffer.data(); - const size_t size = RESET_REASON_BUFFER_SIZE; - - uint32_t cause; - auto ret = hwinfo_get_reset_cause(&cause); - if (ret) { - ESP_LOGE(TAG, "Unable to get reset cause: %d", ret); - buf[0] = '\0'; - return buf; - } - size_t pos = 0; - - pos = append_reset_reason(buf, size, pos, cause & RESET_PIN, "External pin"); - pos = append_reset_reason(buf, size, pos, cause & RESET_SOFTWARE, "Software reset"); - pos = append_reset_reason(buf, size, pos, cause & RESET_BROWNOUT, "Brownout (drop in voltage)"); - pos = append_reset_reason(buf, size, pos, cause & RESET_POR, "Power-on reset (POR)"); - pos = append_reset_reason(buf, size, pos, cause & RESET_WATCHDOG, "Watchdog timer expiration"); - pos = append_reset_reason(buf, size, pos, cause & RESET_DEBUG, "Debug event"); - pos = append_reset_reason(buf, size, pos, cause & RESET_SECURITY, "Security violation"); - pos = append_reset_reason(buf, size, pos, cause & RESET_LOW_POWER_WAKE, "Waking up from low power mode"); - pos = append_reset_reason(buf, size, pos, cause & RESET_CPU_LOCKUP, "CPU lock-up detected"); - pos = append_reset_reason(buf, size, pos, cause & RESET_PARITY, "Parity error"); - pos = append_reset_reason(buf, size, pos, cause & RESET_PLL, "PLL error"); - pos = append_reset_reason(buf, size, pos, cause & RESET_CLOCK, "Clock error"); - pos = append_reset_reason(buf, size, pos, cause & RESET_HARDWARE, "Hardware reset"); - pos = append_reset_reason(buf, size, pos, cause & RESET_USER, "User reset"); - pos = append_reset_reason(buf, size, pos, cause & RESET_TEMPERATURE, "Temperature reset"); - - // Ensure null termination if nothing was written - if (pos == 0) { - buf[0] = '\0'; - } - + const char *buf = zephyr::get_reset_reason(buffer); ESP_LOGD(TAG, "Reset Reason: %s", buf); return buf; } -const char *DebugComponent::get_wakeup_cause_(std::span buffer) { +const char *DebugComponent::get_wakeup_cause_(std::span buffer) { // Zephyr doesn't have detailed wakeup cause like ESP32 return ""; } @@ -120,18 +79,61 @@ static void fa_cb(const struct flash_area *fa, void *user_data) { void DebugComponent::log_partition_info_() { #if CONFIG_FLASH_MAP_LABELS ESP_LOGCONFIG(TAG, "ID | Device | Device Name " - "| Label | Offset | Size\n" - "--------------------------------------------" + "| Label | Offset | Size"); + ESP_LOGCONFIG(TAG, "--------------------------------------------" "-----------------------------------------------"); #else ESP_LOGCONFIG(TAG, "ID | Device | Device Name " - "| Offset | Size\n" - "-----------------------------------------" + "| Offset | Size"); + ESP_LOGCONFIG(TAG, "-----------------------------------------" "------------------------------"); #endif flash_area_foreach(fa_cb, nullptr); } +#ifdef ESPHOME_LOG_HAS_VERBOSE +// Check if an nRF peripheral's ENABLE register indicates it is enabled. +// periph: peripheral register prefix (e.g. USBD, UARTE, SPI) +// reg: register block pointer (e.g. NRF_USBD, NRF_UARTE0) +#define NRF_PERIPH_ENABLED(periph, reg) \ + YESNO(((reg)->ENABLE & periph##_ENABLE_ENABLE_Msk) == (periph##_ENABLE_ENABLE_Enabled << periph##_ENABLE_ENABLE_Pos)) + +static void log_peripherals_info() { + // most peripherals are enabled only when in use so ESP_LOGV is enough + ESP_LOGV(TAG, "Peripherals status:"); + ESP_LOGV(TAG, " USBD: %-3s| UARTE0: %-3s| UARTE1: %-3s| UART0: %-3s", // + NRF_PERIPH_ENABLED(USBD, NRF_USBD), NRF_PERIPH_ENABLED(UARTE, NRF_UARTE0), + NRF_PERIPH_ENABLED(UARTE, NRF_UARTE1), NRF_PERIPH_ENABLED(UART, NRF_UART0)); + ESP_LOGV(TAG, " TWIS0: %-3s| TWIS1: %-3s| TWIM0: %-3s| TWIM1: %-3s", // + NRF_PERIPH_ENABLED(TWIS, NRF_TWIS0), NRF_PERIPH_ENABLED(TWIS, NRF_TWIS1), + NRF_PERIPH_ENABLED(TWIM, NRF_TWIM0), NRF_PERIPH_ENABLED(TWIM, NRF_TWIM1)); + ESP_LOGV(TAG, " TWI0: %-3s| TWI1: %-3s| COMP: %-3s| CCM: %-3s", // + NRF_PERIPH_ENABLED(TWI, NRF_TWI0), NRF_PERIPH_ENABLED(TWI, NRF_TWI1), NRF_PERIPH_ENABLED(COMP, NRF_COMP), + NRF_PERIPH_ENABLED(CCM, NRF_CCM)); + ESP_LOGV(TAG, " PDM: %-3s| SPIS0: %-3s| SPIS1: %-3s| SPIS2: %-3s", // + NRF_PERIPH_ENABLED(PDM, NRF_PDM), NRF_PERIPH_ENABLED(SPIS, NRF_SPIS0), NRF_PERIPH_ENABLED(SPIS, NRF_SPIS1), + NRF_PERIPH_ENABLED(SPIS, NRF_SPIS2)); + ESP_LOGV(TAG, " SPIM0: %-3s| SPIM1: %-3s| SPIM2: %-3s| SPIM3: %-3s", // + NRF_PERIPH_ENABLED(SPIM, NRF_SPIM0), NRF_PERIPH_ENABLED(SPIM, NRF_SPIM1), + NRF_PERIPH_ENABLED(SPIM, NRF_SPIM2), NRF_PERIPH_ENABLED(SPIM, NRF_SPIM3)); + ESP_LOGV(TAG, " SPI0: %-3s| SPI1: %-3s| SPI2: %-3s| SAADC: %-3s", // + NRF_PERIPH_ENABLED(SPI, NRF_SPI0), NRF_PERIPH_ENABLED(SPI, NRF_SPI1), NRF_PERIPH_ENABLED(SPI, NRF_SPI2), + NRF_PERIPH_ENABLED(SAADC, NRF_SAADC)); + ESP_LOGV(TAG, " QSPI: %-3s| QDEC: %-3s| LPCOMP: %-3s| I2S: %-3s", // + NRF_PERIPH_ENABLED(QSPI, NRF_QSPI), NRF_PERIPH_ENABLED(QDEC, NRF_QDEC), + NRF_PERIPH_ENABLED(LPCOMP, NRF_LPCOMP), NRF_PERIPH_ENABLED(I2S, NRF_I2S)); + ESP_LOGV(TAG, " PWM0: %-3s| PWM1: %-3s| PWM2: %-3s| PWM3: %-3s", // + NRF_PERIPH_ENABLED(PWM, NRF_PWM0), NRF_PERIPH_ENABLED(PWM, NRF_PWM1), NRF_PERIPH_ENABLED(PWM, NRF_PWM2), + NRF_PERIPH_ENABLED(PWM, NRF_PWM3)); + ESP_LOGV(TAG, " AAR: %-3s| QSPI deep power-down:%-3s| CRYPTOCELL: %-3s", NRF_PERIPH_ENABLED(AAR, NRF_AAR), + YESNO((NRF_QSPI->IFCONFIG0 & QSPI_IFCONFIG0_DPMENABLE_Msk) == + (QSPI_IFCONFIG0_DPMENABLE_Enable << QSPI_IFCONFIG0_DPMENABLE_Pos)), + YESNO((NRF_CRYPTOCELL->ENABLE & CRYPTOCELL_ENABLE_ENABLE_Msk) == + (CRYPTOCELL_ENABLE_ENABLE_Enabled << CRYPTOCELL_ENABLE_ENABLE_Pos))); +} +#undef NRF_PERIPH_ENABLED +#endif + static const char *regout0_to_str(uint32_t value) { switch (value) { case (UICR_REGOUT0_VOUT_DEFAULT): @@ -325,11 +327,12 @@ size_t DebugComponent::get_device_info_(std::span char mac_pretty[MAC_ADDRESS_PRETTY_BUFFER_SIZE]; get_mac_address_pretty_into_buffer(mac_pretty); ESP_LOGD(TAG, - "Code page size: %u, code size: %u, device id: 0x%08x%08x\n" - "Encryption root: 0x%08x%08x%08x%08x, Identity Root: 0x%08x%08x%08x%08x\n" - "Device address type: %s, address: %s\n" - "Part code: nRF%x, version: %c%c%c%c, package: %s\n" - "RAM: %ukB, Flash: %ukB, production test: %sdone", + "nRF debug info:\n" + " Code page size: %u, code size: %u, device id: 0x%08x%08x\n" + " Encryption root: 0x%08x%08x%08x%08x, Identity Root: 0x%08x%08x%08x%08x\n" + " Device address type: %s, address: %s\n" + " Part code: nRF%x, version: %c%c%c%c, package: %s\n" + " RAM: %ukB, Flash: %ukB, production test: %sdone", NRF_FICR->CODEPAGESIZE, NRF_FICR->CODESIZE, NRF_FICR->DEVICEID[1], NRF_FICR->DEVICEID[0], NRF_FICR->ER[0], NRF_FICR->ER[1], NRF_FICR->ER[2], NRF_FICR->ER[3], NRF_FICR->IR[0], NRF_FICR->IR[1], NRF_FICR->IR[2], NRF_FICR->IR[3], (NRF_FICR->DEVICEADDRTYPE & 0x1 ? "Random" : "Public"), mac_pretty, NRF_FICR->INFO.PART, @@ -340,23 +343,22 @@ size_t DebugComponent::get_device_info_(std::span (NRF_UICR->PSELRESET[0] & UICR_PSELRESET_CONNECT_Msk) == UICR_PSELRESET_CONNECT_Connected << UICR_PSELRESET_CONNECT_Pos; ESP_LOGD( - TAG, "GPIO as NFC pins: %s, GPIO as nRESET pin: %s", + TAG, " GPIO as NFC pins: %s, GPIO as nRESET pin: %s", YESNO((NRF_UICR->NFCPINS & UICR_NFCPINS_PROTECT_Msk) == (UICR_NFCPINS_PROTECT_NFC << UICR_NFCPINS_PROTECT_Pos)), YESNO(n_reset_enabled)); if (n_reset_enabled) { uint8_t port = (NRF_UICR->PSELRESET[0] & UICR_PSELRESET_PORT_Msk) >> UICR_PSELRESET_PORT_Pos; uint8_t pin = (NRF_UICR->PSELRESET[0] & UICR_PSELRESET_PIN_Msk) >> UICR_PSELRESET_PIN_Pos; - ESP_LOGD(TAG, "nRESET port P%u.%02u", port, pin); + ESP_LOGD(TAG, " nRESET port P%u.%02u", port, pin); } #ifdef USE_BOOTLOADER_MCUBOOT - ESP_LOGD(TAG, "bootloader: mcuboot"); + ESP_LOGD(TAG, " Bootloader: mcuboot"); #else - ESP_LOGD(TAG, "bootloader: Adafruit, version %u.%u.%u", (BOOTLOADER_VERSION_REGISTER >> 16) & 0xFF, + ESP_LOGD(TAG, " Bootloader: Adafruit, version %u.%u.%u", (BOOTLOADER_VERSION_REGISTER >> 16) & 0xFF, (BOOTLOADER_VERSION_REGISTER >> 8) & 0xFF, BOOTLOADER_VERSION_REGISTER & 0xFF); - ESP_LOGD(TAG, - "MBR bootloader addr 0x%08x, UICR bootloader addr 0x%08x\n" - "MBR param page addr 0x%08x, UICR param page addr 0x%08x", - read_mem_u32(MBR_BOOTLOADER_ADDR), NRF_UICR->NRFFW[0], read_mem_u32(MBR_PARAM_PAGE_ADDR), + ESP_LOGD(TAG, " MBR bootloader addr 0x%08x, UICR bootloader addr 0x%08x", read_mem_u32(MBR_BOOTLOADER_ADDR), + NRF_UICR->NRFFW[0]); + ESP_LOGD(TAG, " MBR param page addr 0x%08x, UICR param page addr 0x%08x", read_mem_u32(MBR_PARAM_PAGE_ADDR), NRF_UICR->NRFFW[1]); if (is_sd_present()) { uint32_t const sd_id = sd_id_get(); @@ -367,7 +369,7 @@ size_t DebugComponent::get_device_info_(std::span ver[1] = (sd_version - ver[0] * 1000000) / 1000; ver[2] = (sd_version - ver[0] * 1000000 - ver[1] * 1000); - ESP_LOGD(TAG, "SoftDevice: S%u %u.%u.%u", sd_id, ver[0], ver[1], ver[2]); + ESP_LOGD(TAG, " SoftDevice: S%u %u.%u.%u", sd_id, ver[0], ver[1], ver[2]); #ifdef USE_SOFTDEVICE_ID #ifdef USE_SOFTDEVICE_VERSION if (USE_SOFTDEVICE_ID != sd_id || USE_SOFTDEVICE_VERSION != ver[0]) { @@ -393,11 +395,11 @@ size_t DebugComponent::get_device_info_(std::span } return res; }; - ESP_LOGD(TAG, - "NRFFW %s\n" - "NRFHW %s", - uicr(NRF_UICR->NRFFW, 13).c_str(), uicr(NRF_UICR->NRFHW, 12).c_str()); - + ESP_LOGD(TAG, " NRFFW %s", uicr(NRF_UICR->NRFFW, 13).c_str()); + ESP_LOGD(TAG, " NRFHW %s", uicr(NRF_UICR->NRFHW, 12).c_str()); +#ifdef ESPHOME_LOG_HAS_VERBOSE + log_peripherals_info(); +#endif return pos; } diff --git a/esphome/components/deep_sleep/__init__.py b/esphome/components/deep_sleep/__init__.py index 3cfe7aa641..16329bb0fa 100644 --- a/esphome/components/deep_sleep/__init__.py +++ b/esphome/components/deep_sleep/__init__.py @@ -266,8 +266,7 @@ CONFIG_SCHEMA = cv.All( cv.Optional(CONF_WAKEUP_PIN): validate_wakeup_pin, cv.Optional(CONF_WAKEUP_PIN_MODE): cv.All( cv.only_on([PLATFORM_ESP32, PLATFORM_BK72XX]), - cv.enum(WAKEUP_PIN_MODES), - upper=True, + cv.enum(WAKEUP_PIN_MODES, upper=True), ), cv.Optional(CONF_ESP32_EXT1_WAKEUP): cv.All( cv.only_on_esp32, @@ -405,7 +404,10 @@ DEEP_SLEEP_ENTER_SCHEMA = cv.All( @automation.register_action( - "deep_sleep.enter", EnterDeepSleepAction, DEEP_SLEEP_ENTER_SCHEMA + "deep_sleep.enter", + EnterDeepSleepAction, + DEEP_SLEEP_ENTER_SCHEMA, + synchronous=True, ) async def deep_sleep_enter_to_code(config, action_id, template_arg, args): paren = await cg.get_variable(config[CONF_ID]) @@ -428,11 +430,13 @@ async def deep_sleep_enter_to_code(config, action_id, template_arg, args): "deep_sleep.prevent", PreventDeepSleepAction, automation.maybe_simple_id(DEEP_SLEEP_ACTION_SCHEMA), + synchronous=True, ) @automation.register_action( "deep_sleep.allow", AllowDeepSleepAction, automation.maybe_simple_id(DEEP_SLEEP_ACTION_SCHEMA), + synchronous=True, ) async def deep_sleep_action_to_code(config, action_id, template_arg, args): var = cg.new_Pvariable(action_id, template_arg) diff --git a/esphome/components/deep_sleep/deep_sleep_component.cpp b/esphome/components/deep_sleep/deep_sleep_component.cpp index 8066b411ff..3dd1b70930 100644 --- a/esphome/components/deep_sleep/deep_sleep_component.cpp +++ b/esphome/components/deep_sleep/deep_sleep_component.cpp @@ -40,10 +40,6 @@ void DeepSleepComponent::loop() { this->begin_sleep(); } -float DeepSleepComponent::get_loop_priority() const { - return -100.0f; // run after everything else is ready -} - void DeepSleepComponent::set_sleep_duration(uint32_t time_ms) { this->sleep_duration_ = uint64_t(time_ms) * 1000; } void DeepSleepComponent::set_run_duration(uint32_t time_ms) { this->run_duration_ = time_ms; } diff --git a/esphome/components/deep_sleep/deep_sleep_component.h b/esphome/components/deep_sleep/deep_sleep_component.h index 3e6eda2257..9090f91876 100644 --- a/esphome/components/deep_sleep/deep_sleep_component.h +++ b/esphome/components/deep_sleep/deep_sleep_component.h @@ -113,7 +113,6 @@ class DeepSleepComponent : public Component { void setup() override; void dump_config() override; void loop() override; - float get_loop_priority() const override; float get_setup_priority() const override; /// Helper to enter deep sleep mode @@ -143,7 +142,7 @@ class DeepSleepComponent : public Component { #endif // USE_BK72XX #ifdef USE_ESP32 - InternalGPIOPin *wakeup_pin_; + InternalGPIOPin *wakeup_pin_{nullptr}; WakeupPinMode wakeup_pin_mode_{WAKEUP_PIN_MODE_IGNORE}; #if !defined(USE_ESP32_VARIANT_ESP32C2) && !defined(USE_ESP32_VARIANT_ESP32C3) diff --git a/esphome/components/deep_sleep/deep_sleep_esp32.cpp b/esphome/components/deep_sleep/deep_sleep_esp32.cpp index 79c34f627a..4f4d262d30 100644 --- a/esphome/components/deep_sleep/deep_sleep_esp32.cpp +++ b/esphome/components/deep_sleep/deep_sleep_esp32.cpp @@ -3,6 +3,7 @@ #include "driver/gpio.h" #include "deep_sleep_component.h" #include "esphome/core/log.h" +#include namespace esphome { namespace deep_sleep { @@ -26,7 +27,7 @@ namespace deep_sleep { // - ext0: Single pin wakeup using RTC GPIO (esp_sleep_enable_ext0_wakeup) // - ext1: Multiple pin wakeup (esp_sleep_enable_ext1_wakeup) // - Touch: Touch pad wakeup (esp_sleep_enable_touchpad_wakeup) -// - GPIO wakeup: GPIO wakeup for RTC pins (esp_deep_sleep_enable_gpio_wakeup) +// - GPIO wakeup: GPIO wakeup for RTC pins static const char *const TAG = "deep_sleep"; @@ -135,8 +136,13 @@ void DeepSleepComponent::deep_sleep_() { } // Internal pullup/pulldown resistors are enabled automatically, when // ESP_SLEEP_GPIO_ENABLE_INTERNAL_RESISTORS is set (by default it is) - esp_deep_sleep_enable_gpio_wakeup(1 << this->wakeup_pin_->get_pin(), +#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(6, 0, 0) + esp_sleep_enable_gpio_wakeup_on_hp_periph_powerdown(1ULL << this->wakeup_pin_->get_pin(), + static_cast(level)); +#else + esp_deep_sleep_enable_gpio_wakeup(1ULL << this->wakeup_pin_->get_pin(), static_cast(level)); +#endif } #endif diff --git a/esphome/components/deep_sleep/deep_sleep_esp8266.cpp b/esphome/components/deep_sleep/deep_sleep_esp8266.cpp index 54d2aa993d..efbd45c34e 100644 --- a/esphome/components/deep_sleep/deep_sleep_esp8266.cpp +++ b/esphome/components/deep_sleep/deep_sleep_esp8266.cpp @@ -15,7 +15,7 @@ void DeepSleepComponent::dump_config_platform_() {} bool DeepSleepComponent::prepare_to_sleep_() { return true; } void DeepSleepComponent::deep_sleep_() { - ESP.deepSleep(*this->sleep_duration_); // NOLINT(readability-static-accessed-through-instance) + ESP.deepSleep(this->sleep_duration_.value_or(0)); // NOLINT(readability-static-accessed-through-instance) } } // namespace deep_sleep diff --git a/esphome/components/delonghi/delonghi.cpp b/esphome/components/delonghi/delonghi.cpp index 9bc0b5753d..19af703ab2 100644 --- a/esphome/components/delonghi/delonghi.cpp +++ b/esphome/components/delonghi/delonghi.cpp @@ -64,7 +64,7 @@ uint8_t DelonghiClimate::operation_mode_() { uint16_t DelonghiClimate::fan_speed_() { uint16_t fan_speed; - switch (this->fan_mode.value()) { + switch (this->fan_mode.value_or(climate::CLIMATE_FAN_ON)) { case climate::CLIMATE_FAN_LOW: fan_speed = DELONGHI_FAN_LOW; break; diff --git a/esphome/components/demo/demo_alarm_control_panel.h b/esphome/components/demo/demo_alarm_control_panel.h index f59434830b..9976e5c7f0 100644 --- a/esphome/components/demo/demo_alarm_control_panel.h +++ b/esphome/components/demo/demo_alarm_control_panel.h @@ -29,10 +29,11 @@ class DemoAlarmControlPanel : public AlarmControlPanel, public Component { protected: void control(const AlarmControlPanelCall &call) override { auto state = call.get_state().value_or(ACP_STATE_DISARMED); + auto code = call.get_code(); switch (state) { case ACP_STATE_ARMED_AWAY: - if (this->get_requires_code_to_arm() && call.get_code().has_value()) { - if (call.get_code().value() != "1234") { + if (this->get_requires_code_to_arm()) { + if (!code.has_value() || *code != "1234") { this->status_momentary_error("invalid_code", 5000); return; } @@ -40,8 +41,8 @@ class DemoAlarmControlPanel : public AlarmControlPanel, public Component { this->publish_state(ACP_STATE_ARMED_AWAY); break; case ACP_STATE_DISARMED: - if (this->get_requires_code() && call.get_code().has_value()) { - if (call.get_code().value() != "1234") { + if (this->get_requires_code()) { + if (!code.has_value() || *code != "1234") { this->status_momentary_error("invalid_code", 5000); return; } diff --git a/esphome/components/demo/demo_climate.h b/esphome/components/demo/demo_climate.h index e2dfb0142b..c5f07ac114 100644 --- a/esphome/components/demo/demo_climate.h +++ b/esphome/components/demo/demo_climate.h @@ -45,33 +45,31 @@ class DemoClimate : public climate::Climate, public Component { protected: void control(const climate::ClimateCall &call) override { - if (call.get_mode().has_value()) { - this->mode = *call.get_mode(); - } - if (call.get_target_temperature().has_value()) { - this->target_temperature = *call.get_target_temperature(); - } - if (call.get_target_temperature_low().has_value()) { - this->target_temperature_low = *call.get_target_temperature_low(); - } - if (call.get_target_temperature_high().has_value()) { - this->target_temperature_high = *call.get_target_temperature_high(); - } - if (call.get_fan_mode().has_value()) { - this->set_fan_mode_(*call.get_fan_mode()); - } - if (call.get_swing_mode().has_value()) { - this->swing_mode = *call.get_swing_mode(); - } - if (call.has_custom_fan_mode()) { + auto mode = call.get_mode(); + if (mode.has_value()) + this->mode = *mode; + auto target_temperature = call.get_target_temperature(); + if (target_temperature.has_value()) + this->target_temperature = *target_temperature; + auto target_temperature_low = call.get_target_temperature_low(); + if (target_temperature_low.has_value()) + this->target_temperature_low = *target_temperature_low; + auto target_temperature_high = call.get_target_temperature_high(); + if (target_temperature_high.has_value()) + this->target_temperature_high = *target_temperature_high; + auto fan_mode = call.get_fan_mode(); + if (fan_mode.has_value()) + this->set_fan_mode_(*fan_mode); + auto swing_mode = call.get_swing_mode(); + if (swing_mode.has_value()) + this->swing_mode = *swing_mode; + if (call.has_custom_fan_mode()) this->set_custom_fan_mode_(call.get_custom_fan_mode()); - } - if (call.get_preset().has_value()) { - this->set_preset_(*call.get_preset()); - } - if (call.has_custom_preset()) { + auto preset = call.get_preset(); + if (preset.has_value()) + this->set_preset_(*preset); + if (call.has_custom_preset()) this->set_custom_preset_(call.get_custom_preset()); - } this->publish_state(); } climate::ClimateTraits traits() override { diff --git a/esphome/components/demo/demo_cover.h b/esphome/components/demo/demo_cover.h index ec266d46ab..69dd5a4d2d 100644 --- a/esphome/components/demo/demo_cover.h +++ b/esphome/components/demo/demo_cover.h @@ -38,8 +38,9 @@ class DemoCover : public cover::Cover, public Component { protected: void control(const cover::CoverCall &call) override { - if (call.get_position().has_value()) { - float target = *call.get_position(); + auto pos = call.get_position(); + if (pos.has_value()) { + float target = *pos; this->current_operation = target > this->position ? cover::COVER_OPERATION_OPENING : cover::COVER_OPERATION_CLOSING; @@ -49,8 +50,9 @@ class DemoCover : public cover::Cover, public Component { this->publish_state(); }); } - if (call.get_tilt().has_value()) { - this->tilt = *call.get_tilt(); + auto tilt = call.get_tilt(); + if (tilt.has_value()) { + this->tilt = *tilt; } if (call.get_stop()) { this->cancel_timeout("move"); diff --git a/esphome/components/demo/demo_fan.h b/esphome/components/demo/demo_fan.h index 09edc4e0b7..a8b397f19a 100644 --- a/esphome/components/demo/demo_fan.h +++ b/esphome/components/demo/demo_fan.h @@ -47,14 +47,18 @@ class DemoFan : public fan::Fan, public Component { protected: void control(const fan::FanCall &call) override { - if (call.get_state().has_value()) - this->state = *call.get_state(); - if (call.get_oscillating().has_value()) - this->oscillating = *call.get_oscillating(); - if (call.get_speed().has_value()) - this->speed = *call.get_speed(); - if (call.get_direction().has_value()) - this->direction = *call.get_direction(); + auto state = call.get_state(); + if (state.has_value()) + this->state = *state; + auto oscillating = call.get_oscillating(); + if (oscillating.has_value()) + this->oscillating = *oscillating; + auto speed = call.get_speed(); + if (speed.has_value()) + this->speed = *speed; + auto direction = call.get_direction(); + if (direction.has_value()) + this->direction = *direction; this->publish_state(); } diff --git a/esphome/components/demo/demo_lock.h b/esphome/components/demo/demo_lock.h index 94d0f70a14..1e3fd51db4 100644 --- a/esphome/components/demo/demo_lock.h +++ b/esphome/components/demo/demo_lock.h @@ -8,8 +8,9 @@ namespace demo { class DemoLock : public lock::Lock { protected: void control(const lock::LockCall &call) override { - auto state = *call.get_state(); - this->publish_state(state); + auto state = call.get_state(); + if (state.has_value()) + this->publish_state(*state); } }; diff --git a/esphome/components/demo/demo_valve.h b/esphome/components/demo/demo_valve.h index 55d457f176..9a3122aca5 100644 --- a/esphome/components/demo/demo_valve.h +++ b/esphome/components/demo/demo_valve.h @@ -26,12 +26,15 @@ class DemoValve : public valve::Valve { protected: void control(const valve::ValveCall &call) override { - if (call.get_position().has_value()) { - this->position = *call.get_position(); + auto pos = call.get_position(); + if (pos.has_value()) { + this->position = *pos; this->publish_state(); return; - } else if (call.get_toggle().has_value()) { - if (call.get_toggle().value()) { + } + auto toggle = call.get_toggle(); + if (toggle.has_value()) { + if (*toggle) { if (this->position == valve::VALVE_OPEN) { this->position = valve::VALVE_CLOSED; this->publish_state(); diff --git a/esphome/components/dew_point/__init__.py b/esphome/components/dew_point/__init__.py new file mode 100644 index 0000000000..3b852436c3 --- /dev/null +++ b/esphome/components/dew_point/__init__.py @@ -0,0 +1 @@ +CODEOWNERS = ["@CFlix"] diff --git a/esphome/components/dew_point/dew_point.cpp b/esphome/components/dew_point/dew_point.cpp new file mode 100644 index 0000000000..04ac305e3d --- /dev/null +++ b/esphome/components/dew_point/dew_point.cpp @@ -0,0 +1,82 @@ + +#include "dew_point.h" + +namespace esphome::dew_point { + +static const char *const TAG = "dew_point.sensor"; + +void DewPointComponent::setup() { + // Register callbacks for sensor updates + if (this->temperature_sensor_ != nullptr) { + this->temperature_sensor_->add_on_state_callback([this](float state) { + this->temperature_value_ = state; + this->enable_loop(); + }); + // Get initial value + if (this->temperature_sensor_->has_state()) { + this->temperature_value_ = this->temperature_sensor_->get_state(); + } + } + + if (this->humidity_sensor_ != nullptr) { + this->humidity_sensor_->add_on_state_callback([this](float state) { + this->humidity_value_ = state; + this->enable_loop(); + }); + // Get initial value + if (this->humidity_sensor_->has_state()) { + this->humidity_value_ = this->humidity_sensor_->get_state(); + } + } +} + +void DewPointComponent::dump_config() { + LOG_SENSOR("", "Dew Point", this); + ESP_LOGCONFIG(TAG, + "Sources\n" + " Temperature: '%s'\n" + " Humidity: '%s'", + this->temperature_sensor_->get_name().c_str(), this->humidity_sensor_->get_name().c_str()); +} + +float DewPointComponent::get_setup_priority() const { return setup_priority::DATA; } + +void DewPointComponent::loop() { + // Only run once + this->disable_loop(); + + // Check if we have valid values for both sensors + if (std::isnan(this->temperature_value_) || std::isnan(this->humidity_value_)) { + ESP_LOGW(TAG, "Temperature or humidity value is NaN, skipping calculation"); + this->publish_state(NAN); + return; + } + + // Check for valid humidity range + if (this->humidity_value_ <= 0.0f || this->humidity_value_ > 100.0f) { + ESP_LOGW(TAG, "Humidity value out of range (0-100): %.2f", this->humidity_value_); + this->publish_state(NAN); + return; + } + + // Magnus formula constants + const float a{17.625f}; + const float b{243.04f}; + + // Calculate dew point using Magnus formula + // Td = (b * alpha) / (a - alpha) + // where alpha = ln(RH/100) + (a * T) / (b + T) + + const float alpha{std::log(this->humidity_value_ / 100.0f) + + (a * this->temperature_value_) / (b + this->temperature_value_)}; + + const float dew_point{(b * alpha) / (a - alpha)}; + + // Publish the calculated dew point + this->publish_state(dew_point); + + ESP_LOGD(TAG, "'%s' >> %.1f°C (T: %.1f°C, RH: %.1f%%)", this->get_name().c_str(), dew_point, this->temperature_value_, + this->humidity_value_); +} + +} // namespace esphome::dew_point diff --git a/esphome/components/dew_point/dew_point.h b/esphome/components/dew_point/dew_point.h new file mode 100644 index 0000000000..833c50fba2 --- /dev/null +++ b/esphome/components/dew_point/dew_point.h @@ -0,0 +1,26 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/components/sensor/sensor.h" + +namespace esphome::dew_point { + +class DewPointComponent : public Component, public sensor::Sensor { + public: + void set_temperature_sensor(sensor::Sensor *temperature_sensor) { this->temperature_sensor_ = temperature_sensor; } + void set_humidity_sensor(sensor::Sensor *humidity_sensor) { this->humidity_sensor_ = humidity_sensor; } + + void setup() override; + void dump_config() override; + void loop() override; + + float get_setup_priority() const override; + + protected: + sensor::Sensor *temperature_sensor_{nullptr}; + sensor::Sensor *humidity_sensor_{nullptr}; + float temperature_value_{NAN}; + float humidity_value_{NAN}; +}; + +} // namespace esphome::dew_point diff --git a/esphome/components/dew_point/sensor.py b/esphome/components/dew_point/sensor.py new file mode 100644 index 0000000000..4fee095602 --- /dev/null +++ b/esphome/components/dew_point/sensor.py @@ -0,0 +1,46 @@ +import esphome.codegen as cg +from esphome.components import sensor +import esphome.config_validation as cv +from esphome.const import ( + CONF_HUMIDITY, + CONF_TEMPERATURE, + DEVICE_CLASS_TEMPERATURE, + STATE_CLASS_MEASUREMENT, + UNIT_CELSIUS, +) + +DEPENDENCIES = ["sensor"] + +dew_point_ns = cg.esphome_ns.namespace("dew_point") +DewPointComponent = dew_point_ns.class_( + "DewPointComponent", cg.Component, sensor.Sensor +) + +CONFIG_SCHEMA = ( + sensor.sensor_schema( + DewPointComponent, + unit_of_measurement=UNIT_CELSIUS, + accuracy_decimals=1, + device_class=DEVICE_CLASS_TEMPERATURE, + state_class=STATE_CLASS_MEASUREMENT, + icon="mdi:weather-rainy", + ) + .extend( + { + cv.Required(CONF_TEMPERATURE): cv.use_id(sensor.Sensor), + cv.Required(CONF_HUMIDITY): cv.use_id(sensor.Sensor), + } + ) + .extend(cv.COMPONENT_SCHEMA) +) + + +async def to_code(config): + var = await sensor.new_sensor(config) + await cg.register_component(var, config) + + temperature_sensor = await cg.get_variable(config[CONF_TEMPERATURE]) + cg.add(var.set_temperature_sensor(temperature_sensor)) + + humidity_sensor = await cg.get_variable(config[CONF_HUMIDITY]) + cg.add(var.set_humidity_sensor(humidity_sensor)) diff --git a/esphome/components/dfplayer/__init__.py b/esphome/components/dfplayer/__init__.py index 53ebda6bcc..c49420f060 100644 --- a/esphome/components/dfplayer/__init__.py +++ b/esphome/components/dfplayer/__init__.py @@ -2,16 +2,13 @@ from esphome import automation import esphome.codegen as cg from esphome.components import uart import esphome.config_validation as cv -from esphome.const import CONF_DEVICE, CONF_FILE, CONF_ID, CONF_TRIGGER_ID, CONF_VOLUME +from esphome.const import CONF_DEVICE, CONF_FILE, CONF_ID, CONF_VOLUME DEPENDENCIES = ["uart"] CODEOWNERS = ["@glmnet"] dfplayer_ns = cg.esphome_ns.namespace("dfplayer") DFPlayer = dfplayer_ns.class_("DFPlayer", cg.Component) -DFPlayerFinishedPlaybackTrigger = dfplayer_ns.class_( - "DFPlayerFinishedPlaybackTrigger", automation.Trigger.template() -) DFPlayerIsPlayingCondition = dfplayer_ns.class_( "DFPlayerIsPlayingCondition", automation.Condition ) @@ -58,13 +55,7 @@ CONFIG_SCHEMA = cv.All( cv.Schema( { cv.GenerateID(): cv.declare_id(DFPlayer), - cv.Optional(CONF_ON_FINISHED_PLAYBACK): automation.validate_automation( - { - cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id( - DFPlayerFinishedPlaybackTrigger - ), - } - ), + cv.Optional(CONF_ON_FINISHED_PLAYBACK): automation.validate_automation({}), } ).extend(uart.UART_DEVICE_SCHEMA) ) @@ -79,8 +70,9 @@ async def to_code(config): await uart.register_uart_device(var, config) for conf in config.get(CONF_ON_FINISHED_PLAYBACK, []): - trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) - await automation.build_automation(trigger, [], conf) + await automation.build_callback_automation( + var, "add_on_finished_playback_callback", [], conf + ) @automation.register_action( @@ -91,6 +83,7 @@ async def to_code(config): cv.GenerateID(): cv.use_id(DFPlayer), } ), + synchronous=True, ) async def dfplayer_next_to_code(config, action_id, template_arg, args): var = cg.new_Pvariable(action_id, template_arg) @@ -106,6 +99,7 @@ async def dfplayer_next_to_code(config, action_id, template_arg, args): cv.GenerateID(): cv.use_id(DFPlayer), } ), + synchronous=True, ) async def dfplayer_previous_to_code(config, action_id, template_arg, args): var = cg.new_Pvariable(action_id, template_arg) @@ -123,6 +117,7 @@ async def dfplayer_previous_to_code(config, action_id, template_arg, args): }, key=CONF_FILE, ), + synchronous=True, ) async def dfplayer_play_mp3_to_code(config, action_id, template_arg, args): var = cg.new_Pvariable(action_id, template_arg) @@ -143,6 +138,7 @@ async def dfplayer_play_mp3_to_code(config, action_id, template_arg, args): }, key=CONF_FILE, ), + synchronous=True, ) async def dfplayer_play_to_code(config, action_id, template_arg, args): var = cg.new_Pvariable(action_id, template_arg) @@ -166,6 +162,7 @@ async def dfplayer_play_to_code(config, action_id, template_arg, args): cv.Optional(CONF_LOOP): cv.templatable(cv.boolean), } ), + synchronous=True, ) async def dfplayer_play_folder_to_code(config, action_id, template_arg, args): var = cg.new_Pvariable(action_id, template_arg) @@ -191,6 +188,7 @@ async def dfplayer_play_folder_to_code(config, action_id, template_arg, args): }, key=CONF_DEVICE, ), + synchronous=True, ) async def dfplayer_set_device_to_code(config, action_id, template_arg, args): var = cg.new_Pvariable(action_id, template_arg) @@ -210,6 +208,7 @@ async def dfplayer_set_device_to_code(config, action_id, template_arg, args): }, key=CONF_VOLUME, ), + synchronous=True, ) async def dfplayer_set_volume_to_code(config, action_id, template_arg, args): var = cg.new_Pvariable(action_id, template_arg) @@ -227,6 +226,7 @@ async def dfplayer_set_volume_to_code(config, action_id, template_arg, args): cv.GenerateID(): cv.use_id(DFPlayer), } ), + synchronous=True, ) async def dfplayer_volume_up_to_code(config, action_id, template_arg, args): var = cg.new_Pvariable(action_id, template_arg) @@ -242,6 +242,7 @@ async def dfplayer_volume_up_to_code(config, action_id, template_arg, args): cv.GenerateID(): cv.use_id(DFPlayer), } ), + synchronous=True, ) async def dfplayer_volume_down_to_code(config, action_id, template_arg, args): var = cg.new_Pvariable(action_id, template_arg) @@ -259,6 +260,7 @@ async def dfplayer_volume_down_to_code(config, action_id, template_arg, args): }, key=CONF_EQ_PRESET, ), + synchronous=True, ) async def dfplayer_set_eq_to_code(config, action_id, template_arg, args): var = cg.new_Pvariable(action_id, template_arg) @@ -276,6 +278,7 @@ async def dfplayer_set_eq_to_code(config, action_id, template_arg, args): cv.GenerateID(): cv.use_id(DFPlayer), } ), + synchronous=True, ) async def dfplayer_sleep_to_code(config, action_id, template_arg, args): var = cg.new_Pvariable(action_id, template_arg) @@ -291,6 +294,7 @@ async def dfplayer_sleep_to_code(config, action_id, template_arg, args): cv.GenerateID(): cv.use_id(DFPlayer), } ), + synchronous=True, ) async def dfplayer_reset_to_code(config, action_id, template_arg, args): var = cg.new_Pvariable(action_id, template_arg) @@ -306,6 +310,7 @@ async def dfplayer_reset_to_code(config, action_id, template_arg, args): cv.GenerateID(): cv.use_id(DFPlayer), } ), + synchronous=True, ) async def dfplayer_start_to_code(config, action_id, template_arg, args): var = cg.new_Pvariable(action_id, template_arg) @@ -321,6 +326,7 @@ async def dfplayer_start_to_code(config, action_id, template_arg, args): cv.GenerateID(): cv.use_id(DFPlayer), } ), + synchronous=True, ) async def dfplayer_pause_to_code(config, action_id, template_arg, args): var = cg.new_Pvariable(action_id, template_arg) @@ -336,6 +342,7 @@ async def dfplayer_pause_to_code(config, action_id, template_arg, args): cv.GenerateID(): cv.use_id(DFPlayer), } ), + synchronous=True, ) async def dfplayer_stop_to_code(config, action_id, template_arg, args): var = cg.new_Pvariable(action_id, template_arg) @@ -351,6 +358,7 @@ async def dfplayer_stop_to_code(config, action_id, template_arg, args): cv.GenerateID(): cv.use_id(DFPlayer), } ), + synchronous=True, ) async def dfplayer_random_to_code(config, action_id, template_arg, args): var = cg.new_Pvariable(action_id, template_arg) diff --git a/esphome/components/dfplayer/dfplayer.cpp b/esphome/components/dfplayer/dfplayer.cpp index 70bd42e1a5..1e1c33adaf 100644 --- a/esphome/components/dfplayer/dfplayer.cpp +++ b/esphome/components/dfplayer/dfplayer.cpp @@ -1,4 +1,5 @@ #include "dfplayer.h" +#include "esphome/core/helpers.h" #include "esphome/core/log.h" namespace esphome { @@ -131,140 +132,150 @@ void DFPlayer::send_cmd_(uint8_t cmd, uint16_t argument) { } void DFPlayer::loop() { - // Read message - while (this->available()) { - uint8_t byte; - this->read_byte(&byte); + // Read all available bytes in batches to reduce UART call overhead. + size_t avail = this->available(); + uint8_t buf[64]; + while (avail > 0) { + size_t to_read = std::min(avail, sizeof(buf)); + if (!this->read_array(buf, to_read)) { + break; + } + avail -= to_read; + for (size_t bi = 0; bi < to_read; bi++) { + uint8_t byte = buf[bi]; - if (this->read_pos_ == DFPLAYER_READ_BUFFER_LENGTH) - this->read_pos_ = 0; + if (this->read_pos_ == DFPLAYER_READ_BUFFER_LENGTH) + this->read_pos_ = 0; - switch (this->read_pos_) { - case 0: // Start mark - if (byte != 0x7E) - continue; - break; - case 1: // Version - if (byte != 0xFF) { - ESP_LOGW(TAG, "Expected Version 0xFF, got %#02x", byte); - this->read_pos_ = 0; - continue; - } - break; - case 2: // Buffer length - if (byte != 0x06) { - ESP_LOGW(TAG, "Expected Buffer length 0x06, got %#02x", byte); - this->read_pos_ = 0; - continue; - } - break; - case 9: // End byte + switch (this->read_pos_) { + case 0: // Start mark + if (byte != 0x7E) + continue; + break; + case 1: // Version + if (byte != 0xFF) { + ESP_LOGW(TAG, "Expected Version 0xFF, got %#02x", byte); + this->read_pos_ = 0; + continue; + } + break; + case 2: // Buffer length + if (byte != 0x06) { + ESP_LOGW(TAG, "Expected Buffer length 0x06, got %#02x", byte); + this->read_pos_ = 0; + continue; + } + break; + case 9: // End byte #ifdef ESPHOME_LOG_HAS_VERY_VERBOSE - char byte_sequence[100]; - byte_sequence[0] = '\0'; - for (size_t i = 0; i < this->read_pos_ + 1; ++i) { - snprintf(byte_sequence + strlen(byte_sequence), sizeof(byte_sequence) - strlen(byte_sequence), "%02X ", - this->read_buffer_[i]); - } - ESP_LOGVV(TAG, "Received byte sequence: %s", byte_sequence); + char byte_sequence[100]; + byte_sequence[0] = '\0'; + for (size_t i = 0; i < this->read_pos_ + 1; ++i) { + snprintf(byte_sequence + strlen(byte_sequence), sizeof(byte_sequence) - strlen(byte_sequence), "%02X ", + this->read_buffer_[i]); + } + ESP_LOGVV(TAG, "Received byte sequence: %s", byte_sequence); #endif - if (byte != 0xEF) { - ESP_LOGW(TAG, "Expected end byte 0xEF, got %#02x", byte); + if (byte != 0xEF) { + ESP_LOGW(TAG, "Expected end byte 0xEF, got %#02x", byte); + this->read_pos_ = 0; + continue; + } + // Parse valid received command + uint8_t cmd = this->read_buffer_[3]; + uint16_t argument = (this->read_buffer_[5] << 8) | this->read_buffer_[6]; + + ESP_LOGV(TAG, "Received message cmd: %#02x arg %#04x", cmd, argument); + + switch (cmd) { + case 0x3A: + if (argument == 1) { + ESP_LOGI(TAG, "USB loaded"); + } else if (argument == 2) { + ESP_LOGI(TAG, "TF Card loaded"); + } + break; + case 0x3B: + if (argument == 1) { + ESP_LOGI(TAG, "USB unloaded"); + } else if (argument == 2) { + ESP_LOGI(TAG, "TF Card unloaded"); + } + break; + case 0x3F: + if (argument == 1) { + ESP_LOGI(TAG, "USB available"); + } else if (argument == 2) { + ESP_LOGI(TAG, "TF Card available"); + } else if (argument == 3) { + ESP_LOGI(TAG, "USB, TF Card available"); + } + break; + case 0x40: + ESP_LOGV(TAG, "Nack"); + this->ack_set_is_playing_ = false; + this->ack_reset_is_playing_ = false; + switch (argument) { + case 0x01: + ESP_LOGE(TAG, "Module is busy or uninitialized"); + break; + case 0x02: + ESP_LOGE(TAG, "Module is in sleep mode"); + break; + case 0x03: + ESP_LOGE(TAG, "Serial receive error"); + break; + case 0x04: + ESP_LOGE(TAG, "Checksum incorrect"); + break; + case 0x05: + ESP_LOGE(TAG, "Specified track is out of current track scope"); + this->is_playing_ = false; + break; + case 0x06: + ESP_LOGE(TAG, "Specified track is not found"); + this->is_playing_ = false; + break; + case 0x07: + ESP_LOGE(TAG, + "Insertion error (an inserting operation only can be done when a track is being played)"); + break; + case 0x08: + ESP_LOGE(TAG, "SD card reading failed (SD card pulled out or damaged)"); + break; + case 0x09: + ESP_LOGE(TAG, "Entered into sleep mode"); + this->is_playing_ = false; + break; + } + break; + case 0x41: + ESP_LOGV(TAG, "Ack ok"); + this->is_playing_ |= this->ack_set_is_playing_; + this->is_playing_ &= !this->ack_reset_is_playing_; + this->ack_set_is_playing_ = false; + this->ack_reset_is_playing_ = false; + break; + case 0x3C: + ESP_LOGV(TAG, "Playback finished (USB drive)"); + this->is_playing_ = false; + this->on_finished_playback_callback_.call(); + break; + case 0x3D: + ESP_LOGV(TAG, "Playback finished (SD card)"); + this->is_playing_ = false; + this->on_finished_playback_callback_.call(); + break; + default: + ESP_LOGE(TAG, "Received unknown cmd %#02x arg %#04x", cmd, argument); + } + this->sent_cmd_ = 0; this->read_pos_ = 0; continue; - } - // Parse valid received command - uint8_t cmd = this->read_buffer_[3]; - uint16_t argument = (this->read_buffer_[5] << 8) | this->read_buffer_[6]; - - ESP_LOGV(TAG, "Received message cmd: %#02x arg %#04x", cmd, argument); - - switch (cmd) { - case 0x3A: - if (argument == 1) { - ESP_LOGI(TAG, "USB loaded"); - } else if (argument == 2) { - ESP_LOGI(TAG, "TF Card loaded"); - } - break; - case 0x3B: - if (argument == 1) { - ESP_LOGI(TAG, "USB unloaded"); - } else if (argument == 2) { - ESP_LOGI(TAG, "TF Card unloaded"); - } - break; - case 0x3F: - if (argument == 1) { - ESP_LOGI(TAG, "USB available"); - } else if (argument == 2) { - ESP_LOGI(TAG, "TF Card available"); - } else if (argument == 3) { - ESP_LOGI(TAG, "USB, TF Card available"); - } - break; - case 0x40: - ESP_LOGV(TAG, "Nack"); - this->ack_set_is_playing_ = false; - this->ack_reset_is_playing_ = false; - switch (argument) { - case 0x01: - ESP_LOGE(TAG, "Module is busy or uninitialized"); - break; - case 0x02: - ESP_LOGE(TAG, "Module is in sleep mode"); - break; - case 0x03: - ESP_LOGE(TAG, "Serial receive error"); - break; - case 0x04: - ESP_LOGE(TAG, "Checksum incorrect"); - break; - case 0x05: - ESP_LOGE(TAG, "Specified track is out of current track scope"); - this->is_playing_ = false; - break; - case 0x06: - ESP_LOGE(TAG, "Specified track is not found"); - this->is_playing_ = false; - break; - case 0x07: - ESP_LOGE(TAG, "Insertion error (an inserting operation only can be done when a track is being played)"); - break; - case 0x08: - ESP_LOGE(TAG, "SD card reading failed (SD card pulled out or damaged)"); - break; - case 0x09: - ESP_LOGE(TAG, "Entered into sleep mode"); - this->is_playing_ = false; - break; - } - break; - case 0x41: - ESP_LOGV(TAG, "Ack ok"); - this->is_playing_ |= this->ack_set_is_playing_; - this->is_playing_ &= !this->ack_reset_is_playing_; - this->ack_set_is_playing_ = false; - this->ack_reset_is_playing_ = false; - break; - case 0x3C: - ESP_LOGV(TAG, "Playback finished (USB drive)"); - this->is_playing_ = false; - this->on_finished_playback_callback_.call(); - case 0x3D: - ESP_LOGV(TAG, "Playback finished (SD card)"); - this->is_playing_ = false; - this->on_finished_playback_callback_.call(); - break; - default: - ESP_LOGE(TAG, "Received unknown cmd %#02x arg %#04x", cmd, argument); - } - this->sent_cmd_ = 0; - this->read_pos_ = 0; - continue; + } + this->read_buffer_[this->read_pos_] = byte; + this->read_pos_++; } - this->read_buffer_[this->read_pos_] = byte; - this->read_pos_++; } } void DFPlayer::dump_config() { diff --git a/esphome/components/dfplayer/dfplayer.h b/esphome/components/dfplayer/dfplayer.h index 03d2230ca6..0d240566c3 100644 --- a/esphome/components/dfplayer/dfplayer.h +++ b/esphome/components/dfplayer/dfplayer.h @@ -51,8 +51,8 @@ class DFPlayer : public uart::UARTDevice, public Component { bool is_playing() { return is_playing_; } void dump_config() override; - void add_on_finished_playback_callback(std::function callback) { - this->on_finished_playback_callback_.add(std::move(callback)); + template void add_on_finished_playback_callback(F &&callback) { + this->on_finished_playback_callback_.add(std::forward(callback)); } protected: @@ -171,12 +171,5 @@ template class DFPlayerIsPlayingCondition : public Conditionparent_->is_playing(); } }; -class DFPlayerFinishedPlaybackTrigger : public Trigger<> { - public: - explicit DFPlayerFinishedPlaybackTrigger(DFPlayer *parent) { - parent->add_on_finished_playback_callback([this]() { this->trigger(); }); - } -}; - } // namespace dfplayer } // namespace esphome diff --git a/esphome/components/dfrobot_sen0395/__init__.py b/esphome/components/dfrobot_sen0395/__init__.py index d54b147036..ba77e56abb 100644 --- a/esphome/components/dfrobot_sen0395/__init__.py +++ b/esphome/components/dfrobot_sen0395/__init__.py @@ -52,6 +52,7 @@ async def to_code(config): cv.GenerateID(): cv.use_id(DfrobotSen0395Component), } ), + synchronous=True, ) async def dfrobot_sen0395_reset_to_code(config, action_id, template_arg, args): var = cg.new_Pvariable(action_id, template_arg) @@ -151,6 +152,7 @@ MMWAVE_SETTINGS_SCHEMA = cv.Schema( "dfrobot_sen0395.settings", DfrobotSen0395SettingsAction, MMWAVE_SETTINGS_SCHEMA, + synchronous=True, ) async def dfrobot_sen0395_settings_to_code(config, action_id, template_arg, args): var = cg.new_Pvariable(action_id, template_arg) diff --git a/esphome/components/dfrobot_sen0395/commands.cpp b/esphome/components/dfrobot_sen0395/commands.cpp index 2c44c6fba9..0f69c82f39 100644 --- a/esphome/components/dfrobot_sen0395/commands.cpp +++ b/esphome/components/dfrobot_sen0395/commands.cpp @@ -187,18 +187,18 @@ uint8_t DetRangeCfgCommand::on_message(std::string &message) { } else if (message == "Done") { ESP_LOGI(TAG, "Updated detection area config:\n" - "Detection area 1 from %.02fm to %.02fm.", + " Detection area 1 from %.02fm to %.02fm.", this->min1_, this->max1_); if (this->min2_ >= 0 && this->max2_ >= 0) { - ESP_LOGI(TAG, "Detection area 2 from %.02fm to %.02fm.", this->min2_, this->max2_); + ESP_LOGI(TAG, " Detection area 2 from %.02fm to %.02fm.", this->min2_, this->max2_); } if (this->min3_ >= 0 && this->max3_ >= 0) { - ESP_LOGI(TAG, "Detection area 3 from %.02fm to %.02fm.", this->min3_, this->max3_); + ESP_LOGI(TAG, " Detection area 3 from %.02fm to %.02fm.", this->min3_, this->max3_); } if (this->min4_ >= 0 && this->max4_ >= 0) { - ESP_LOGI(TAG, "Detection area 4 from %.02fm to %.02fm.", this->min4_, this->max4_); + ESP_LOGI(TAG, " Detection area 4 from %.02fm to %.02fm.", this->min4_, this->max4_); } - ESP_LOGD(TAG, "Used command: %s", this->cmd_.c_str()); + ESP_LOGD(TAG, " Used command: %s", this->cmd_.c_str()); return 1; // Command done } return 0; // Command not done yet. @@ -222,10 +222,10 @@ uint8_t SetLatencyCommand::on_message(std::string &message) { } else if (message == "Done") { ESP_LOGI(TAG, "Updated output latency config:\n" - "Signal that someone was detected is delayed by %.03f s.\n" - "Signal that nobody is detected anymore is delayed by %.03f s.", + " Signal that someone was detected is delayed by %.03f s.\n" + " Signal that nobody is detected anymore is delayed by %.03f s.", this->delay_after_detection_, this->delay_after_disappear_); - ESP_LOGD(TAG, "Used command: %s", this->cmd_.c_str()); + ESP_LOGD(TAG, " Used command: %s", this->cmd_.c_str()); return 1; // Command done } return 0; // Command not done yet diff --git a/esphome/components/dfrobot_sen0395/commands.h b/esphome/components/dfrobot_sen0395/commands.h index 3b0551b184..95167efb4d 100644 --- a/esphome/components/dfrobot_sen0395/commands.h +++ b/esphome/components/dfrobot_sen0395/commands.h @@ -30,11 +30,9 @@ class Command { class ReadStateCommand : public Command { public: + ReadStateCommand() { timeout_ms_ = 500; } uint8_t execute(DfrobotSen0395Component *parent) override; uint8_t on_message(std::string &message) override; - - protected: - uint32_t timeout_ms_{500}; }; class PowerCommand : public Command { @@ -99,12 +97,12 @@ class ResetSystemCommand : public Command { class SaveCfgCommand : public Command { public: - SaveCfgCommand() { cmd_ = "saveCfg 0x45670123 0xCDEF89AB 0x956128C6 0xDF54AC89"; } + SaveCfgCommand() { + cmd_ = "saveCfg 0x45670123 0xCDEF89AB 0x956128C6 0xDF54AC89"; + cmd_duration_ms_ = 3000; + timeout_ms_ = 3500; + } uint8_t on_message(std::string &message) override; - - protected: - uint32_t cmd_duration_ms_{3000}; - uint32_t timeout_ms_{3500}; }; class LedModeCommand : public Command { diff --git a/esphome/components/dht/dht.cpp b/esphome/components/dht/dht.cpp index 276ea24717..5b7b6a268f 100644 --- a/esphome/components/dht/dht.cpp +++ b/esphome/components/dht/dht.cpp @@ -2,8 +2,7 @@ #include "esphome/core/helpers.h" #include "esphome/core/log.h" -namespace esphome { -namespace dht { +namespace esphome::dht { static const char *const TAG = "dht"; @@ -45,16 +44,13 @@ void DHT::update() { } if (success) { - ESP_LOGD(TAG, "Temperature %.1f°C Humidity %.1f%%", temperature, humidity); - if (this->temperature_sensor_ != nullptr) this->temperature_sensor_->publish_state(temperature); if (this->humidity_sensor_ != nullptr) this->humidity_sensor_->publish_state(humidity); this->status_clear_warning(); } else { - ESP_LOGW(TAG, "Invalid readings! Check pin number and pull-up resistor%s.", - this->is_auto_detect_ ? " and try manually specifying the model" : ""); + ESP_LOGW(TAG, "Invalid readings"); if (this->temperature_sensor_ != nullptr) this->temperature_sensor_->publish_state(NAN); if (this->humidity_sensor_ != nullptr) @@ -63,8 +59,6 @@ void DHT::update() { } } -float DHT::get_setup_priority() const { return setup_priority::DATA; } - void DHT::set_dht_model(DHTModel model) { this->model_ = model; this->is_auto_detect_ = model == DHT_MODEL_AUTO_DETECT; @@ -75,8 +69,7 @@ bool HOT IRAM_ATTR DHT::read_sensor_(float *temperature, float *humidity, bool r *temperature = NAN; int error_code = 0; - int8_t i = 0; - uint8_t data[5] = {0, 0, 0, 0, 0}; + uint8_t data[5] = {}; #ifndef USE_ESP32 this->pin_.pin_mode(gpio::FLAG_OUTPUT); @@ -109,7 +102,9 @@ bool HOT IRAM_ATTR DHT::read_sensor_(float *temperature, float *humidity, bool r uint8_t bit = 7; uint8_t byte = 0; - for (i = -1; i < 40; i++) { + // On 32-bit Xtensa/RISC-V cores, int8_t would require masking/sign-extension for comparisons + // vs. native int. Using int i is native word size — small win in the timing-critical section. + for (int i = -1; i < 40; i++) { uint32_t start_time = micros(); // Wait for rising edge @@ -158,11 +153,9 @@ bool HOT IRAM_ATTR DHT::read_sensor_(float *temperature, float *humidity, bool r } } } - if (!report_errors && error_code != 0) - return false; - - if (error_code) { - ESP_LOGW(TAG, ESP_LOG_MSG_COMM_FAIL); + if (error_code != 0) { + if (report_errors) + ESP_LOGW(TAG, ESP_LOG_MSG_COMM_FAIL); return false; } @@ -179,7 +172,7 @@ bool HOT IRAM_ATTR DHT::read_sensor_(float *temperature, float *humidity, bool r if (checksum_a != data[4] && checksum_b != data[4]) { if (report_errors) { - ESP_LOGW(TAG, "Checksum invalid: %u!=%u", checksum_a, data[4]); + ESP_LOGW(TAG, "Invalid checksum"); } return false; } @@ -236,5 +229,4 @@ bool HOT IRAM_ATTR DHT::read_sensor_(float *temperature, float *humidity, bool r return true; } -} // namespace dht -} // namespace esphome +} // namespace esphome::dht diff --git a/esphome/components/dht/dht.h b/esphome/components/dht/dht.h index 9047dd2c96..0c535f7cf6 100644 --- a/esphome/components/dht/dht.h +++ b/esphome/components/dht/dht.h @@ -4,10 +4,9 @@ #include "esphome/core/hal.h" #include "esphome/components/sensor/sensor.h" -namespace esphome { -namespace dht { +namespace esphome::dht { -enum DHTModel { +enum DHTModel : uint8_t { DHT_MODEL_AUTO_DETECT = 0, DHT_MODEL_DHT11, DHT_MODEL_DHT22, @@ -42,7 +41,6 @@ class DHT : public PollingComponent { this->t_pin_ = pin; this->pin_ = pin->to_isr(); } - void set_model(DHTModel model) { model_ = model; } void set_temperature_sensor(sensor::Sensor *temperature_sensor) { temperature_sensor_ = temperature_sensor; } void set_humidity_sensor(sensor::Sensor *humidity_sensor) { humidity_sensor_ = humidity_sensor; } @@ -51,19 +49,16 @@ class DHT : public PollingComponent { void dump_config() override; /// Update sensor values and push them to the frontend. void update() override; - /// HARDWARE_LATE setup priority. - float get_setup_priority() const override; protected: bool read_sensor_(float *temperature, float *humidity, bool report_errors); + sensor::Sensor *temperature_sensor_{nullptr}; + sensor::Sensor *humidity_sensor_{nullptr}; InternalGPIOPin *t_pin_; ISRInternalGPIOPin pin_; DHTModel model_{DHT_MODEL_AUTO_DETECT}; bool is_auto_detect_{false}; - sensor::Sensor *temperature_sensor_{nullptr}; - sensor::Sensor *humidity_sensor_{nullptr}; }; -} // namespace dht -} // namespace esphome +} // namespace esphome::dht diff --git a/esphome/components/dht12/dht12.cpp b/esphome/components/dht12/dht12.cpp index 445d150be0..1d884daad6 100644 --- a/esphome/components/dht12/dht12.cpp +++ b/esphome/components/dht12/dht12.cpp @@ -49,7 +49,7 @@ void DHT12Component::dump_config() { LOG_SENSOR(" ", "Temperature", this->temperature_sensor_); LOG_SENSOR(" ", "Humidity", this->humidity_sensor_); } -float DHT12Component::get_setup_priority() const { return setup_priority::DATA; } + bool DHT12Component::read_data_(uint8_t *data) { if (!this->read_bytes(0, data, 5)) { ESP_LOGW(TAG, "Updating DHT12 failed!"); diff --git a/esphome/components/dht12/dht12.h b/esphome/components/dht12/dht12.h index 2a706039ba..ab19d7c723 100644 --- a/esphome/components/dht12/dht12.h +++ b/esphome/components/dht12/dht12.h @@ -11,7 +11,6 @@ class DHT12Component : public PollingComponent, public i2c::I2CDevice { public: void setup() override; void dump_config() override; - float get_setup_priority() const override; void update() override; void set_temperature_sensor(sensor::Sensor *temperature_sensor) { temperature_sensor_ = temperature_sensor; } diff --git a/esphome/components/display/__init__.py b/esphome/components/display/__init__.py index ccbeedcd2f..67d76a59d9 100644 --- a/esphome/components/display/__init__.py +++ b/esphome/components/display/__init__.py @@ -1,6 +1,9 @@ +from dataclasses import dataclass + from esphome import automation, core from esphome.automation import maybe_simple_id import esphome.codegen as cg +from esphome.components.const import KEY_METADATA import esphome.config_validation as cv from esphome.const import ( CONF_AUTO_CLEAR_ENABLED, @@ -15,8 +18,10 @@ from esphome.const import ( CONF_UPDATE_INTERVAL, SCHEDULER_DONT_RUN, ) -from esphome.core import CoroPriority, coroutine_with_priority +from esphome.core import CORE, CoroPriority, coroutine_with_priority +from esphome.cpp_generator import MockObj +DOMAIN = "display" IS_PLATFORM_COMPONENT = True display_ns = cg.esphome_ns.namespace("display") @@ -112,8 +117,9 @@ FULL_DISPLAY_SCHEMA.add_extra(_validate_test_card) async def setup_display_core_(var, config): - if CONF_ROTATION in config: - cg.add(var.set_rotation(DISPLAY_ROTATIONS[config[CONF_ROTATION]])) + if rotation := config.get(CONF_ROTATION, 0): + # Default initialised value for rotation is 0 + cg.add(var.set_rotation(DISPLAY_ROTATIONS[rotation])) if (auto_clear := config.get(CONF_AUTO_CLEAR_ENABLED)) is not None: # Default to true if pages or lambda is specified. Ideally this would be done during validation, but @@ -146,6 +152,39 @@ async def setup_display_core_(var, config): cg.add(var.show_test_card()) +# Storage of display metadata in a central location, accessible via the id + + +@dataclass(frozen=True) +class DisplayMetaData: + width: int = 0 + height: int = 0 + has_writer: bool = False + has_hardware_rotation: bool = False + + +def get_all_display_metadata() -> dict[str, DisplayMetaData]: + """Get all display metadata.""" + return CORE.data.setdefault(DOMAIN, {}).setdefault(KEY_METADATA, {}) + + +def get_display_metadata(display_id: str) -> DisplayMetaData | None: + """Get display metadata by ID for use by other components.""" + return get_all_display_metadata().get(display_id, DisplayMetaData()) + + +def add_metadata( + id: str | MockObj, + width: int, + height: int, + has_writer: bool, + has_hardware_rotation: bool = False, +): + get_all_display_metadata()[str(id)] = DisplayMetaData( + width, height, has_writer, has_hardware_rotation + ) + + async def register_display(var, config): await cg.register_component(var, config) await setup_display_core_(var, config) @@ -159,6 +198,7 @@ async def register_display(var, config): cv.Required(CONF_ID): cv.templatable(cv.use_id(DisplayPage)), } ), + synchronous=True, ) async def display_page_show_to_code(config, action_id, template_arg, args): var = cg.new_Pvariable(action_id, template_arg) @@ -179,6 +219,7 @@ async def display_page_show_to_code(config, action_id, template_arg, args): cv.GenerateID(CONF_ID): cv.templatable(cv.use_id(Display)), } ), + synchronous=True, ) async def display_page_show_next_to_code(config, action_id, template_arg, args): paren = await cg.get_variable(config[CONF_ID]) @@ -193,6 +234,7 @@ async def display_page_show_next_to_code(config, action_id, template_arg, args): cv.GenerateID(CONF_ID): cv.templatable(cv.use_id(Display)), } ), + synchronous=True, ) async def display_page_show_previous_to_code(config, action_id, template_arg, args): paren = await cg.get_variable(config[CONF_ID]) @@ -222,3 +264,8 @@ async def display_is_displaying_page_to_code(config, condition_id, template_arg, async def to_code(config): cg.add_global(display_ns.using) 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") diff --git a/esphome/components/display/display.cpp b/esphome/components/display/display.cpp index ebc3c0a9f6..f8569b6e7c 100644 --- a/esphome/components/display/display.cpp +++ b/esphome/components/display/display.cpp @@ -9,8 +9,7 @@ namespace esphome { namespace display { static const char *const TAG = "display"; -const Color COLOR_OFF(0, 0, 0, 0); -const Color COLOR_ON(255, 255, 255, 255); +// COLOR_OFF and COLOR_ON are now inline constexpr in display.h void Display::fill(Color color) { this->filled_rectangle(0, 0, this->get_width(), this->get_height(), color); } void Display::clear() { this->fill(COLOR_OFF); } @@ -662,6 +661,9 @@ void Display::printf(int x, int y, BaseFont *font, const char *format, ...) { void Display::set_writer(display_writer_t &&writer) { this->writer_ = writer; } void Display::set_pages(std::vector pages) { + if (pages.empty()) + return; + for (auto *page : pages) page->set_parent(this); @@ -811,9 +813,9 @@ bool Display::clamp_y_(int y, int h, int &min_y, int &max_y) { return min_y < max_y; } -const uint8_t TESTCARD_FONT[3][8] PROGMEM = {{0x41, 0x7F, 0x7F, 0x09, 0x19, 0x7F, 0x66, 0x00}, // 'R' - {0x1C, 0x3E, 0x63, 0x41, 0x51, 0x73, 0x72, 0x00}, // 'G' - {0x41, 0x7F, 0x7F, 0x49, 0x49, 0x7F, 0x36, 0x00}}; // 'B' +constexpr uint8_t TESTCARD_FONT[3][8] PROGMEM = {{0x41, 0x7F, 0x7F, 0x09, 0x19, 0x7F, 0x66, 0x00}, // 'R' + {0x1C, 0x3E, 0x63, 0x41, 0x51, 0x73, 0x72, 0x00}, // 'G' + {0x41, 0x7F, 0x7F, 0x49, 0x49, 0x7F, 0x36, 0x00}}; // 'B' void Display::test_card() { int w = get_width(), h = get_height(), image_w, image_h; diff --git a/esphome/components/display/display.h b/esphome/components/display/display.h index 47d40915aa..6e38300d0e 100644 --- a/esphome/components/display/display.h +++ b/esphome/components/display/display.h @@ -298,9 +298,9 @@ using display_writer_t = DisplayWriter; } /// Turn the pixel OFF. -extern const Color COLOR_OFF; +inline constexpr Color COLOR_OFF(0, 0, 0, 0); /// Turn the pixel ON. -extern const Color COLOR_ON; +inline constexpr Color COLOR_ON(255, 255, 255, 255); class BaseImage { public: @@ -704,7 +704,7 @@ class Display : public PollingComponent { void add_on_page_change_trigger(DisplayOnPageChangeTrigger *t) { this->on_page_change_triggers_.push_back(t); } /// Internal method to set the display rotation with. - void set_rotation(DisplayRotation rotation); + virtual void set_rotation(DisplayRotation rotation); // Internal method to set display auto clearing. void set_auto_clear(bool auto_clear_enabled) { this->auto_clear_enabled_ = auto_clear_enabled; } diff --git a/esphome/components/display_menu_base/__init__.py b/esphome/components/display_menu_base/__init__.py index 658292ec7a..c9a0c7ee93 100644 --- a/esphome/components/display_menu_base/__init__.py +++ b/esphome/components/display_menu_base/__init__.py @@ -294,50 +294,67 @@ MENU_ACTION_SCHEMA = maybe_simple_id( ) -@automation.register_action("display_menu.up", UpAction, MENU_ACTION_SCHEMA) +@automation.register_action( + "display_menu.up", UpAction, MENU_ACTION_SCHEMA, synchronous=True +) async def menu_up_to_code(config, action_id, template_arg, args): paren = await cg.get_variable(config[CONF_ID]) return cg.new_Pvariable(action_id, template_arg, paren) -@automation.register_action("display_menu.down", DownAction, MENU_ACTION_SCHEMA) +@automation.register_action( + "display_menu.down", DownAction, MENU_ACTION_SCHEMA, synchronous=True +) async def menu_down_to_code(config, action_id, template_arg, args): paren = await cg.get_variable(config[CONF_ID]) return cg.new_Pvariable(action_id, template_arg, paren) -@automation.register_action("display_menu.left", LeftAction, MENU_ACTION_SCHEMA) +@automation.register_action( + "display_menu.left", LeftAction, MENU_ACTION_SCHEMA, synchronous=True +) async def menu_left_to_code(config, action_id, template_arg, args): paren = await cg.get_variable(config[CONF_ID]) return cg.new_Pvariable(action_id, template_arg, paren) -@automation.register_action("display_menu.right", RightAction, MENU_ACTION_SCHEMA) +@automation.register_action( + "display_menu.right", RightAction, MENU_ACTION_SCHEMA, synchronous=True +) async def menu_right_to_code(config, action_id, template_arg, args): paren = await cg.get_variable(config[CONF_ID]) return cg.new_Pvariable(action_id, template_arg, paren) -@automation.register_action("display_menu.enter", EnterAction, MENU_ACTION_SCHEMA) +@automation.register_action( + "display_menu.enter", EnterAction, MENU_ACTION_SCHEMA, synchronous=True +) async def menu_enter_to_code(config, action_id, template_arg, args): paren = await cg.get_variable(config[CONF_ID]) return cg.new_Pvariable(action_id, template_arg, paren) -@automation.register_action("display_menu.show", ShowAction, MENU_ACTION_SCHEMA) +@automation.register_action( + "display_menu.show", ShowAction, MENU_ACTION_SCHEMA, synchronous=True +) async def menu_show_to_code(config, action_id, template_arg, args): paren = await cg.get_variable(config[CONF_ID]) return cg.new_Pvariable(action_id, template_arg, paren) -@automation.register_action("display_menu.hide", HideAction, MENU_ACTION_SCHEMA) +@automation.register_action( + "display_menu.hide", HideAction, MENU_ACTION_SCHEMA, synchronous=True +) async def menu_hide_to_code(config, action_id, template_arg, args): paren = await cg.get_variable(config[CONF_ID]) return cg.new_Pvariable(action_id, template_arg, paren) @automation.register_action( - "display_menu.show_main", ShowMainAction, MENU_ACTION_SCHEMA + "display_menu.show_main", + ShowMainAction, + MENU_ACTION_SCHEMA, + synchronous=True, ) async def menu_show_main_to_code(config, action_id, template_arg, args): paren = await cg.get_variable(config[CONF_ID]) diff --git a/esphome/components/display_menu_base/automation.h b/esphome/components/display_menu_base/automation.h index 9c64794cef..50c26c344c 100644 --- a/esphome/components/display_menu_base/automation.h +++ b/esphome/components/display_menu_base/automation.h @@ -96,37 +96,52 @@ template class IsActiveCondition : public Condition { class DisplayMenuOnEnterTrigger : public Trigger { public: - explicit DisplayMenuOnEnterTrigger(MenuItem *parent) { - parent->add_on_enter_callback([this, parent]() { this->trigger(parent); }); + explicit DisplayMenuOnEnterTrigger(MenuItem *parent) : parent_(parent) { + parent->add_on_enter_callback([this]() { this->trigger(this->parent_); }); } + + protected: + MenuItem *parent_; }; class DisplayMenuOnLeaveTrigger : public Trigger { public: - explicit DisplayMenuOnLeaveTrigger(MenuItem *parent) { - parent->add_on_leave_callback([this, parent]() { this->trigger(parent); }); + explicit DisplayMenuOnLeaveTrigger(MenuItem *parent) : parent_(parent) { + parent->add_on_leave_callback([this]() { this->trigger(this->parent_); }); } + + protected: + MenuItem *parent_; }; class DisplayMenuOnValueTrigger : public Trigger { public: - explicit DisplayMenuOnValueTrigger(MenuItem *parent) { - parent->add_on_value_callback([this, parent]() { this->trigger(parent); }); + explicit DisplayMenuOnValueTrigger(MenuItem *parent) : parent_(parent) { + parent->add_on_value_callback([this]() { this->trigger(this->parent_); }); } + + protected: + MenuItem *parent_; }; class DisplayMenuOnNextTrigger : public Trigger { public: - explicit DisplayMenuOnNextTrigger(MenuItemCustom *parent) { - parent->add_on_next_callback([this, parent]() { this->trigger(parent); }); + explicit DisplayMenuOnNextTrigger(MenuItemCustom *parent) : parent_(parent) { + parent->add_on_next_callback([this]() { this->trigger(this->parent_); }); } + + protected: + MenuItemCustom *parent_; }; class DisplayMenuOnPrevTrigger : public Trigger { public: - explicit DisplayMenuOnPrevTrigger(MenuItemCustom *parent) { - parent->add_on_prev_callback([this, parent]() { this->trigger(parent); }); + explicit DisplayMenuOnPrevTrigger(MenuItemCustom *parent) : parent_(parent) { + parent->add_on_prev_callback([this]() { this->trigger(this->parent_); }); } + + protected: + MenuItemCustom *parent_; }; } // namespace display_menu_base diff --git a/esphome/components/display_menu_base/menu_item.h b/esphome/components/display_menu_base/menu_item.h index 36de146031..57d7350b9e 100644 --- a/esphome/components/display_menu_base/menu_item.h +++ b/esphome/components/display_menu_base/menu_item.h @@ -44,9 +44,9 @@ class MenuItem { MenuItemMenu *get_parent() { return this->parent_; } MenuItemType get_type() const { return this->item_type_; } template void set_text(V val) { this->text_ = val; } - void add_on_enter_callback(std::function &&cb) { this->on_enter_callbacks_.add(std::move(cb)); } - void add_on_leave_callback(std::function &&cb) { this->on_leave_callbacks_.add(std::move(cb)); } - void add_on_value_callback(std::function &&cb) { this->on_value_callbacks_.add(std::move(cb)); } + template void add_on_enter_callback(F &&cb) { this->on_enter_callbacks_.add(std::forward(cb)); } + template void add_on_leave_callback(F &&cb) { this->on_leave_callbacks_.add(std::forward(cb)); } + template void add_on_value_callback(F &&cb) { this->on_value_callbacks_.add(std::forward(cb)); } std::string get_text() const { return const_cast(this)->text_.value(this); } virtual bool get_immediate_edit() const { return false; } @@ -170,8 +170,8 @@ class MenuItemCommand : public MenuItem { class MenuItemCustom : public MenuItemEditable { public: explicit MenuItemCustom() : MenuItemEditable(MENU_ITEM_CUSTOM) {} - void add_on_next_callback(std::function &&cb) { this->on_next_callbacks_.add(std::move(cb)); } - void add_on_prev_callback(std::function &&cb) { this->on_prev_callbacks_.add(std::move(cb)); } + template void add_on_next_callback(F &&cb) { this->on_next_callbacks_.add(std::forward(cb)); } + template void add_on_prev_callback(F &&cb) { this->on_prev_callbacks_.add(std::forward(cb)); } bool has_value() const override { return this->value_getter_.has_value(); } std::string get_value_text() const override; diff --git a/esphome/components/dlms_meter/__init__.py b/esphome/components/dlms_meter/__init__.py new file mode 100644 index 0000000000..c22ab7b552 --- /dev/null +++ b/esphome/components/dlms_meter/__init__.py @@ -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]])) diff --git a/esphome/components/dlms_meter/dlms.h b/esphome/components/dlms_meter/dlms.h new file mode 100644 index 0000000000..a3d8f62ce6 --- /dev/null +++ b/esphome/components/dlms_meter/dlms.h @@ -0,0 +1,71 @@ +#pragma once + +#include + +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 diff --git a/esphome/components/dlms_meter/dlms_meter.cpp b/esphome/components/dlms_meter/dlms_meter.cpp new file mode 100644 index 0000000000..b732e71d24 --- /dev/null +++ b/esphome/components/dlms_meter/dlms_meter.cpp @@ -0,0 +1,516 @@ +#include "dlms_meter.h" + +#include + +#if defined(USE_ESP8266_FRAMEWORK_ARDUINO) +#include +#elif defined(USE_ESP32) +#include +#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(6, 0, 0) +#include +#else +#include "mbedtls/esp_config.h" +#include "mbedtls/gcm.h" +#endif +#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: %" PRIu32 " 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 + size_t avail = this->available(); + if (avail > 0) { + size_t remaining = MBUS_MAX_FRAME_LENGTH * 2 - this->receive_buffer_.size(); + if (remaining == 0) { + ESP_LOGW(TAG, "Receive buffer full, dropping remaining bytes"); + } else { + // Read all available bytes in batches to reduce UART call overhead. + // Cap reads to remaining buffer capacity. + if (avail > remaining) { + avail = remaining; + } + uint8_t buf[64]; + while (avail > 0) { + size_t to_read = std::min(avail, sizeof(buf)); + if (!this->read_array(buf, to_read)) { + break; + } + avail -= to_read; + this->receive_buffer_.insert(this->receive_buffer_.end(), buf, buf + to_read); + this->last_read_ = millis(); + } + } + } + + if (!this->receive_buffer_.empty() && millis() - this->last_read_ > this->read_timeout_) { + this->mbus_payload_.clear(); + if (!this->parse_mbus_(this->mbus_payload_)) + return; + + uint16_t message_length; + uint8_t systitle_length; + uint16_t header_offset; + if (!this->parse_dlms_(this->mbus_payload_, message_length, systitle_length, header_offset)) + return; + + if (message_length < DECODER_START_OFFSET || message_length > MAX_MESSAGE_LENGTH) { + ESP_LOGE(TAG, "DLMS: Message length invalid: %u", message_length); + this->receive_buffer_.clear(); + return; + } + + // Decrypt in place and then decode the OBIS codes + if (!this->decrypt_(this->mbus_payload_, message_length, systitle_length, header_offset)) + return; + this->decode_obis_(&this->mbus_payload_[header_offset + DLMS_PAYLOAD_OFFSET], message_length); + } +} + +bool DlmsMeterComponent::parse_mbus_(std::vector &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 &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 &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) +#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(6, 0, 0) + // PSA Crypto multipart AEAD (no tag verification, matching legacy behavior) + psa_key_attributes_t attributes = PSA_KEY_ATTRIBUTES_INIT; + psa_set_key_type(&attributes, PSA_KEY_TYPE_AES); + psa_set_key_bits(&attributes, this->decryption_key_.size() * 8); + psa_set_key_usage_flags(&attributes, PSA_KEY_USAGE_DECRYPT); + psa_set_key_algorithm(&attributes, PSA_ALG_GCM); + + mbedtls_svc_key_id_t key_id; + bool decrypt_failed = true; + if (psa_import_key(&attributes, this->decryption_key_.data(), this->decryption_key_.size(), &key_id) == PSA_SUCCESS) { + psa_aead_operation_t op = PSA_AEAD_OPERATION_INIT; + if (psa_aead_decrypt_setup(&op, key_id, PSA_ALG_GCM) == PSA_SUCCESS && + psa_aead_set_nonce(&op, iv, sizeof(iv)) == PSA_SUCCESS) { + size_t outlen = 0; + if (psa_aead_update(&op, payload_ptr, message_length, payload_ptr, message_length, &outlen) == PSA_SUCCESS && + outlen == message_length) { + decrypt_failed = false; + } + } + psa_aead_abort(&op); + psa_destroy_key(key_id); + } + if (decrypt_failed) { + ESP_LOGE(TAG, "Decryption failed"); + this->receive_buffer_.clear(); + return false; + } +#else + size_t outlen = 0; + mbedtls_gcm_context gcm_ctx; + mbedtls_gcm_init(&gcm_ctx); + mbedtls_gcm_setkey(&gcm_ctx, MBEDTLS_CIPHER_ID_AES, this->decryption_key_.data(), this->decryption_key_.size() * 8); + mbedtls_gcm_starts(&gcm_ctx, MBEDTLS_GCM_DECRYPT, iv, sizeof(iv)); + auto ret = mbedtls_gcm_update(&gcm_ctx, payload_ptr, message_length, payload_ptr, message_length, &outlen); + mbedtls_gcm_free(&gcm_ctx); + if (ret != 0) { + ESP_LOGE(TAG, "Decryption failed with error: %d", ret); + this->receive_buffer_.clear(); + return false; + } +#endif +#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(plaintext[current_position + 1]); + if (scaler != 0) { + value *= pow10_int(scaler); + } + } + + // on EVN Meters there is no additional break + if (this->provider_ == PROVIDER_NETZNOE) { + current_position += 4; + } else { + current_position += 6; + } + } + + // Handle numeric values (LONG_UNSIGNED and DOUBLE_LONG_UNSIGNED) + if (value_size > 0) { + switch (obis_cd) { + case OBIS_VOLTAGE_L1: + data.voltage_l1 = value; + break; + case OBIS_VOLTAGE_L2: + data.voltage_l2 = value; + break; + case OBIS_VOLTAGE_L3: + data.voltage_l3 = value; + break; + case OBIS_CURRENT_L1: + data.current_l1 = value; + break; + case OBIS_CURRENT_L2: + data.current_l2 = value; + break; + case OBIS_CURRENT_L3: + data.current_l3 = value; + break; + case OBIS_ACTIVE_POWER_PLUS: + data.active_power_plus = value; + break; + case OBIS_ACTIVE_POWER_MINUS: + data.active_power_minus = value; + break; + case OBIS_ACTIVE_ENERGY_PLUS: + data.active_energy_plus = value; + break; + case OBIS_ACTIVE_ENERGY_MINUS: + data.active_energy_minus = value; + break; + case OBIS_REACTIVE_ENERGY_PLUS: + data.reactive_energy_plus = value; + break; + case OBIS_REACTIVE_ENERGY_MINUS: + data.reactive_energy_minus = value; + break; + case OBIS_POWER_FACTOR: + data.power_factor = value; + power_factor_found = true; + break; + default: + ESP_LOGW(TAG, "Unsupported OBIS code 0x%04X", obis_cd); + } + } + } + + this->receive_buffer_.clear(); + + ESP_LOGI(TAG, "Received valid data"); + this->publish_sensors(data); + this->status_clear_warning(); +} + +} // namespace esphome::dlms_meter diff --git a/esphome/components/dlms_meter/dlms_meter.h b/esphome/components/dlms_meter/dlms_meter.h new file mode 100644 index 0000000000..c50e6f6b4d --- /dev/null +++ b/esphome/components/dlms_meter/dlms_meter.h @@ -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 +#include + +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 &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 &mbus_payload); + bool parse_dlms_(const std::vector &mbus_payload, uint16_t &message_length, uint8_t &systitle_length, + uint16_t &header_offset); + bool decrypt_(std::vector &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 receive_buffer_; // Stores the packet currently being received + std::vector 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 decryption_key_; +}; + +} // namespace esphome::dlms_meter diff --git a/esphome/components/dlms_meter/mbus.h b/esphome/components/dlms_meter/mbus.h new file mode 100644 index 0000000000..293d43a55b --- /dev/null +++ b/esphome/components/dlms_meter/mbus.h @@ -0,0 +1,69 @@ +#pragma once + +#include + +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 diff --git a/esphome/components/dlms_meter/obis.h b/esphome/components/dlms_meter/obis.h new file mode 100644 index 0000000000..1bb960e61e --- /dev/null +++ b/esphome/components/dlms_meter/obis.h @@ -0,0 +1,94 @@ +#pragma once + +#include + +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 diff --git a/esphome/components/dlms_meter/sensor/__init__.py b/esphome/components/dlms_meter/sensor/__init__.py new file mode 100644 index 0000000000..27fd44f008 --- /dev/null +++ b/esphome/components/dlms_meter/sensor/__init__.py @@ -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)) + ) diff --git a/esphome/components/dlms_meter/text_sensor/__init__.py b/esphome/components/dlms_meter/text_sensor/__init__.py new file mode 100644 index 0000000000..4d2373f4f9 --- /dev/null +++ b/esphome/components/dlms_meter/text_sensor/__init__.py @@ -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)), + ) diff --git a/esphome/components/dps310/dps310.cpp b/esphome/components/dps310/dps310.cpp index 6b6f9622fa..b1366cd069 100644 --- a/esphome/components/dps310/dps310.cpp +++ b/esphome/components/dps310/dps310.cpp @@ -98,8 +98,6 @@ void DPS310Component::dump_config() { LOG_SENSOR(" ", "Pressure", this->pressure_sensor_); } -float DPS310Component::get_setup_priority() const { return setup_priority::DATA; } - void DPS310Component::update() { if (!this->update_in_progress_) { this->update_in_progress_ = true; @@ -129,8 +127,7 @@ void DPS310Component::read_() { this->update_in_progress_ = false; this->status_clear_warning(); } else { - auto f = std::bind(&DPS310Component::read_, this); - this->set_timeout("dps310", 10, f); + this->set_timeout("dps310", 10, [this]() { this->read_(); }); } } diff --git a/esphome/components/dps310/dps310.h b/esphome/components/dps310/dps310.h index 50e7d93c8a..dce220d44b 100644 --- a/esphome/components/dps310/dps310.h +++ b/esphome/components/dps310/dps310.h @@ -40,7 +40,6 @@ class DPS310Component : public PollingComponent, public i2c::I2CDevice { public: void setup() override; void dump_config() override; - float get_setup_priority() const override; void update() override; void set_temperature_sensor(sensor::Sensor *temperature_sensor) { temperature_sensor_ = temperature_sensor; } diff --git a/esphome/components/ds1307/ds1307.cpp b/esphome/components/ds1307/ds1307.cpp index adbd7b5487..8fff4213b4 100644 --- a/esphome/components/ds1307/ds1307.cpp +++ b/esphome/components/ds1307/ds1307.cpp @@ -26,8 +26,6 @@ void DS1307Component::dump_config() { RealTimeClock::dump_config(); } -float DS1307Component::get_setup_priority() const { return setup_priority::DATA; } - void DS1307Component::read_time() { if (!this->read_rtc_()) { return; @@ -42,11 +40,8 @@ void DS1307Component::read_time() { .hour = uint8_t(ds1307_.reg.hour + 10u * ds1307_.reg.hour_10), .day_of_week = uint8_t(ds1307_.reg.weekday), .day_of_month = uint8_t(ds1307_.reg.day + 10u * ds1307_.reg.day_10), - .day_of_year = 1, // ignored by recalc_timestamp_utc(false) .month = uint8_t(ds1307_.reg.month + 10u * ds1307_.reg.month_10), .year = uint16_t(ds1307_.reg.year + 10u * ds1307_.reg.year_10 + 2000), - .is_dst = false, // not used - .timestamp = 0 // overwritten by recalc_timestamp_utc(false) }; rtc_time.recalc_timestamp_utc(false); if (!rtc_time.is_valid()) { diff --git a/esphome/components/ds1307/ds1307.h b/esphome/components/ds1307/ds1307.h index f7f06253b7..1712056006 100644 --- a/esphome/components/ds1307/ds1307.h +++ b/esphome/components/ds1307/ds1307.h @@ -12,7 +12,6 @@ class DS1307Component : public time::RealTimeClock, public i2c::I2CDevice { void setup() override; void update() override; void dump_config() override; - float get_setup_priority() const override; void read_time(); void write_time(); diff --git a/esphome/components/ds1307/time.py b/esphome/components/ds1307/time.py index 42b7184db9..0e7bb976a2 100644 --- a/esphome/components/ds1307/time.py +++ b/esphome/components/ds1307/time.py @@ -27,6 +27,7 @@ CONFIG_SCHEMA = time.TIME_SCHEMA.extend( cv.GenerateID(): cv.use_id(DS1307Component), } ), + synchronous=True, ) async def ds1307_write_time_to_code(config, action_id, template_arg, args): var = cg.new_Pvariable(action_id, template_arg) @@ -42,6 +43,7 @@ async def ds1307_write_time_to_code(config, action_id, template_arg, args): cv.GenerateID(): cv.use_id(DS1307Component), } ), + synchronous=True, ) async def ds1307_read_time_to_code(config, action_id, template_arg, args): var = cg.new_Pvariable(action_id, template_arg) diff --git a/esphome/components/ds2484/ds2484.cpp b/esphome/components/ds2484/ds2484.cpp index 7c890ff433..0b36f86874 100644 --- a/esphome/components/ds2484/ds2484.cpp +++ b/esphome/components/ds2484/ds2484.cpp @@ -110,9 +110,9 @@ uint8_t DS2484OneWireBus::read8() { } uint64_t DS2484OneWireBus::read64() { - uint8_t response = 0; + uint64_t response = 0; for (uint8_t i = 0; i < 8; i++) { - response |= (this->read8() << (i * 8)); + response |= (static_cast(this->read8()) << (i * 8)); } return response; } diff --git a/esphome/components/dsmr/__init__.py b/esphome/components/dsmr/__init__.py index 386da3ce21..b1ff9794a3 100644 --- a/esphome/components/dsmr/__init__.py +++ b/esphome/components/dsmr/__init__.py @@ -4,7 +4,7 @@ from esphome.components import uart import esphome.config_validation as cv from esphome.const import CONF_ID, CONF_RECEIVE_TIMEOUT, CONF_UART_ID -CODEOWNERS = ["@glmnet", "@zuidwijk", "@PolarGoose"] +CODEOWNERS = ["@glmnet", "@PolarGoose"] MULTI_CONF = True @@ -16,6 +16,7 @@ CONF_DECRYPTION_KEY = "decryption_key" CONF_DSMR_ID = "dsmr_id" CONF_GAS_MBUS_ID = "gas_mbus_id" CONF_WATER_MBUS_ID = "water_mbus_id" +CONF_THERMAL_MBUS_ID = "thermal_mbus_id" CONF_MAX_TELEGRAM_LENGTH = "max_telegram_length" CONF_REQUEST_INTERVAL = "request_interval" CONF_REQUEST_PIN = "request_pin" @@ -35,6 +36,7 @@ CONFIG_SCHEMA = cv.All( cv.Optional(CONF_CRC_CHECK, default=True): cv.boolean, cv.Optional(CONF_GAS_MBUS_ID, default=1): cv.int_, cv.Optional(CONF_WATER_MBUS_ID, default=2): cv.int_, + cv.Optional(CONF_THERMAL_MBUS_ID, default=3): cv.int_, cv.Optional(CONF_MAX_TELEGRAM_LENGTH, default=1500): cv.int_, cv.Optional(CONF_REQUEST_PIN): pins.gpio_output_pin_schema, cv.Optional( @@ -64,9 +66,10 @@ async def to_code(config): cg.add_build_flag("-DDSMR_GAS_MBUS_ID=" + str(config[CONF_GAS_MBUS_ID])) cg.add_build_flag("-DDSMR_WATER_MBUS_ID=" + str(config[CONF_WATER_MBUS_ID])) + cg.add_build_flag("-DDSMR_THERMAL_MBUS_ID=" + str(config[CONF_THERMAL_MBUS_ID])) # DSMR Parser - cg.add_library("esphome/dsmr_parser", "1.0.0") + cg.add_library("esphome/dsmr_parser", "1.1.0") # Crypto cg.add_library("polargoose/Crypto-no-arduino", "0.4.0") diff --git a/esphome/components/dsmr/dsmr.cpp b/esphome/components/dsmr/dsmr.cpp index c78d37bf5e..baf7f59314 100644 --- a/esphome/components/dsmr/dsmr.cpp +++ b/esphome/components/dsmr/dsmr.cpp @@ -40,9 +40,7 @@ bool Dsmr::ready_to_request_data_() { this->start_requesting_data_(); } if (!this->requesting_data_) { - while (this->available()) { - this->read(); - } + this->drain_rx_buffer_(); } } return this->requesting_data_; @@ -115,138 +113,169 @@ void Dsmr::stop_requesting_data_() { } else { ESP_LOGV(TAG, "Stop reading data from P1 port"); } - while (this->available()) { - this->read(); - } + this->drain_rx_buffer_(); this->requesting_data_ = false; } } +void Dsmr::drain_rx_buffer_() { + uint8_t buf[64]; + size_t avail; + while ((avail = this->available()) > 0) { + if (!this->read_array(buf, std::min(avail, sizeof(buf)))) { + break; + } + } +} + void Dsmr::reset_telegram_() { this->header_found_ = false; this->footer_found_ = false; this->bytes_read_ = 0; this->crypt_bytes_read_ = 0; this->crypt_telegram_len_ = 0; - this->last_read_time_ = 0; } void Dsmr::receive_telegram_() { while (this->available_within_timeout_()) { - const char c = this->read(); + // Read all available bytes in batches to reduce UART call overhead. + uint8_t buf[64]; + size_t avail = this->available(); + while (avail > 0) { + size_t to_read = std::min(avail, sizeof(buf)); + if (!this->read_array(buf, to_read)) + return; + avail -= to_read; - // Find a new telegram header, i.e. forward slash. - if (c == '/') { - ESP_LOGV(TAG, "Header of telegram found"); - this->reset_telegram_(); - this->header_found_ = true; - } - if (!this->header_found_) - continue; + for (size_t i = 0; i < to_read; i++) { + const char c = static_cast(buf[i]); - // Check for buffer overflow. - if (this->bytes_read_ >= this->max_telegram_len_) { - this->reset_telegram_(); - ESP_LOGE(TAG, "Error: telegram larger than buffer (%d bytes)", this->max_telegram_len_); - return; - } + // Find a new telegram header, i.e. forward slash. + if (c == '/') { + ESP_LOGV(TAG, "Header of telegram found"); + this->reset_telegram_(); + this->header_found_ = true; + } + if (!this->header_found_) + continue; - // Some v2.2 or v3 meters will send a new value which starts with '(' - // in a new line, while the value belongs to the previous ObisId. For - // proper parsing, remove these new line characters. - if (c == '(') { - while (true) { - auto previous_char = this->telegram_[this->bytes_read_ - 1]; - if (previous_char == '\n' || previous_char == '\r') { - this->bytes_read_--; - } else { - break; + // Check for buffer overflow. + if (this->bytes_read_ >= this->max_telegram_len_) { + this->reset_telegram_(); + ESP_LOGE(TAG, "Error: telegram larger than buffer (%d bytes)", this->max_telegram_len_); + return; + } + + // Some v2.2 or v3 meters will send a new value which starts with '(' + // in a new line, while the value belongs to the previous ObisId. For + // proper parsing, remove these new line characters. + if (c == '(') { + while (true) { + auto previous_char = this->telegram_[this->bytes_read_ - 1]; + if (previous_char == '\n' || previous_char == '\r') { + this->bytes_read_--; + } else { + break; + } + } + } + + // Store the byte in the buffer. + this->telegram_[this->bytes_read_] = c; + this->bytes_read_++; + + // Check for a footer, i.e. exclamation mark, followed by a hex checksum. + if (c == '!') { + ESP_LOGV(TAG, "Footer of telegram found"); + this->footer_found_ = true; + continue; + } + // Check for the end of the hex checksum, i.e. a newline. + if (this->footer_found_ && c == '\n') { + // Parse the telegram and publish sensor values. + this->parse_telegram(); + this->reset_telegram_(); + return; } } } - - // Store the byte in the buffer. - this->telegram_[this->bytes_read_] = c; - this->bytes_read_++; - - // Check for a footer, i.e. exclamation mark, followed by a hex checksum. - if (c == '!') { - ESP_LOGV(TAG, "Footer of telegram found"); - this->footer_found_ = true; - continue; - } - // Check for the end of the hex checksum, i.e. a newline. - if (this->footer_found_ && c == '\n') { - // Parse the telegram and publish sensor values. - this->parse_telegram(); - this->reset_telegram_(); - return; - } } } void Dsmr::receive_encrypted_telegram_() { while (this->available_within_timeout_()) { - const char c = this->read(); + // Read all available bytes in batches to reduce UART call overhead. + uint8_t buf[64]; + size_t avail = this->available(); + while (avail > 0) { + size_t to_read = std::min(avail, sizeof(buf)); + if (!this->read_array(buf, to_read)) + return; + avail -= to_read; - // Find a new telegram start byte. - if (!this->header_found_) { - if ((uint8_t) c != 0xDB) { - continue; + for (size_t i = 0; i < to_read; i++) { + const char c = static_cast(buf[i]); + + // Find a new telegram start byte. + if (!this->header_found_) { + if ((uint8_t) c != 0xDB) { + continue; + } + ESP_LOGV(TAG, "Start byte 0xDB of encrypted telegram found"); + this->reset_telegram_(); + this->header_found_ = true; + } + + // Check for buffer overflow. + if (this->crypt_bytes_read_ >= this->max_telegram_len_) { + this->reset_telegram_(); + ESP_LOGE(TAG, "Error: encrypted telegram larger than buffer (%d bytes)", this->max_telegram_len_); + return; + } + + // Store the byte in the buffer. + this->crypt_telegram_[this->crypt_bytes_read_] = c; + this->crypt_bytes_read_++; + + // Read the length of the incoming encrypted telegram. + if (this->crypt_telegram_len_ == 0 && this->crypt_bytes_read_ > 20) { + // Complete header + data bytes + this->crypt_telegram_len_ = 13 + (this->crypt_telegram_[11] << 8 | this->crypt_telegram_[12]); + ESP_LOGV(TAG, "Encrypted telegram length: %d bytes", this->crypt_telegram_len_); + } + + // Check for the end of the encrypted telegram. + if (this->crypt_telegram_len_ == 0 || this->crypt_bytes_read_ != this->crypt_telegram_len_) { + continue; + } + ESP_LOGV(TAG, "End of encrypted telegram found"); + + // Decrypt the encrypted telegram. + GCM *gcmaes128{new GCM()}; + gcmaes128->setKey(this->decryption_key_.data(), gcmaes128->keySize()); + // the iv is 8 bytes of the system title + 4 bytes frame counter + // system title is at byte 2 and frame counter at byte 15 + for (int i = 10; i < 14; i++) + this->crypt_telegram_[i] = this->crypt_telegram_[i + 4]; + constexpr uint16_t iv_size{12}; + gcmaes128->setIV(&this->crypt_telegram_[2], iv_size); + gcmaes128->decrypt(reinterpret_cast(this->telegram_), + // the ciphertext start at byte 18 + &this->crypt_telegram_[18], + // cipher size + this->crypt_bytes_read_ - 17); + delete gcmaes128; // NOLINT(cppcoreguidelines-owning-memory) + + this->bytes_read_ = strnlen(this->telegram_, this->max_telegram_len_); + ESP_LOGV(TAG, "Decrypted telegram size: %d bytes", this->bytes_read_); + ESP_LOGVV(TAG, "Decrypted telegram: %s", this->telegram_); + + // Parse the decrypted telegram and publish sensor values. + this->parse_telegram(); + this->reset_telegram_(); + return; } - ESP_LOGV(TAG, "Start byte 0xDB of encrypted telegram found"); - this->reset_telegram_(); - this->header_found_ = true; } - - // Check for buffer overflow. - if (this->crypt_bytes_read_ >= this->max_telegram_len_) { - this->reset_telegram_(); - ESP_LOGE(TAG, "Error: encrypted telegram larger than buffer (%d bytes)", this->max_telegram_len_); - return; - } - - // Store the byte in the buffer. - this->crypt_telegram_[this->crypt_bytes_read_] = c; - this->crypt_bytes_read_++; - - // Read the length of the incoming encrypted telegram. - if (this->crypt_telegram_len_ == 0 && this->crypt_bytes_read_ > 20) { - // Complete header + data bytes - this->crypt_telegram_len_ = 13 + (this->crypt_telegram_[11] << 8 | this->crypt_telegram_[12]); - ESP_LOGV(TAG, "Encrypted telegram length: %d bytes", this->crypt_telegram_len_); - } - - // Check for the end of the encrypted telegram. - if (this->crypt_telegram_len_ == 0 || this->crypt_bytes_read_ != this->crypt_telegram_len_) { - continue; - } - ESP_LOGV(TAG, "End of encrypted telegram found"); - - // Decrypt the encrypted telegram. - GCM *gcmaes128{new GCM()}; - gcmaes128->setKey(this->decryption_key_.data(), gcmaes128->keySize()); - // the iv is 8 bytes of the system title + 4 bytes frame counter - // system title is at byte 2 and frame counter at byte 15 - for (int i = 10; i < 14; i++) - this->crypt_telegram_[i] = this->crypt_telegram_[i + 4]; - constexpr uint16_t iv_size{12}; - gcmaes128->setIV(&this->crypt_telegram_[2], iv_size); - gcmaes128->decrypt(reinterpret_cast(this->telegram_), - // the ciphertext start at byte 18 - &this->crypt_telegram_[18], - // cipher size - this->crypt_bytes_read_ - 17); - delete gcmaes128; // NOLINT(cppcoreguidelines-owning-memory) - - this->bytes_read_ = strnlen(this->telegram_, this->max_telegram_len_); - ESP_LOGV(TAG, "Decrypted telegram size: %d bytes", this->bytes_read_); - ESP_LOGVV(TAG, "Decrypted telegram: %s", this->telegram_); - - // Parse the decrypted telegram and publish sensor values. - this->parse_telegram(); - this->reset_telegram_(); - return; } } diff --git a/esphome/components/dsmr/dsmr.h b/esphome/components/dsmr/dsmr.h index b7e05a22b3..dc81ba9b2a 100644 --- a/esphome/components/dsmr/dsmr.h +++ b/esphome/components/dsmr/dsmr.h @@ -64,6 +64,9 @@ class Dsmr : public Component, public uart::UARTDevice { void dump_config() override; void set_decryption_key(const char *decryption_key); + // Remove before 2026.8.0 + ESPDEPRECATED("Pass .c_str() - e.g. set_decryption_key(key.c_str()). Removed in 2026.8.0", "2026.2.0") + void set_decryption_key(const std::string &decryption_key) { this->set_decryption_key(decryption_key.c_str()); } void set_max_telegram_length(size_t length) { this->max_telegram_len_ = length; } void set_request_pin(GPIOPin *request_pin) { this->request_pin_ = request_pin; } void set_request_interval(uint32_t interval) { this->request_interval_ = interval; } @@ -85,6 +88,7 @@ class Dsmr : public Component, public uart::UARTDevice { void receive_telegram_(); void receive_encrypted_telegram_(); void reset_telegram_(); + void drain_rx_buffer_(); /// Wait for UART data to become available within the read timeout. /// diff --git a/esphome/components/dsmr/sensor.py b/esphome/components/dsmr/sensor.py index 7d69f79530..863af42d1b 100644 --- a/esphome/components/dsmr/sensor.py +++ b/esphome/components/dsmr/sensor.py @@ -718,14 +718,6 @@ CONFIG_SCHEMA = cv.Schema( device_class=DEVICE_CLASS_POWER, state_class=STATE_CLASS_MEASUREMENT, ), - cv.Optional("fw_core_version"): sensor.sensor_schema( - accuracy_decimals=3, - state_class=STATE_CLASS_MEASUREMENT, - ), - cv.Optional("fw_module_version"): sensor.sensor_schema( - accuracy_decimals=3, - state_class=STATE_CLASS_MEASUREMENT, - ), } ).extend(cv.COMPONENT_SCHEMA) diff --git a/esphome/components/dsmr/text_sensor.py b/esphome/components/dsmr/text_sensor.py index 4c7455a38f..203c9c997e 100644 --- a/esphome/components/dsmr/text_sensor.py +++ b/esphome/components/dsmr/text_sensor.py @@ -26,7 +26,9 @@ CONFIG_SCHEMA = cv.Schema( cv.Optional("sub_equipment_id"): text_sensor.text_sensor_schema(), cv.Optional("gas_delivered_text"): text_sensor.text_sensor_schema(), cv.Optional("fw_core_checksum"): text_sensor.text_sensor_schema(), + cv.Optional("fw_core_version"): text_sensor.text_sensor_schema(), cv.Optional("fw_module_checksum"): text_sensor.text_sensor_schema(), + cv.Optional("fw_module_version"): text_sensor.text_sensor_schema(), cv.Optional("telegram"): text_sensor.text_sensor_schema().extend( {cv.Optional(CONF_INTERNAL, default=True): cv.boolean} ), diff --git a/esphome/components/duty_cycle/duty_cycle_sensor.cpp b/esphome/components/duty_cycle/duty_cycle_sensor.cpp index 40a728d025..f801769d27 100644 --- a/esphome/components/duty_cycle/duty_cycle_sensor.cpp +++ b/esphome/components/duty_cycle/duty_cycle_sensor.cpp @@ -43,8 +43,6 @@ void DutyCycleSensor::update() { this->last_update_ = now; } -float DutyCycleSensor::get_setup_priority() const { return setup_priority::DATA; } - void IRAM_ATTR DutyCycleSensorStore::gpio_intr(DutyCycleSensorStore *arg) { const bool new_level = arg->pin.digital_read(); if (new_level == arg->last_level) diff --git a/esphome/components/duty_cycle/duty_cycle_sensor.h b/esphome/components/duty_cycle/duty_cycle_sensor.h index ffb1802e14..ffb8e3b622 100644 --- a/esphome/components/duty_cycle/duty_cycle_sensor.h +++ b/esphome/components/duty_cycle/duty_cycle_sensor.h @@ -22,7 +22,6 @@ class DutyCycleSensor : public sensor::Sensor, public PollingComponent { void set_pin(InternalGPIOPin *pin) { pin_ = pin; } void setup() override; - float get_setup_priority() const override; void dump_config() override; void update() override; diff --git a/esphome/components/duty_time/duty_time_sensor.h b/esphome/components/duty_time/duty_time_sensor.h index d9fb2a6d60..d21802ebb6 100644 --- a/esphome/components/duty_time/duty_time_sensor.h +++ b/esphome/components/duty_time/duty_time_sensor.h @@ -41,9 +41,9 @@ class DutyTimeSensor : public sensor::Sensor, public PollingComponent { sensor::Sensor *last_duty_time_sensor_{nullptr}; ESPPreferenceObject pref_; - uint32_t total_sec_; - uint32_t last_time_; - uint32_t edge_time_; + uint32_t total_sec_{0}; + uint32_t last_time_{0}; + uint32_t edge_time_{0}; bool last_state_{false}; bool restore_; }; diff --git a/esphome/components/duty_time/sensor.py b/esphome/components/duty_time/sensor.py index 1907b3fcfe..456859f8e4 100644 --- a/esphome/components/duty_time/sensor.py +++ b/esphome/components/duty_time/sensor.py @@ -90,21 +90,27 @@ DUTY_TIME_ID_SCHEMA = maybe_simple_id( ) -@register_action("sensor.duty_time.start", StartAction, DUTY_TIME_ID_SCHEMA) +@register_action( + "sensor.duty_time.start", StartAction, DUTY_TIME_ID_SCHEMA, synchronous=True +) async def sensor_runtime_start_to_code(config, action_id, template_arg, args): var = cg.new_Pvariable(action_id, template_arg) await cg.register_parented(var, config[CONF_ID]) return var -@register_action("sensor.duty_time.stop", StopAction, DUTY_TIME_ID_SCHEMA) +@register_action( + "sensor.duty_time.stop", StopAction, DUTY_TIME_ID_SCHEMA, synchronous=True +) async def sensor_runtime_stop_to_code(config, action_id, template_arg, args): var = cg.new_Pvariable(action_id, template_arg) await cg.register_parented(var, config[CONF_ID]) return var -@register_action("sensor.duty_time.reset", ResetAction, DUTY_TIME_ID_SCHEMA) +@register_action( + "sensor.duty_time.reset", ResetAction, DUTY_TIME_ID_SCHEMA, synchronous=True +) async def sensor_runtime_reset_to_code(config, action_id, template_arg, args): var = cg.new_Pvariable(action_id, template_arg) await cg.register_parented(var, config[CONF_ID]) diff --git a/esphome/components/e131/e131.cpp b/esphome/components/e131/e131.cpp index f11e7f4fe3..a7a695c167 100644 --- a/esphome/components/e131/e131.cpp +++ b/esphome/components/e131/e131.cpp @@ -14,12 +14,17 @@ static const int PORT = 5568; E131Component::E131Component() {} E131Component::~E131Component() { +#if defined(USE_SOCKET_IMPL_BSD_SOCKETS) || defined(USE_SOCKET_IMPL_LWIP_SOCKETS) if (this->socket_) { this->socket_->close(); } +#elif defined(USE_SOCKET_IMPL_LWIP_TCP) + this->udp_.stop(); +#endif } void E131Component::setup() { +#if defined(USE_SOCKET_IMPL_BSD_SOCKETS) || defined(USE_SOCKET_IMPL_LWIP_SOCKETS) this->socket_ = socket::socket_ip(SOCK_DGRAM, IPPROTO_IP); int enable = 1; @@ -50,30 +55,35 @@ void E131Component::setup() { this->mark_failed(); return; } +#elif defined(USE_SOCKET_IMPL_LWIP_TCP) + if (!this->udp_.begin(PORT)) { + ESP_LOGW(TAG, "Cannot bind E1.31 to port %d.", PORT); + this->mark_failed(); + return; + } +#endif join_igmp_groups_(); } void E131Component::loop() { - std::vector payload; E131Packet packet; int universe = 0; uint8_t buf[1460]; + ssize_t len; - ssize_t len = this->socket_->read(buf, sizeof(buf)); - if (len == -1) { - return; - } - payload.resize(len); - memmove(&payload[0], buf, len); + // Drain all queued packets so multi-universe frames are applied + // atomically before the light writes. Without this, each universe + // packet would trigger a separate full-strip write causing tearing. + while ((len = this->read_(buf, sizeof(buf))) > 0) { + if (!this->packet_(buf, (size_t) len, universe, packet)) { + ESP_LOGV(TAG, "Invalid packet received of size %d.", (int) len); + continue; + } - if (!this->packet_(payload, universe, packet)) { - ESP_LOGV(TAG, "Invalid packet received of size %zu.", payload.size()); - return; - } - - if (!this->process_(universe, packet)) { - ESP_LOGV(TAG, "Ignored packet for %d universe of size %d.", universe, packet.count); + if (!this->process_(universe, packet)) { + ESP_LOGV(TAG, "Ignored packet for %d universe of size %d.", universe, packet.count); + } } } diff --git a/esphome/components/e131/e131.h b/esphome/components/e131/e131.h index 831138a545..8f0b808946 100644 --- a/esphome/components/e131/e131.h +++ b/esphome/components/e131/e131.h @@ -1,11 +1,14 @@ #pragma once #include "esphome/core/defines.h" #ifdef USE_NETWORK +#if defined(USE_SOCKET_IMPL_BSD_SOCKETS) || defined(USE_SOCKET_IMPL_LWIP_SOCKETS) #include "esphome/components/socket/socket.h" +#elif defined(USE_SOCKET_IMPL_LWIP_TCP) +#include +#endif #include "esphome/core/component.h" #include -#include #include #include @@ -23,6 +26,11 @@ struct E131Packet { uint8_t values[E131_MAX_PROPERTY_VALUES_COUNT]; }; +struct UniverseConsumer { + uint16_t universe; + uint16_t consumers; +}; + class E131Component : public esphome::Component { public: E131Component(); @@ -38,16 +46,30 @@ class E131Component : public esphome::Component { void set_method(E131ListenMethod listen_method) { this->listen_method_ = listen_method; } protected: - bool packet_(const std::vector &data, int &universe, E131Packet &packet); + inline ssize_t read_(uint8_t *buf, size_t len) { +#if defined(USE_SOCKET_IMPL_BSD_SOCKETS) || defined(USE_SOCKET_IMPL_LWIP_SOCKETS) + return this->socket_->read(buf, len); +#elif defined(USE_SOCKET_IMPL_LWIP_TCP) + if (!this->udp_.parsePacket()) + return -1; + return this->udp_.read(buf, len); +#endif + } + bool packet_(const uint8_t *data, size_t len, int &universe, E131Packet &packet); bool process_(int universe, const E131Packet &packet); bool join_igmp_groups_(); + UniverseConsumer *find_universe_(int universe); void join_(int universe); void leave_(int universe); E131ListenMethod listen_method_{E131_MULTICAST}; +#if defined(USE_SOCKET_IMPL_BSD_SOCKETS) || defined(USE_SOCKET_IMPL_LWIP_SOCKETS) std::unique_ptr socket_; +#elif defined(USE_SOCKET_IMPL_LWIP_TCP) + WiFiUDP udp_; +#endif std::vector light_effects_; - std::map universe_consumers_; + std::vector universe_consumers_; }; } // namespace e131 diff --git a/esphome/components/e131/e131_addressable_light_effect.cpp b/esphome/components/e131/e131_addressable_light_effect.cpp index 7d62f739a2..f6010a7cc9 100644 --- a/esphome/components/e131/e131_addressable_light_effect.cpp +++ b/esphome/components/e131/e131_addressable_light_effect.cpp @@ -54,8 +54,10 @@ bool E131AddressableLightEffect::process_(int universe, const E131Packet &packet int32_t output_offset = (universe - first_universe_) * get_lights_per_universe(); // limit amount of lights per universe and received + // packet.count is the number of DMX bytes including start code; divide by channels to get the number of lights + int lights_in_packet = (packet.count > 0) ? (packet.count - 1) / channels_ : 0; int output_end = - std::min(it->size(), std::min(output_offset + get_lights_per_universe(), output_offset + packet.count - 1)); + std::min(it->size(), std::min(output_offset + get_lights_per_universe(), output_offset + lights_in_packet)); auto *input_data = packet.values + 1; auto effect_name = get_name(); diff --git a/esphome/components/e131/e131_packet.cpp b/esphome/components/e131/e131_packet.cpp index e663a3d0fc..600793f5d3 100644 --- a/esphome/components/e131/e131_packet.cpp +++ b/esphome/components/e131/e131_packet.cpp @@ -1,3 +1,4 @@ +#include #include #include "e131.h" #ifdef USE_NETWORK @@ -57,20 +58,22 @@ union E131RawPacket { // We need to have at least one `1` value // Get the offset of `property_values[1]` -const size_t E131_MIN_PACKET_SIZE = reinterpret_cast(&((E131RawPacket *) nullptr)->property_values[1]); +const size_t E131_MIN_PACKET_SIZE = offsetof(E131RawPacket, property_values) + sizeof(uint8_t); bool E131Component::join_igmp_groups_() { - if (listen_method_ != E131_MULTICAST) + if (this->listen_method_ != E131_MULTICAST) return false; +#if defined(USE_SOCKET_IMPL_BSD_SOCKETS) || defined(USE_SOCKET_IMPL_LWIP_SOCKETS) if (this->socket_ == nullptr) return false; +#endif - for (auto universe : universe_consumers_) { - if (!universe.second) + for (auto &entry : this->universe_consumers_) { + if (!entry.consumers) continue; ip4_addr_t multicast_addr = - network::IPAddress(239, 255, ((universe.first >> 8) & 0xff), ((universe.first >> 0) & 0xff)); + network::IPAddress(239, 255, ((entry.universe >> 8) & 0xff), ((entry.universe >> 0) & 0xff)); err_t err; { @@ -79,34 +82,47 @@ bool E131Component::join_igmp_groups_() { } if (err) { - ESP_LOGW(TAG, "IGMP join for %d universe of E1.31 failed. Multicast might not work.", universe.first); + ESP_LOGW(TAG, "IGMP join for %d universe of E1.31 failed. Multicast might not work.", entry.universe); } } return true; } +UniverseConsumer *E131Component::find_universe_(int universe) { + for (auto &entry : this->universe_consumers_) { + if (entry.universe == universe) + return &entry; + } + return nullptr; +} + void E131Component::join_(int universe) { // store only latest received packet for the given universe - auto consumers = ++universe_consumers_[universe]; - - if (consumers > 1) { - return; // we already joined before + auto *consumer = this->find_universe_(universe); + if (consumer != nullptr) { + if (consumer->consumers++ > 0) { + return; // we already joined before + } + } else { + this->universe_consumers_.push_back({static_cast(universe), 1}); } - if (join_igmp_groups_()) { + if (this->join_igmp_groups_()) { ESP_LOGD(TAG, "Joined %d universe for E1.31.", universe); } } void E131Component::leave_(int universe) { - auto consumers = --universe_consumers_[universe]; + auto *consumer = this->find_universe_(universe); + if (consumer == nullptr) + return; - if (consumers > 0) { + if (--consumer->consumers > 0) { return; // we have other consumers of the given universe } - if (listen_method_ == E131_MULTICAST) { + if (this->listen_method_ == E131_MULTICAST) { ip4_addr_t multicast_addr = network::IPAddress(239, 255, ((universe >> 8) & 0xff), ((universe >> 0) & 0xff)); LwIPLock lock; @@ -116,11 +132,11 @@ void E131Component::leave_(int universe) { ESP_LOGD(TAG, "Left %d universe for E1.31.", universe); } -bool E131Component::packet_(const std::vector &data, int &universe, E131Packet &packet) { - if (data.size() < E131_MIN_PACKET_SIZE) +bool E131Component::packet_(const uint8_t *data, size_t len, int &universe, E131Packet &packet) { + if (len < E131_MIN_PACKET_SIZE) return false; - auto *sbuff = reinterpret_cast(&data[0]); + auto *sbuff = reinterpret_cast(data); if (memcmp(sbuff->acn_id, ACN_ID, sizeof(sbuff->acn_id)) != 0) return false; diff --git a/esphome/components/ee895/ee895.cpp b/esphome/components/ee895/ee895.cpp index 602e31db14..93e5d4203b 100644 --- a/esphome/components/ee895/ee895.cpp +++ b/esphome/components/ee895/ee895.cpp @@ -24,7 +24,7 @@ void EE895Component::setup() { this->read(serial_number, 20); crc16_check = (serial_number[19] << 8) + serial_number[18]; - if (crc16_check != calc_crc16_(serial_number, 19)) { + if (crc16_check != calc_crc16_(serial_number, 18)) { this->error_code_ = CRC_CHECK_FAILED; this->mark_failed(); return; @@ -55,8 +55,6 @@ void EE895Component::dump_config() { LOG_SENSOR(" ", "Pressure", this->pressure_sensor_); } -float EE895Component::get_setup_priority() const { return setup_priority::DATA; } - void EE895Component::update() { write_command_(TEMPERATURE_ADDRESS, 2); this->set_timeout(50, [this]() { @@ -86,7 +84,7 @@ void EE895Component::write_command_(uint16_t addr, uint16_t reg_cnt) { address[2] = addr & 0xFF; address[3] = (reg_cnt >> 8) & 0xFF; address[4] = reg_cnt & 0xFF; - crc16 = calc_crc16_(address, 6); + crc16 = calc_crc16_(address, 5); address[5] = crc16 & 0xFF; address[6] = (crc16 >> 8) & 0xFF; this->write(address, 7); @@ -97,7 +95,7 @@ float EE895Component::read_float_() { uint8_t i2c_response[8]; this->read(i2c_response, 8); crc16_check = (i2c_response[7] << 8) + i2c_response[6]; - if (crc16_check != calc_crc16_(i2c_response, 7)) { + if (crc16_check != calc_crc16_(i2c_response, 6)) { this->error_code_ = CRC_CHECK_FAILED; this->status_set_warning(); return 0; @@ -109,12 +107,9 @@ float EE895Component::read_float_() { } uint16_t EE895Component::calc_crc16_(const uint8_t buf[], uint8_t len) { - uint8_t crc_check_buf[22]; - for (int i = 0; i < len; i++) { - crc_check_buf[i + 1] = buf[i]; - } - crc_check_buf[0] = this->address_; - return crc16(crc_check_buf, len); + uint8_t addr = this->address_; + uint16_t crc = crc16(&addr, 1); + return crc16(buf, len, crc); } } // namespace ee895 } // namespace esphome diff --git a/esphome/components/ee895/ee895.h b/esphome/components/ee895/ee895.h index 83bd7c6e82..ff1085e05d 100644 --- a/esphome/components/ee895/ee895.h +++ b/esphome/components/ee895/ee895.h @@ -14,7 +14,6 @@ class EE895Component : public PollingComponent, public i2c::I2CDevice { void set_temperature_sensor(sensor::Sensor *temperature_sensor) { temperature_sensor_ = temperature_sensor; } void set_pressure_sensor(sensor::Sensor *pressure_sensor) { pressure_sensor_ = pressure_sensor; } - float get_setup_priority() const override; void setup() override; void dump_config() override; void update() override; @@ -23,9 +22,9 @@ class EE895Component : public PollingComponent, public i2c::I2CDevice { void write_command_(uint16_t addr, uint16_t reg_cnt); float read_float_(); uint16_t calc_crc16_(const uint8_t buf[], uint8_t len); - sensor::Sensor *co2_sensor_; - sensor::Sensor *temperature_sensor_; - sensor::Sensor *pressure_sensor_; + sensor::Sensor *co2_sensor_{nullptr}; + sensor::Sensor *temperature_sensor_{nullptr}; + sensor::Sensor *pressure_sensor_{nullptr}; enum ErrorCode { NONE = 0, COMMUNICATION_FAILED, CRC_CHECK_FAILED } error_code_{NONE}; }; diff --git a/esphome/components/emc2101/emc2101.cpp b/esphome/components/emc2101/emc2101.cpp index 7d85cd31cf..068e25568f 100644 --- a/esphome/components/emc2101/emc2101.cpp +++ b/esphome/components/emc2101/emc2101.cpp @@ -72,7 +72,7 @@ void Emc2101Component::setup() { config |= EMC2101_DAC_BIT; } if (this->inverted_) { - config |= EMC2101_POLARITY_BIT; + reg(EMC2101_REGISTER_FAN_CONFIG) |= EMC2101_POLARITY_BIT; } if (this->dac_mode_) { // DAC mode configurations diff --git a/esphome/components/emc2101/sensor/emc2101_sensor.cpp b/esphome/components/emc2101/sensor/emc2101_sensor.cpp index 2a199f48e9..3014c7da07 100644 --- a/esphome/components/emc2101/sensor/emc2101_sensor.cpp +++ b/esphome/components/emc2101/sensor/emc2101_sensor.cpp @@ -7,8 +7,6 @@ namespace emc2101 { static const char *const TAG = "EMC2101.sensor"; -float EMC2101Sensor::get_setup_priority() const { return setup_priority::DATA; } - void EMC2101Sensor::dump_config() { ESP_LOGCONFIG(TAG, "Emc2101 sensor:"); LOG_SENSOR(" ", "Internal temperature", this->internal_temperature_sensor_); diff --git a/esphome/components/emc2101/sensor/emc2101_sensor.h b/esphome/components/emc2101/sensor/emc2101_sensor.h index 3e8dcebc8e..3e033f58a7 100644 --- a/esphome/components/emc2101/sensor/emc2101_sensor.h +++ b/esphome/components/emc2101/sensor/emc2101_sensor.h @@ -15,8 +15,6 @@ class EMC2101Sensor : public PollingComponent { void dump_config() override; /** Used by ESPHome framework. */ void update() override; - /** Used by ESPHome framework. */ - float get_setup_priority() const override; /** Used by ESPHome framework. */ void set_internal_temperature_sensor(sensor::Sensor *sensor) { this->internal_temperature_sensor_ = sensor; } diff --git a/esphome/components/emmeti/emmeti.cpp b/esphome/components/emmeti/emmeti.cpp index 5286f962b8..04976d95d7 100644 --- a/esphome/components/emmeti/emmeti.cpp +++ b/esphome/components/emmeti/emmeti.cpp @@ -28,7 +28,7 @@ uint8_t EmmetiClimate::set_mode_() { } uint8_t EmmetiClimate::set_fan_speed_() { - switch (this->fan_mode.value()) { + switch (this->fan_mode.value_or(climate::CLIMATE_FAN_ON)) { case climate::CLIMATE_FAN_LOW: return EMMETI_FAN_1; case climate::CLIMATE_FAN_MEDIUM: @@ -153,10 +153,7 @@ void EmmetiClimate::reverse_add_(T val, size_t len, esphome::remote_base::Remote bool EmmetiClimate::check_checksum_(uint8_t checksum) { uint8_t expected = this->gen_checksum_(); - ESP_LOGV(TAG, - "Expected checksum: %X\n" - "Checksum received: %X", - expected, checksum); + ESP_LOGV(TAG, "Expected checksum: %X, Checksum received: %X", expected, checksum); return checksum == expected; } @@ -266,10 +263,7 @@ bool EmmetiClimate::on_receive(remote_base::RemoteReceiveData data) { } } - ESP_LOGD(TAG, - "Swing: %d\n" - "Sleep: %d", - (curr_state.bitmap >> 1) & 0x01, (curr_state.bitmap >> 2) & 0x01); + ESP_LOGD(TAG, "Swing: %d, Sleep: %d", (curr_state.bitmap >> 1) & 0x01, (curr_state.bitmap >> 2) & 0x01); for (size_t pos = 0; pos < 4; pos++) { if (data.expect_item(EMMETI_BIT_MARK, EMMETI_ONE_SPACE)) { @@ -295,13 +289,8 @@ bool EmmetiClimate::on_receive(remote_base::RemoteReceiveData data) { } } - ESP_LOGD(TAG, - "Turbo: %d\n" - "Light: %d\n" - "Tree: %d\n" - "Blow: %d", - (curr_state.bitmap >> 3) & 0x01, (curr_state.bitmap >> 4) & 0x01, (curr_state.bitmap >> 5) & 0x01, - (curr_state.bitmap >> 6) & 0x01); + ESP_LOGD(TAG, "Turbo: %d, Light: %d, Tree: %d, Blow: %d", (curr_state.bitmap >> 3) & 0x01, + (curr_state.bitmap >> 4) & 0x01, (curr_state.bitmap >> 5) & 0x01, (curr_state.bitmap >> 6) & 0x01); uint16_t control_data = 0; for (size_t pos = 0; pos < 11; pos++) { diff --git a/esphome/components/endstop/endstop_cover.cpp b/esphome/components/endstop/endstop_cover.cpp index 2c281ea2e6..5e0b9c72d3 100644 --- a/esphome/components/endstop/endstop_cover.cpp +++ b/esphome/components/endstop/endstop_cover.cpp @@ -37,8 +37,9 @@ void EndstopCover::control(const CoverCall &call) { } } } - if (call.get_position().has_value()) { - auto pos = *call.get_position(); + auto opt_pos = call.get_position(); + if (opt_pos.has_value()) { + auto pos = *opt_pos; if (pos == this->position) { // already at target } else { @@ -111,7 +112,7 @@ void EndstopCover::dump_config() { LOG_BINARY_SENSOR(" ", "Open Endstop", this->open_endstop_); LOG_BINARY_SENSOR(" ", "Close Endstop", this->close_endstop_); } -float EndstopCover::get_setup_priority() const { return setup_priority::DATA; } + void EndstopCover::stop_prev_trigger_() { if (this->prev_command_trigger_ != nullptr) { this->prev_command_trigger_->stop_action(); @@ -141,15 +142,15 @@ void EndstopCover::start_direction_(CoverOperation dir) { Trigger<> *trig; switch (dir) { case COVER_OPERATION_IDLE: - trig = this->stop_trigger_; + trig = &this->stop_trigger_; break; case COVER_OPERATION_OPENING: this->last_operation_ = dir; - trig = this->open_trigger_; + trig = &this->open_trigger_; break; case COVER_OPERATION_CLOSING: this->last_operation_ = dir; - trig = this->close_trigger_; + trig = &this->close_trigger_; break; default: return; diff --git a/esphome/components/endstop/endstop_cover.h b/esphome/components/endstop/endstop_cover.h index 6ae15de8c1..32ede12335 100644 --- a/esphome/components/endstop/endstop_cover.h +++ b/esphome/components/endstop/endstop_cover.h @@ -13,11 +13,10 @@ class EndstopCover : public cover::Cover, public Component { void setup() override; void loop() override; void dump_config() override; - float get_setup_priority() const override; - Trigger<> *get_open_trigger() const { return this->open_trigger_; } - Trigger<> *get_close_trigger() const { return this->close_trigger_; } - Trigger<> *get_stop_trigger() const { return this->stop_trigger_; } + Trigger<> *get_open_trigger() { return &this->open_trigger_; } + Trigger<> *get_close_trigger() { return &this->close_trigger_; } + Trigger<> *get_stop_trigger() { return &this->stop_trigger_; } void set_open_endstop(binary_sensor::BinarySensor *open_endstop) { this->open_endstop_ = open_endstop; } void set_close_endstop(binary_sensor::BinarySensor *close_endstop) { this->close_endstop_ = close_endstop; } void set_open_duration(uint32_t open_duration) { this->open_duration_ = open_duration; } @@ -39,11 +38,11 @@ class EndstopCover : public cover::Cover, public Component { binary_sensor::BinarySensor *open_endstop_; binary_sensor::BinarySensor *close_endstop_; - Trigger<> *open_trigger_{new Trigger<>()}; + Trigger<> open_trigger_; uint32_t open_duration_; - Trigger<> *close_trigger_{new Trigger<>()}; + Trigger<> close_trigger_; uint32_t close_duration_; - Trigger<> *stop_trigger_{new Trigger<>()}; + Trigger<> stop_trigger_; uint32_t max_duration_{UINT32_MAX}; Trigger<> *prev_command_trigger_{nullptr}; diff --git a/esphome/components/ens160_base/ens160_base.cpp b/esphome/components/ens160_base/ens160_base.cpp index 785b053f04..e1cee5005c 100644 --- a/esphome/components/ens160_base/ens160_base.cpp +++ b/esphome/components/ens160_base/ens160_base.cpp @@ -152,12 +152,13 @@ void ENS160Component::update() { // verbose status logging ESP_LOGV(TAG, - "Status: ENS160 STATAS bit 0x%x\n" - "Status: ENS160 STATER bit 0x%x\n" - "Status: ENS160 VALIDITY FLAG 0x%02x\n" - "Status: ENS160 NEWDAT bit 0x%x\n" - "Status: ENS160 NEWGPR bit 0x%x", - (ENS160_DATA_STATUS_STATAS & (status_value)) == ENS160_DATA_STATUS_STATAS, + "ENS160 Status Register: 0x%02x\n" + " STATAS bit 0x%x\n" + " STATER bit 0x%x\n" + " VALIDITY FLAG 0x%02x\n" + " NEWDAT bit 0x%x\n" + " NEWGPR bit 0x%x", + status_value, (ENS160_DATA_STATUS_STATAS & (status_value)) == ENS160_DATA_STATUS_STATAS, (ENS160_DATA_STATUS_STATER & (status_value)) == ENS160_DATA_STATUS_STATER, (ENS160_DATA_STATUS_VALIDITY & status_value) >> 2, (ENS160_DATA_STATUS_NEWDAT & (status_value)) == ENS160_DATA_STATUS_NEWDAT, diff --git a/esphome/components/ens210/ens210.cpp b/esphome/components/ens210/ens210.cpp index 98a300f5d7..8bee9bfb18 100644 --- a/esphome/components/ens210/ens210.cpp +++ b/esphome/components/ens210/ens210.cpp @@ -136,8 +136,6 @@ void ENS210Component::dump_config() { LOG_SENSOR(" ", "Humidity", this->humidity_sensor_); } -float ENS210Component::get_setup_priority() const { return setup_priority::DATA; } - void ENS210Component::update() { // Execute a single measurement if (!this->write_byte(ENS210_REGISTER_SENS_RUN, 0x00)) { diff --git a/esphome/components/ens210/ens210.h b/esphome/components/ens210/ens210.h index 0fb6ff634d..ae2bf81b5f 100644 --- a/esphome/components/ens210/ens210.h +++ b/esphome/components/ens210/ens210.h @@ -10,7 +10,6 @@ namespace ens210 { /// This class implements support for the ENS210 relative humidity and temperature i2c sensor. class ENS210Component : public PollingComponent, public i2c::I2CDevice { public: - float get_setup_priority() const override; void dump_config() override; void setup() override; void update() override; diff --git a/esphome/components/epaper_spi/colorconv.h b/esphome/components/epaper_spi/colorconv.h new file mode 100644 index 0000000000..a2ea28f4b6 --- /dev/null +++ b/esphome/components/epaper_spi/colorconv.h @@ -0,0 +1,67 @@ +#pragma once + +#include +#include +#include "esphome/core/color.h" + +/* Utility for converting internal \a Color RGB representation to supported IC hardware color keys + * + * Focus in driver layer is on efficiency. + * For optimum output quality on RGB inputs consider offline color keying/dithering. + * Also see e.g. Image component. + */ + +namespace esphome::epaper_spi { + +/** Delta for when to regard as gray */ +static constexpr uint8_t COLORCONV_GRAY_THRESHOLD = 50; + +/** Map RGB color to discrete BWYR hex 4 color key + * + * @tparam NATIVE_COLOR Type of native hardware color values + * @param color RGB color to convert from + * @param hw_black Native value for black + * @param hw_white Native value for white + * @param hw_yellow Native value for yellow + * @param hw_red Native value for red + * @return Converted native hardware color value + * @internal Constexpr. Does not depend on side effects ("pure"). + */ +template +constexpr NATIVE_COLOR color_to_bwyr(Color color, NATIVE_COLOR hw_black, NATIVE_COLOR hw_white, NATIVE_COLOR hw_yellow, + NATIVE_COLOR hw_red) { + // --- Step 1: Check for Grayscale (Black or White) --- + // We define "grayscale" as a color where the min and max components + // are close to each other. + + const auto [min_rgb, max_rgb] = std::minmax({color.r, color.g, color.b}); + + if ((max_rgb - min_rgb) < COLORCONV_GRAY_THRESHOLD) { + // It's a shade of gray. Map to BLACK or WHITE. + // We split the luminance at the halfway point (382 = (255*3)/2) + if ((static_cast(color.r) + color.g + color.b) > 382) { + return hw_white; + } + return hw_black; + } + + // --- Step 2: Check for Primary/Secondary Colors --- + // If it's not gray, it's a color. We check which components are + // "on" (over 128) vs "off". This divides the RGB cube into 8 corners. + const bool r_on = (color.r > 128); + const bool g_on = (color.g > 128); + const bool b_on = (color.b > 128); + + if (r_on) { + if (!b_on) { + return g_on ? hw_yellow : hw_red; + } + + // At least red+blue high (but not gray) -> White + return hw_white; + } else { + return (b_on && g_on) ? hw_white : hw_black; + } +} + +} // namespace esphome::epaper_spi diff --git a/esphome/components/epaper_spi/display.py b/esphome/components/epaper_spi/display.py index 8cc7b2663c..658f9e2c4a 100644 --- a/esphome/components/epaper_spi/display.py +++ b/esphome/components/epaper_spi/display.py @@ -49,10 +49,6 @@ EPaperBase = epaper_spi_ns.class_( ) Transform = epaper_spi_ns.enum("Transform") -EPaperSpectraE6 = epaper_spi_ns.class_("EPaperSpectraE6", EPaperBase) -EPaper7p3InSpectraE6 = epaper_spi_ns.class_("EPaper7p3InSpectraE6", EPaperSpectraE6) - - # Import all models dynamically from the models package for module_info in pkgutil.iter_modules(models.__path__): importlib.import_module(f".models.{module_info.name}", package=__package__) @@ -76,50 +72,42 @@ def model_schema(config): model.get_default(CONF_MINIMUM_UPDATE_INTERVAL, "1s") ) cv_dimensions = cv.Optional if model.get_default(CONF_WIDTH) else cv.Required - return ( - display.FULL_DISPLAY_SCHEMA.extend( - spi.spi_device_schema( - cs_pin_required=False, - default_mode="MODE0", - default_data_rate=model.get_default(CONF_DATA_RATE, 10_000_000), - ) - ) - .extend( - { - model.option(pin): pins.gpio_output_pin_schema - for pin in (CONF_RESET_PIN, CONF_CS_PIN, CONF_BUSY_PIN) - } - ) - .extend( - { - cv.Optional(CONF_ROTATION, default=0): validate_rotation, - cv.Required(CONF_MODEL): cv.one_of(model.name, upper=True), - cv.Optional(CONF_UPDATE_INTERVAL, default=cv.UNDEFINED): cv.All( - update_interval, cv.Range(min=minimum_update_interval) - ), - cv.Optional(CONF_TRANSFORM): cv.Schema( - { - cv.Required(CONF_MIRROR_X): cv.boolean, - cv.Required(CONF_MIRROR_Y): cv.boolean, - } - ), - cv.Optional(CONF_FULL_UPDATE_EVERY, default=1): cv.int_range(1, 255), - model.option(CONF_DC_PIN, fallback=None): pins.gpio_output_pin_schema, - cv.GenerateID(): cv.declare_id(class_name), - cv.GenerateID(CONF_INIT_SEQUENCE_ID): cv.declare_id(cg.uint8), - cv_dimensions(CONF_DIMENSIONS): DIMENSION_SCHEMA, - model.option(CONF_ENABLE_PIN): cv.ensure_list( - pins.gpio_output_pin_schema - ), - model.option(CONF_INIT_SEQUENCE, cv.UNDEFINED): cv.ensure_list( - map_sequence - ), - model.option(CONF_RESET_DURATION, cv.UNDEFINED): cv.All( - cv.positive_time_period_milliseconds, - cv.Range(max=core.TimePeriod(milliseconds=500)), - ), - } + return display.FULL_DISPLAY_SCHEMA.extend( + spi.spi_device_schema( + cs_pin_required=False, + default_mode="MODE0", + default_data_rate=model.get_default(CONF_DATA_RATE, 10_000_000), ) + ).extend( + { + cv.Optional(CONF_ROTATION, default=0): validate_rotation, + cv.Required(CONF_MODEL): cv.one_of(model.name, upper=True), + cv.Optional(CONF_UPDATE_INTERVAL, default=cv.UNDEFINED): cv.All( + update_interval, cv.Range(min=minimum_update_interval) + ), + cv.Optional(CONF_TRANSFORM): cv.Schema( + { + cv.Required(CONF_MIRROR_X): cv.boolean, + cv.Required(CONF_MIRROR_Y): cv.boolean, + } + ), + cv.Optional(CONF_FULL_UPDATE_EVERY, default=1): cv.int_range(1, 255), + model.option(CONF_BUSY_PIN): pins.gpio_input_pin_schema, + model.option(CONF_CS_PIN): pins.gpio_output_pin_schema, + model.option(CONF_DC_PIN, fallback=None): pins.gpio_output_pin_schema, + model.option(CONF_RESET_PIN): pins.gpio_output_pin_schema, + cv.GenerateID(): cv.declare_id(class_name), + cv.GenerateID(CONF_INIT_SEQUENCE_ID): cv.declare_id(cg.uint8), + cv_dimensions(CONF_DIMENSIONS): DIMENSION_SCHEMA, + model.option(CONF_ENABLE_PIN): cv.ensure_list(pins.gpio_output_pin_schema), + model.option(CONF_INIT_SEQUENCE, cv.UNDEFINED): cv.ensure_list( + map_sequence + ), + model.option(CONF_RESET_DURATION, cv.UNDEFINED): cv.All( + cv.positive_time_period_milliseconds, + cv.Range(max=core.TimePeriod(milliseconds=500)), + ), + } ) @@ -187,9 +175,7 @@ async def to_code(config): *model.get_constructor_args(config), ) - # Rotation is handled by setting the transform - display_config = {k: v for k, v in config.items() if k != CONF_ROTATION} - await display.register_display(var, display_config) + await display.register_display(var, config) await spi.register_spi_device(var, config, write_only=True) dc = await cg.gpio_pin_expression(config[CONF_DC_PIN]) @@ -213,16 +199,6 @@ async def to_code(config): transform[CONF_SWAP_XY] = False else: transform = {x: model.get_default(x, False) for x in TRANSFORM_OPTIONS} - rotation = config[CONF_ROTATION] - if rotation == 180: - transform[CONF_MIRROR_X] = not transform[CONF_MIRROR_X] - transform[CONF_MIRROR_Y] = not transform[CONF_MIRROR_Y] - elif rotation == 90: - transform[CONF_SWAP_XY] = not transform[CONF_SWAP_XY] - transform[CONF_MIRROR_X] = not transform[CONF_MIRROR_X] - elif rotation == 270: - transform[CONF_SWAP_XY] = not transform[CONF_SWAP_XY] - transform[CONF_MIRROR_Y] = not transform[CONF_MIRROR_Y] transform_str = "|".join( { str(getattr(Transform, x.upper())) diff --git a/esphome/components/epaper_spi/epaper_spi.cpp b/esphome/components/epaper_spi/epaper_spi.cpp index db803305a5..a2ca311b30 100644 --- a/esphome/components/epaper_spi/epaper_spi.cpp +++ b/esphome/components/epaper_spi/epaper_spi.cpp @@ -97,6 +97,23 @@ bool EPaperBase::reset() { return true; } +void EPaperBase::update_effective_transform_() { + switch (this->rotation_) { + case DISPLAY_ROTATION_90_DEGREES: + this->effective_transform_ = this->transform_ ^ (SWAP_XY | MIRROR_X); + break; + case DISPLAY_ROTATION_180_DEGREES: + this->effective_transform_ = this->transform_ ^ (MIRROR_Y | MIRROR_X); + break; + case DISPLAY_ROTATION_270_DEGREES: + this->effective_transform_ = this->transform_ ^ (SWAP_XY | MIRROR_Y); + break; + default: + this->effective_transform_ = this->transform_; + break; + } +} + void EPaperBase::update() { if (this->state_ != EPaperState::IDLE) { ESP_LOGE(TAG, "Display already in state %s", epaper_state_to_string_()); @@ -182,7 +199,9 @@ void EPaperBase::process_state_() { this->set_state_(EPaperState::RESET); break; case EPaperState::INITIALISE: - this->initialise(this->update_count_ != 0); + if (!this->initialise(this->update_count_ != 0)) { + return; // Not done yet, come back next loop + } this->set_state_(EPaperState::TRANSFER_DATA); break; case EPaperState::TRANSFER_DATA: @@ -239,11 +258,9 @@ void EPaperBase::start_data_() { void EPaperBase::on_safe_shutdown() { this->deep_sleep(); } -void EPaperBase::initialise(bool partial) { +void EPaperBase::send_init_sequence_(const uint8_t *sequence, size_t length) { size_t index = 0; - auto *sequence = this->init_sequence_; - auto length = this->init_sequence_length_; while (index != length) { if (length - index < 2) { this->mark_failed(LOG_STR("Malformed init sequence")); @@ -266,6 +283,11 @@ void EPaperBase::initialise(bool partial) { } } +bool EPaperBase::initialise(bool partial) { + this->send_init_sequence_(this->init_sequence_, this->init_sequence_length_); + return true; +} + /** * Check and rotate coordinates based on the transform flags. * @param x @@ -275,11 +297,11 @@ void EPaperBase::initialise(bool partial) { bool EPaperBase::rotate_coordinates_(int &x, int &y) { if (!this->get_clipping().inside(x, y)) return false; - if (this->transform_ & SWAP_XY) + if (this->effective_transform_ & SWAP_XY) std::swap(x, y); - if (this->transform_ & MIRROR_X) + if (this->effective_transform_ & MIRROR_X) x = this->width_ - x - 1; - if (this->transform_ & MIRROR_Y) + if (this->effective_transform_ & MIRROR_Y) y = this->height_ - y - 1; if (x >= this->width_ || y >= this->height_ || x < 0 || y < 0) return false; diff --git a/esphome/components/epaper_spi/epaper_spi.h b/esphome/components/epaper_spi/epaper_spi.h index 521543f026..47b4f9f72d 100644 --- a/esphome/components/epaper_spi/epaper_spi.h +++ b/esphome/components/epaper_spi/epaper_spi.h @@ -1,6 +1,6 @@ #pragma once -#include "esphome/components/display/display_buffer.h" +#include "esphome/components/display/display.h" #include "esphome/components/spi/spi.h" #include "esphome/components/split_buffer/split_buffer.h" #include "esphome/core/component.h" @@ -51,7 +51,14 @@ class EPaperBase : public Display, void set_reset_pin(GPIOPin *reset) { this->reset_pin_ = reset; } void set_busy_pin(GPIOPin *busy) { this->busy_pin_ = busy; } void set_reset_duration(uint32_t reset_duration) { this->reset_duration_ = reset_duration; } - void set_transform(uint8_t transform) { this->transform_ = transform; } + void set_transform(uint8_t transform) { + this->transform_ = transform; + this->update_effective_transform_(); + } + void set_rotation(DisplayRotation rotation) override { + Display::set_rotation(rotation); + this->update_effective_transform_(); + } void set_full_update_every(uint8_t full_update_every) { this->full_update_every_ = full_update_every; } void dump_config() override; @@ -76,7 +83,7 @@ class EPaperBase : public Display, static uint8_t color_to_bit(Color color) { // It's always a shade of gray. Map to BLACK or WHITE. // We split the luminance at a suitable point - if ((static_cast(color.r) + color.g + color.b) > 512) { + if ((color.r + color.g + color.b) >= 382) { return 1; } return 0; @@ -106,8 +113,8 @@ class EPaperBase : public Display, protected: int get_height_internal() override { return this->height_; }; int get_width_internal() override { return this->width_; }; - int get_width() override { return this->transform_ & SWAP_XY ? this->height_ : this->width_; } - int get_height() override { return this->transform_ & SWAP_XY ? this->width_ : this->height_; } + int get_width() override { return this->effective_transform_ & SWAP_XY ? this->height_ : this->width_; } + int get_height() override { return this->effective_transform_ & SWAP_XY ? this->width_ : this->height_; } void draw_pixel_at(int x, int y, Color color) override; void process_state_(); @@ -115,9 +122,11 @@ class EPaperBase : public Display, bool is_idle_() const; void setup_pins_() const; virtual bool reset(); - virtual void initialise(bool partial); + virtual bool initialise(bool partial); + void send_init_sequence_(const uint8_t *sequence, size_t length); void wait_for_idle_(bool should_wait); bool init_buffer_(size_t buffer_length); + void update_effective_transform_(); bool rotate_coordinates_(int &x, int &y); /** @@ -170,6 +179,7 @@ class EPaperBase : public Display, uint32_t delay_until_{}; // timestamp until which to delay processing uint16_t next_delay_{}; // milliseconds to delay before next state uint8_t transform_{}; + uint8_t effective_transform_{}; uint8_t update_count_{}; // these values represent the bounds of the updated buffer. Note that x_high and y_high // point to the pixel past the last one updated, i.e. may range up to width/height. diff --git a/esphome/components/epaper_spi/epaper_spi_jd79660.cpp b/esphome/components/epaper_spi/epaper_spi_jd79660.cpp new file mode 100644 index 0000000000..1cd1087c6b --- /dev/null +++ b/esphome/components/epaper_spi/epaper_spi_jd79660.cpp @@ -0,0 +1,227 @@ +#include "epaper_spi_jd79660.h" +#include "colorconv.h" + +#include "esphome/core/log.h" + +namespace esphome::epaper_spi { +static constexpr const char *const TAG = "epaper_spi.jd79660"; + +/** Pixel color as 2bpp. Must match IC LUT values. */ +enum JD79660Color : uint8_t { + BLACK = 0b00, + WHITE = 0b01, + YELLOW = 0b10, + RED = 0b11, +}; + +/** Map RGB color to JD79660 BWYR hex color keys */ +static JD79660Color HOT color_to_hex(Color color) { + return color_to_bwyr(color, JD79660Color::BLACK, JD79660Color::WHITE, JD79660Color::YELLOW, JD79660Color::RED); +} + +void EPaperJD79660::fill(Color color) { + // If clipping is active, fall back to base implementation + if (this->get_clipping().is_set()) { + EPaperBase::fill(color); + return; + } + + const auto pixel_color = color_to_hex(color); + + // We store 4 pixels per byte + this->buffer_.fill(pixel_color | (pixel_color << 2) | (pixel_color << 4) | (pixel_color << 6)); +} + +void HOT EPaperJD79660::draw_pixel_at(int x, int y, Color color) { + if (!this->rotate_coordinates_(x, y)) + return; + const auto pixel_bits = color_to_hex(color); + const uint32_t pixel_position = x + y * this->get_width_internal(); + // We store 4 pixels per byte at LSB offsets 6, 4, 2, 0 + const uint32_t byte_position = pixel_position / 4; + const uint32_t bit_offset = 6 - ((pixel_position % 4) * 2); + const auto original = this->buffer_[byte_position]; + + this->buffer_[byte_position] = (original & (~(0b11 << bit_offset))) | // mask old 2bpp + (pixel_bits << bit_offset); // add new 2bpp +} + +bool EPaperJD79660::reset() { + // On entry state RESET set step, next state will be RESET_END + if (this->state_ == EPaperState::RESET) { + this->step_ = FSMState::RESET_STEP0_H; + } + + switch (this->step_) { + case FSMState::RESET_STEP0_H: + // Step #0: Reset H for some settle time. + + ESP_LOGVV(TAG, "reset #0"); + this->reset_pin_->digital_write(true); + + this->reset_duration_ = SLEEP_MS_RESET0; + this->step_ = FSMState::RESET_STEP1_L; + return false; // another loop: step #1 below + + case FSMState::RESET_STEP1_L: + // Step #1: Reset L pulse for slightly >1.5ms. + // This is actual reset trigger. + + ESP_LOGVV(TAG, "reset #1"); + + // As commented on SLEEP_MS_RESET1: Reset pulse must happen within time window. + // So do not use FSM loop, and avoid other calls/logs during pulse below. + this->reset_pin_->digital_write(false); + delay(SLEEP_MS_RESET1); + this->reset_pin_->digital_write(true); + + this->reset_duration_ = SLEEP_MS_RESET2; + this->step_ = FSMState::RESET_STEP2_IDLECHECK; + return false; // another loop: step #2 below + + case FSMState::RESET_STEP2_IDLECHECK: + // Step #2: Basically finished. Check sanity, and move FSM to INITIALISE state + ESP_LOGVV(TAG, "reset #2"); + + if (!this->is_idle_()) { + // Expectation: Idle after reset + settle time. + // Improperly connected/unexpected hardware? + // Error path reproducable e.g. with disconnected VDD/... pins + // (optimally while busy_pin configured with local pulldown). + // -> Mark failed to avoid followup problems. + this->mark_failed(LOG_STR("Busy after reset")); + } + break; // End state loop below + + default: + // Unexpected step = bug? + this->mark_failed(); + } + + this->step_ = FSMState::INIT_STEP0_REGULARINIT; // reset for initialize state + return true; +} + +bool EPaperJD79660::initialise(bool partial) { + switch (this->step_) { + case FSMState::INIT_STEP0_REGULARINIT: + // Step #0: Regular init sequence + ESP_LOGVV(TAG, "init #0"); + if (!EPaperBase::initialise(partial)) { // Call parent impl + return false; // If parent should request another loop, do so + } + + // Fast init requested + supported? + if (partial && (this->fast_update_length_ > 0)) { + this->step_ = FSMState::INIT_STEP1_FASTINIT; + this->wait_for_idle_(true); // Must wait for idle before fastinit sequence in next loop + return false; // another loop: step #1 below + } + + break; // End state loop below + + case FSMState::INIT_STEP1_FASTINIT: + // Step #1: Fast init sequence + ESP_LOGVV(TAG, "init #1"); + this->write_fastinit_(); + break; // End state loop below + + default: + // Unexpected step = bug? + this->mark_failed(); + } + + this->step_ = FSMState::NONE; + return true; // Finished: State transition waits for idle +} + +bool EPaperJD79660::transfer_buffer_chunks_() { + size_t buf_idx = 0; + uint8_t bytes_to_send[MAX_TRANSFER_SIZE]; + const uint32_t start_time = App.get_loop_component_start_time(); + const auto buffer_length = this->buffer_length_; + while (this->current_data_index_ != buffer_length) { + bytes_to_send[buf_idx++] = this->buffer_[this->current_data_index_++]; + + if (buf_idx == sizeof bytes_to_send) { + this->start_data_(); + this->write_array(bytes_to_send, buf_idx); + this->disable(); + ESP_LOGVV(TAG, "Wrote %zu bytes at %ums", buf_idx, (unsigned) millis()); + buf_idx = 0; + + if (millis() - start_time > MAX_TRANSFER_TIME) { + // Let the main loop run and come back next loop + return false; + } + } + } + + // Finished the entire dataset + if (buf_idx != 0) { + this->start_data_(); + this->write_array(bytes_to_send, buf_idx); + this->disable(); + ESP_LOGVV(TAG, "Wrote %zu bytes at %ums", buf_idx, (unsigned) millis()); + } + // Cleanup for next transfer + this->current_data_index_ = 0; + + // Finished with all buffer chunks + return true; +} + +void EPaperJD79660::write_fastinit_() { + // Undocumented register sequence in vendor register range. + // Related to Fast Init/Update. + // Should likely happen after regular init seq and power on, but before refresh. + // Might only work for some models with certain factory MTP. + // Please do not change without knowledge to avoid breakage. + + this->send_init_sequence_(this->fast_update_, this->fast_update_length_); +} + +bool EPaperJD79660::transfer_data() { + // For now always send full frame buffer in chunks. + // JD79660 might support partial window transfers. But sample code missing. + // And likely minimal impact, solely on SPI transfer time into RAM. + + if (this->current_data_index_ == 0) { + this->command(CMD_TRANSFER); + } + + return this->transfer_buffer_chunks_(); +} + +void EPaperJD79660::refresh_screen([[maybe_unused]] bool partial) { + ESP_LOGV(TAG, "Refresh"); + this->cmd_data(CMD_REFRESH, {(uint8_t) 0x00}); +} + +void EPaperJD79660::power_off() { + ESP_LOGV(TAG, "Power off"); + this->cmd_data(CMD_POWEROFF, {(uint8_t) 0x00}); +} + +void EPaperJD79660::deep_sleep() { + ESP_LOGV(TAG, "Deep sleep"); + // "Deepsleep between update": Ensure EPD sleep to avoid early hardware wearout! + this->cmd_data(CMD_DEEPSLEEP, {(uint8_t) 0xA5}); + + // Notes: + // - VDD: Some boards (Waveshare) with "clever reset logic" would allow switching off + // EPD VDD by pulling reset pin low for longer time. + // However, a) not all boards have this, b) reliable sequence timing is difficult, + // c) saving is not worth it after deepsleep command above. + // If needed: Better option is to drive VDD via MOSFET with separate enable pin. + // + // - Possible safe shutdown: + // EPaperBase::on_safe_shutdown() may also trigger deep_sleep() again. + // Regularly, in IDLE state, this does not make sense for this "deepsleep between update" model, + // but SPI sequence should simply be ignored by sleeping receiver. + // But if triggering during lengthy update, this quick SPI sleep sequence may have benefit. + // Optimally, EPDs should even be set all white for longer storage. + // But full sequence (>15s) not possible w/o app logic. +} + +} // namespace esphome::epaper_spi diff --git a/esphome/components/epaper_spi/epaper_spi_jd79660.h b/esphome/components/epaper_spi/epaper_spi_jd79660.h new file mode 100644 index 0000000000..4e488fe93e --- /dev/null +++ b/esphome/components/epaper_spi/epaper_spi_jd79660.h @@ -0,0 +1,145 @@ +#pragma once + +#include "epaper_spi.h" + +namespace esphome::epaper_spi { + +/** + * JD7966x IC driver implementation + * + * Currently tested with: + * - JD79660 (max res: 200x200) + * + * May also work for other JD7966x chipset family members with minimal adaptations. + * + * Capabilities: + * - HW frame buffer layout: + * 4 colors (gray0..3, commonly BWYR). Bytes consist of 4px/2bpp. + * Width must be rounded to multiple of 4. + * - Fast init/update (shorter wave forms): Yes. Controlled by CONF_FULL_UPDATE_EVERY. + * Needs undocumented fastinit sequence, based on likely vendor specific MTP content. + * - Partial transfer (transfer only changed window): No. Maybe possible by HW. + * - Partial refresh (refresh only changed window): No. Likely HW limit. + * + * @internal \c final saves few bytes by devirtualization. Remove \c final when subclassing. + */ +class EPaperJD79660 final : public EPaperBase { + public: + EPaperJD79660(const char *name, uint16_t width, uint16_t height, const uint8_t *init_sequence, + size_t init_sequence_length, const uint8_t *fast_update, uint16_t fast_update_length) + : EPaperBase(name, width, height, init_sequence, init_sequence_length, DISPLAY_TYPE_COLOR), + fast_update_(fast_update), + fast_update_length_(fast_update_length) { + this->row_width_ = (width + 3) / 4; // Fix base class calc (2bpp instead of 1bpp) + this->buffer_length_ = this->row_width_ * height; + } + + void fill(Color color) override; + + protected: + /** Draw colored pixel into frame buffer */ + void draw_pixel_at(int x, int y, Color color) override; + + /** Reset (multistep sequence) + * @pre this->reset_pin_ != nullptr // cv.Required check + * @post Should be idle on successful reset. Can mark failures. + */ + bool reset() override; + + /** Initialise (multistep sequence) */ + bool initialise(bool partial) override; + + /** Buffer transfer */ + bool transfer_data() override; + + /** Power on: Already part of init sequence (likely needed there before transferring buffers). + * So nothing to do in FSM state. + */ + void power_on() override {} + + /** Refresh screen + * @param partial Ignored: Needed earlier in \a ::initialize + * @pre Must be idle. + * @post Should return to idle later after processing. + */ + void refresh_screen([[maybe_unused]] bool partial) override; + + /** Power off + * @pre Must be idle. + * @post Should return to idle later after processing. + * (latter will take long period like ~15-20s on actual refresh!) + */ + void power_off() override; + + /** Deepsleep: Must be used to avoid hardware wearout! + * @pre Must be idle. + * @post Will go busy, and not return idle till ::reset! + */ + void deep_sleep() override; + + /** Internal: Send fast init sequence via undocumented vendor registers + * @pre Must be directly after regular ::initialise sequence, before ::transfer_data + * @pre Must be idle. + * @post Should return to idle later after processing. + */ + void write_fastinit_(); + + /** Internal: Send raw buffer in chunks + * \retval true Finished + * \retval false Loop time elapsed. Need to call again next loop. + */ + bool transfer_buffer_chunks_(); + + /** @name IC commands @{ */ + static constexpr uint8_t CMD_POWEROFF = 0x02; + static constexpr uint8_t CMD_DEEPSLEEP = 0x07; + static constexpr uint8_t CMD_TRANSFER = 0x10; + static constexpr uint8_t CMD_REFRESH = 0x12; + /** @} */ + + /** State machine constants for \a step_ */ + enum class FSMState : uint8_t { + NONE = 0, //!< Initial/default value: Unused + + /* Reset state steps */ + RESET_STEP0_H, + RESET_STEP1_L, + RESET_STEP2_IDLECHECK, + + /* Init state steps */ + INIT_STEP0_REGULARINIT, + INIT_STEP1_FASTINIT, + }; + + /** Wait time (millisec) for first reset phase: High + * + * Wait via FSM loop. + */ + static constexpr uint16_t SLEEP_MS_RESET0 = 200; + + /** Wait time (millisec) for second reset phase: Low + * + * Holding Reset Low too long may trigger "clever reset" logic + * of e.g. Waveshare Rev2 boards: VDD is shut down via MOSFET, and IC + * will not report idle anymore! + * FSM loop may spuriously increase delay, e.g. >16ms. + * Therefore, sync wait below, as allowed (code rule "delays > 10ms not permitted"), + * yet only slightly exceeding known IC min req of >1.5ms. + */ + static constexpr uint16_t SLEEP_MS_RESET1 = 2; + + /** Wait time (millisec) for third reset phase: High + * + * Wait via FSM loop. + */ + static constexpr uint16_t SLEEP_MS_RESET2 = 200; + + // properties initialised in the constructor + const uint8_t *const fast_update_{}; + const uint16_t fast_update_length_{}; + + /** Counter for tracking substeps within FSM state */ + FSMState step_{FSMState::NONE}; +}; + +} // namespace esphome::epaper_spi diff --git a/esphome/components/epaper_spi/epaper_spi_spectra_e6.h b/esphome/components/epaper_spi/epaper_spi_spectra_e6.h index b8dbf0b0c5..9c251068af 100644 --- a/esphome/components/epaper_spi/epaper_spi_spectra_e6.h +++ b/esphome/components/epaper_spi/epaper_spi_spectra_e6.h @@ -4,7 +4,7 @@ namespace esphome::epaper_spi { -class EPaperSpectraE6 : public EPaperBase { +class EPaperSpectraE6 final : public EPaperBase { public: EPaperSpectraE6(const char *name, uint16_t width, uint16_t height, const uint8_t *init_sequence, size_t init_sequence_length) diff --git a/esphome/components/epaper_spi/epaper_waveshare.cpp b/esphome/components/epaper_spi/epaper_waveshare.cpp index 8d382d86e7..7a7b4e22d3 100644 --- a/esphome/components/epaper_spi/epaper_waveshare.cpp +++ b/esphome/components/epaper_spi/epaper_waveshare.cpp @@ -4,7 +4,7 @@ namespace esphome::epaper_spi { static const char *const TAG = "epaper_spi.waveshare"; -void EpaperWaveshare::initialise(bool partial) { +bool EpaperWaveshare::initialise(bool partial) { EPaperBase::initialise(partial); if (partial) { this->cmd_data(0x32, this->partial_lut_, this->partial_lut_length_); @@ -17,6 +17,7 @@ void EpaperWaveshare::initialise(bool partial) { this->cmd_data(0x3C, {0x05}); } this->send_red_ = true; + return true; } void EpaperWaveshare::set_window() { diff --git a/esphome/components/epaper_spi/epaper_waveshare.h b/esphome/components/epaper_spi/epaper_waveshare.h index 6b894cfd09..d3ad313d92 100644 --- a/esphome/components/epaper_spi/epaper_waveshare.h +++ b/esphome/components/epaper_spi/epaper_waveshare.h @@ -6,7 +6,7 @@ namespace esphome::epaper_spi { /** * An epaper display that needs LUTs to be sent to it. */ -class EpaperWaveshare : public EPaperMono { +class EpaperWaveshare final : public EPaperMono { public: EpaperWaveshare(const char *name, uint16_t width, uint16_t height, const uint8_t *init_sequence, size_t init_sequence_length, const uint8_t *lut, size_t lut_length, const uint8_t *partial_lut, @@ -18,7 +18,7 @@ class EpaperWaveshare : public EPaperMono { partial_lut_length_(partial_lut_length) {} protected: - void initialise(bool partial) override; + bool initialise(bool partial) override; void set_window() override; void refresh_screen(bool partial) override; void deep_sleep() override; diff --git a/esphome/components/epaper_spi/epaper_weact_3c.cpp b/esphome/components/epaper_spi/epaper_weact_3c.cpp new file mode 100644 index 0000000000..d4dac7076c --- /dev/null +++ b/esphome/components/epaper_spi/epaper_weact_3c.cpp @@ -0,0 +1,244 @@ +#include "epaper_weact_3c.h" +#include "esphome/core/log.h" + +namespace esphome::epaper_spi { + +static constexpr const char *const TAG = "epaper_weact_3c"; + +enum class BwrState : uint8_t { + BWR_BLACK, + BWR_WHITE, + BWR_RED, +}; + +static BwrState color_to_bwr(Color color) { + if (color.r > color.g + color.b && color.r > 127) { + return BwrState::BWR_RED; + } + if (color.r + color.g + color.b >= 382) { + return BwrState::BWR_WHITE; + } + return BwrState::BWR_BLACK; +} +// SSD1680 3-color display notes: +// - Buffer uses 1 bit per pixel, 8 pixels per byte +// - Buffer first half (black_offset): Black/White plane (0=black, 1=white) +// - Buffer second half (red_offset): Red plane (1=red, 0=no red) +// - Total buffer: width * height / 4 bytes = 2 * (width * height / 8) +// - For 128x296: 128*296/4 = 9472 bytes total (4736 per color) + +void EPaperWeAct3C::draw_pixel_at(int x, int y, Color color) { + if (!this->rotate_coordinates_(x, y)) + return; + + // Calculate position in the 1-bit buffer + const uint32_t pos = (x / 8) + (y * this->row_width_); + const uint8_t bit = 0x80 >> (x & 0x07); + const uint32_t red_offset = this->buffer_length_ / 2u; + + // Use luminance threshold for B/W mapping + // Split at halfway point (382 = (255*3)/2) + auto bwr = color_to_bwr(color); + + // Update black/white plane (first half of buffer) + if (bwr == BwrState::BWR_WHITE) { + // White pixel - set bit in black plane + this->buffer_[pos] |= bit; + } else { + // Black pixel - clear bit in black plane + this->buffer_[pos] &= ~bit; + } + + // Update red plane (second half of buffer) + // Red if red component is dominant (r > g+b) + if (bwr == BwrState::BWR_RED) { + // Red pixel - set bit in red plane + this->buffer_[red_offset + pos] |= bit; + } else { + // Not red - clear bit in red plane + this->buffer_[red_offset + pos] &= ~bit; + } +} + +void EPaperWeAct3C::fill(Color color) { + // For 3-color e-paper with 1-bit buffer format: + // - Black buffer: 1=black, 0=white + // - Red buffer: 1=red, 0=no red + // The buffer is stored as two halves: [black plane][red plane] + const size_t half_buffer = this->buffer_length_ / 2u; + + // Use luminance threshold for B/W mapping + auto bits = color_to_bwr(color); + + // Fill both planes + if (bits == BwrState::BWR_BLACK) { + // Black - both planes = 0x00 + this->buffer_.fill(0x00); + } else if (bits == BwrState::BWR_RED) { + // Red - black plane = 0x00, red plane = 0xFF + for (size_t i = 0; i < half_buffer; i++) + this->buffer_[i] = 0x00; + for (size_t i = 0; i < half_buffer; i++) + this->buffer_[half_buffer + i] = 0xFF; + } else { + // White - black plane = 0xFF, red plane = 0x00 + for (size_t i = 0; i < half_buffer; i++) + this->buffer_[i] = 0xFF; + for (size_t i = 0; i < half_buffer; i++) + this->buffer_[half_buffer + i] = 0x00; + } +} + +void EPaperWeAct3C::clear() { + // Clear buffer to white, just like real paper. + this->fill(COLOR_ON); +} + +void EPaperWeAct3C::set_window_() { + // For full screen refresh, we always start from (0,0) + // The y_low_/y_high_ values track the dirty region for optimization, + // but for display refresh we need to write from the beginning + uint16_t x_start = 0; + uint16_t x_end = this->width_ - 1; + uint16_t y_start = 0; + uint16_t y_end = this->height_ - 1; // height = 296 for 2.9" display + + // Set RAM X address boundaries (0x44) + // X coordinates are byte-aligned (divided by 8) + this->cmd_data(0x44, {(uint8_t) (x_start / 8), (uint8_t) (x_end / 8)}); + + // Set RAM Y address boundaries (0x45) + // Format: Y start (LSB, MSB), Y end (LSB, MSB) + this->cmd_data(0x45, {(uint8_t) y_start, (uint8_t) (y_start >> 8), (uint8_t) (y_end & 0xFF), (uint8_t) (y_end >> 8)}); + + // Reset RAM X counter to start (0x4E) - 1 byte + this->cmd_data(0x4E, {(uint8_t) (x_start / 8)}); + + // Reset RAM Y counter to start (0x4F) - 2 bytes (LSB, MSB) + this->cmd_data(0x4F, {(uint8_t) y_start, (uint8_t) (y_start >> 8)}); +} + +bool HOT EPaperWeAct3C::transfer_data() { + const uint32_t start_time = millis(); + const size_t buffer_length = this->buffer_length_; + const size_t half_buffer = buffer_length / 2u; + + ESP_LOGV(TAG, "transfer_data: buffer_length=%u, half_buffer=%u", buffer_length, half_buffer); + + // Use a local buffer for SPI transfers + uint8_t bytes_to_send[MAX_TRANSFER_SIZE]; + + // First, send the RED buffer (0x26 = WRITE_COLOR) + // The red plane is in the second half of our buffer + // NOTE: Must set RAM window first to reset address counters! + if (this->current_data_index_ < half_buffer) { + if (this->current_data_index_ == 0) { + ESP_LOGV(TAG, "transfer_data: sending RED buffer (0x26)"); + this->set_window_(); // Reset RAM X/Y counters to start position + this->command(0x26); + } + + this->start_data_(); + size_t red_offset = half_buffer; + while (this->current_data_index_ < half_buffer) { + size_t bytes_to_copy = std::min(MAX_TRANSFER_SIZE, half_buffer - this->current_data_index_); + + for (size_t i = 0; i < bytes_to_copy; i++) { + bytes_to_send[i] = this->buffer_[red_offset + this->current_data_index_ + i]; + } + + this->write_array(bytes_to_send, bytes_to_copy); + + this->current_data_index_ += bytes_to_copy; + + if (millis() - start_time > MAX_TRANSFER_TIME) { + // Let the main loop run and come back next loop + this->disable(); + return false; + } + } + this->disable(); + } + + // Finished the red buffer, now send the BLACK buffer (0x24 = WRITE_BLACK) + // The black plane is in the first half of our buffer + if (this->current_data_index_ < buffer_length) { + if (this->current_data_index_ == half_buffer) { + ESP_LOGV(TAG, "transfer_data: finished red buffer, sending BLACK buffer (0x24)"); + + // Do NOT reset RAM counters here for WeAct displays (Reference implementation behavior) + // this->set_window(); + this->command(0x24); + // Continue using current_data_index_, but we need to map it to the start of the buffer + } + + this->start_data_(); + while (this->current_data_index_ < buffer_length) { + size_t remaining = buffer_length - this->current_data_index_; + size_t bytes_to_copy = std::min(MAX_TRANSFER_SIZE, remaining); + + // Calculate offset into the BLACK buffer (which is at the start of this->buffer_) + // current_data_index_ goes from half_buffer to buffer_length + size_t buffer_offset = this->current_data_index_ - half_buffer; + + for (size_t i = 0; i < bytes_to_copy; i++) { + bytes_to_send[i] = this->buffer_[buffer_offset + i]; + } + + this->write_array(bytes_to_send, bytes_to_copy); + + this->current_data_index_ += bytes_to_copy; + + if (millis() - start_time > MAX_TRANSFER_TIME) { + // Let the main loop run and come back next loop + this->disable(); + return false; + } + } + this->disable(); + } + + this->current_data_index_ = 0; + ESP_LOGV(TAG, "transfer_data: completed (red=%u, black=%u bytes)", half_buffer, half_buffer); + return true; +} + +void EPaperWeAct3C::refresh_screen(bool partial) { + // SSD1680 refresh sequence: + // Reset RAM X/Y address counters to 0,0 so display reads from start + // 0x4E: RAM X counter - 1 byte (X / 8) + // 0x4F: RAM Y counter - 2 bytes (Y LSB, Y MSB) + this->cmd_data(0x4E, {0x00}); // RAM X counter = 0 (1 byte) + this->cmd_data(0x4F, {0x00, 0x00}); // RAM Y counter = 0 (2 bytes) + + // Send UPDATE_FULL command (0x22) with display update control parameter + // Both WeAct and waveshare reference use 0xF7: {0x22, 0xF7} + // 0xF7 = Display update: Load temperature, Load LUT, Enable RAM content + this->cmd_data(0x22, {0xF7}); // Command 0x22 with parameter 0xF7 + this->command(0x20); // Activate display update + + // COMMAND TERMINATE FRAME READ WRITE (required by SSD1680) + // Removed 0xFF based on working reference implementation + // this->command(0xFF); +} + +void EPaperWeAct3C::power_on() { + // Power on sequence - send command to turn on power + // According to SSD1680 spec: 0x22, 0xF8 powers on the display + this->cmd_data(0x22, {0xF8}); // Power on + this->command(0x20); // Activate +} + +void EPaperWeAct3C::power_off() { + // Power off sequence - send command to turn off power + // According to SSD1680 spec: 0x22, 0x83 powers off the display + this->cmd_data(0x22, {0x83}); // Power off + this->command(0x20); // Activate +} + +void EPaperWeAct3C::deep_sleep() { + // Deep sleep sequence + this->cmd_data(0x10, {0x01}); // Deep sleep mode +} + +} // namespace esphome::epaper_spi diff --git a/esphome/components/epaper_spi/epaper_weact_3c.h b/esphome/components/epaper_spi/epaper_weact_3c.h new file mode 100644 index 0000000000..2df6f1ba09 --- /dev/null +++ b/esphome/components/epaper_spi/epaper_weact_3c.h @@ -0,0 +1,39 @@ +#pragma once + +#include "epaper_spi.h" + +namespace esphome::epaper_spi { + +/** + * WeAct 3-color e-paper displays (SSD1683 controller). + * Supports multiple sizes: 2.9" (128x296), 4.2" (400x300), etc. + * + * Color scheme: Black, White, Red (BWR) + * Buffer layout: 1 bit per pixel, separate planes + * - Buffer first half: Black/White plane (1=black, 0=white) + * - Buffer second half: Red plane (1=red, 0=no red) + * - Total buffer: width * height / 4 bytes (2 * width * height / 8) + */ +class EPaperWeAct3C : public EPaperBase { + public: + EPaperWeAct3C(const char *name, uint16_t width, uint16_t height, const uint8_t *init_sequence, + size_t init_sequence_length) + : EPaperBase(name, width, height, init_sequence, init_sequence_length, DISPLAY_TYPE_BINARY) { + this->buffer_length_ = this->row_width_ * height * 2; + } + + void fill(Color color) override; + void clear() override; + + protected: + void set_window_(); + void refresh_screen(bool partial) override; + void power_on() override; + void power_off() override; + void deep_sleep() override; + void draw_pixel_at(int x, int y, Color color) override; + + bool transfer_data() override; +}; + +} // namespace esphome::epaper_spi diff --git a/esphome/components/epaper_spi/models/jd79660.py b/esphome/components/epaper_spi/models/jd79660.py new file mode 100644 index 0000000000..a0457c5812 --- /dev/null +++ b/esphome/components/epaper_spi/models/jd79660.py @@ -0,0 +1,118 @@ +import esphome.codegen as cg +from esphome.components.mipi import flatten_sequence +import esphome.config_validation as cv +from esphome.const import CONF_BUSY_PIN, CONF_RESET_PIN +from esphome.core import ID + +from ..display import CONF_INIT_SEQUENCE_ID +from . import EpaperModel + + +class JD79660(EpaperModel): + def __init__(self, name, class_name="EPaperJD79660", fast_update=None, **kwargs): + super().__init__(name, class_name, **kwargs) + self.fast_update = fast_update + + def option(self, name, fallback=cv.UNDEFINED) -> cv.Optional | cv.Required: + # Validate required pins, as C++ code will assume existence + if name in (CONF_RESET_PIN, CONF_BUSY_PIN): + return cv.Required(name) + + # Delegate to parent + return super().option(name, fallback) + + def get_constructor_args(self, config) -> tuple: + # Resembles init_sequence handling for fast_update config + if self.fast_update is None: + fast_update = cg.nullptr, 0 + else: + flat_fast_update = flatten_sequence(self.fast_update) + fast_update = ( + cg.static_const_array( + ID( + config[CONF_INIT_SEQUENCE_ID].id + "_fast_update", type=cg.uint8 + ), + flat_fast_update, + ), + len(flat_fast_update), + ) + return (*fast_update,) + + +jd79660 = JD79660( + "jd79660", + # Specified refresh times are ~20s (full) or ~15s (fast) due to BWRY. + # So disallow low update intervals (with safety margin), to avoid e.g. FSM update loops. + # Even less frequent intervals (min/h) highly recommended to optimize lifetime! + minimum_update_interval="30s", + # SPI rate: From spec comparisons, IC should allow SCL write cycles up to 10MHz rate. + # Existing code samples also prefer 10MHz. So justifies as default. + # Decrease value further in user config if needed (e.g. poor cabling). + data_rate="10MHz", + # No need to set optional reset_duration: + # Code requires multistep reset sequence with precise timings + # according to data sheet or samples. +) + +# Waveshare 1.54-G +# +# Device may have specific factory provisioned MTP content to facilitate vendor register features like fast init. +# Vendor specific init derived from vendor sample code +# +# Compatible MIT license, see esphome/LICENSE file. +# +# fmt: off +jd79660.extend( + "Waveshare-1.54in-G", + width=200, + height=200, + + initsequence=( + (0x4D, 0x78,), + (0x00, 0x0F, 0x29,), + (0x06, 0x0d, 0x12, 0x30, 0x20, 0x19, 0x2a, 0x22,), + (0x50, 0x37,), + (0x61, 200 // 256, 200 % 256, 200 // 256, 200 % 256,), # RES: 200x200 fixed + (0xE9, 0x01,), + (0x30, 0x08,), + # Power On (0x04): Must be early part of init seq = Disabled later! + (0x04,), + ), + fast_update=( + (0xE0, 0x02,), + (0xE6, 0x5D,), + (0xA5, 0x00,), + ), +) + +# Waveshare 7.5-H +# +# Vendor init derived from vendor sample code +# +# Compatible MIT license, see esphome/LICENSE file. +# +# Note: busy pin uses LOW=busy, HIGH=idle. Configure with inverted: true in YAML. +# +# fmt: off +jd79660.extend( + "Waveshare-7.5in-H", + width=800, + height=480, + + initsequence=( + (0x00, 0x0F, 0x29,), + (0x06, 0x0F, 0x8B, 0x93, 0xA1,), + (0x41, 0x00,), + (0x50, 0x37,), + (0x60, 0x02, 0x02,), + (0x61, 800 // 256, 800 % 256, 480 // 256, 480 % 256,), # RES: 800x480 + (0x62, 0x98, 0x98, 0x98, 0x75, 0xCA, 0xB2, 0x98, 0x7E,), + (0x65, 0x00, 0x00, 0x00, 0x00,), + (0xE7, 0x1C,), + (0xE3, 0x00,), + (0xE9, 0x01,), + (0x30, 0x08,), + # Power On (0x04): Must be early part of init seq = Disabled later! + (0x04,), + ), +) diff --git a/esphome/components/epaper_spi/models/weact_bwr.py b/esphome/components/epaper_spi/models/weact_bwr.py new file mode 100644 index 0000000000..21a015b08d --- /dev/null +++ b/esphome/components/epaper_spi/models/weact_bwr.py @@ -0,0 +1,75 @@ +"""WeAct Black/White/Red e-paper displays using SSD1683 controller. + +Supported models: +- weact-2.13in-3c: 122x250 pixels (2.13" display) +- weact-2.9in-3c: 128x296 pixels (2.9" display) +- weact-4.2in-3c: 400x300 pixels (4.2" display) + +These displays use SSD1680 or SSD1683 controller and require a specific initialization +sequence. The DRV_OUT_CTL command is calculated from the display height. +""" + +from . import EpaperModel + + +class WeActBWR(EpaperModel): + """Base EpaperModel class for WeAct Black/White/Red displays using SSD1683 controller.""" + + def __init__(self, name, **defaults): + super().__init__(name, "EPaperWeAct3C", **defaults) + + def get_init_sequence(self, config): + """Generate initialization sequence for WeAct BWR displays. + + The initialization sequence is based on SSD1680 and SSD1683 controller datasheet + and the WeAct display specifications. + """ + _, height = self.get_dimensions(config) + # DRV_OUT_CTL: MSB of (height-1), LSB of (height-1), gate setting (0x00) + height_minus_1 = height - 1 + msb = height_minus_1 >> 8 + lsb = height_minus_1 & 0xFF + return ( + # Step 1: Software Reset (0x12) - REQUIRED per SSD1680, but works without it as well, so it's commented out for now + # (0x12,), + # Step 2: Wait 10ms after SWRESET (?) not sure how to implement wht waiting for 10ms after SWRESET, so it's commented out for now + # Step 3: DRV_OUT_CTL - driver output control (height-dependent) + # Format: (command, LSB, MSB, gate setting) + (0x01, lsb, msb, 0x00), + # Step 4: DATA_ENTRY - data entry mode (0x03 = decrement Y, increment X) + (0x11, 0x03), + # Step 5: BORDER_FULL - border waveform control + (0x3C, 0x05), + # Step 6: TEMP_SENS - internal temperature sensor + (0x18, 0x80), + # Step 7: DISPLAY_UPDATE - display update control + (0x21, 0x00, 0x80), + ) + + +# Model: WeAct 2.9" 3C - 128x296 pixels, SSD1680 controller +weact_2p9in3c = WeActBWR( + "weact-2.9in-3c", + width=128, + height=296, + data_rate="10MHz", + minimum_update_interval="1s", +) + +# Model: WeAct 2.13" 3C - 122x250 pixels, SSD1680 controller +weact_2p13in3c = WeActBWR( + "weact-2.13in-3c", + width=122, + height=250, + data_rate="10MHz", + minimum_update_interval="1s", +) + +# Model: WeAct 4.2" 3C - 400x300 pixels, SSD1683 controller +weact_4p2in3c = WeActBWR( + "weact-4.2in-3c", + width=400, + height=300, + data_rate="10MHz", + minimum_update_interval="10s", +) diff --git a/esphome/components/es7210/es7210.cpp b/esphome/components/es7210/es7210.cpp index 1358121c1b..4371075fa9 100644 --- a/esphome/components/es7210/es7210.cpp +++ b/esphome/components/es7210/es7210.cpp @@ -172,7 +172,7 @@ uint8_t ES7210::es7210_gain_reg_value_(float mic_gain) { // reg: 12 - 34.5dB, 13 - 36dB, 14 - 37.5dB mic_gain += 0.5; if (mic_gain <= 33.0) { - return (uint8_t) mic_gain / 3; + return (uint8_t) (mic_gain / 3); } if (mic_gain < 36.0) { return 12; diff --git a/esphome/components/es8156/audio_dac.py b/esphome/components/es8156/audio_dac.py index b9d8eae6b0..c5fb6096da 100644 --- a/esphome/components/es8156/audio_dac.py +++ b/esphome/components/es8156/audio_dac.py @@ -2,7 +2,8 @@ import esphome.codegen as cg from esphome.components import i2c from esphome.components.audio_dac import AudioDac 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"] 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): var = cg.new_Pvariable(config[CONF_ID]) await cg.register_component(var, config) diff --git a/esphome/components/es8156/es8156.cpp b/esphome/components/es8156/es8156.cpp index e84252efe2..961dc24b29 100644 --- a/esphome/components/es8156/es8156.cpp +++ b/esphome/components/es8156/es8156.cpp @@ -17,24 +17,61 @@ static const char *const TAG = "es8156"; } 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)); + + // 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)); + + // REG21 ANALOG SYSTEM: VSEL=0x1C (bias level ~120%), normal VREF ramp speed 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)); + + // 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)); + + // REG23 ANALOG SYSTEM: Lowest bias (IBIAS_SW=0), VMIDLVL=VDDA/2, normal impedance 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_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)); + + // REG19 EQ CONTROL 1: EQ disabled (EQ_ON=0), EQ_BAND_NUM=2 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)); + + // REG09 MISC CONTROL 2: Default settings 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)); + + // REG08 CLOCK OFF: Enable all internal clocks (0x3F = all clock gates open) 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)); + // Then: CSM_ON=1 (enable chip state machine), RST_DIG=1 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)); } diff --git a/esphome/components/es8388/es8388.cpp b/esphome/components/es8388/es8388.cpp index 9deb29416f..c252cdb707 100644 --- a/esphome/components/es8388/es8388.cpp +++ b/esphome/components/es8388/es8388.cpp @@ -152,7 +152,7 @@ void ES8388::dump_config() { bool ES8388::set_volume(float volume) { volume = clamp(volume, 0.0f, 1.0f); - uint8_t value = remap(volume, 0.0f, 1.0f, -96, 0); + uint8_t value = remap(volume, 0.0f, 1.0f, 192, 0); ESP_LOGD(TAG, "Setting ES8388_DACCONTROL4 / ES8388_DACCONTROL5 to 0x%02X (volume: %f)", value, volume); ES8388_ERROR_CHECK(this->write_byte(ES8388_DACCONTROL4, value)); ES8388_ERROR_CHECK(this->write_byte(ES8388_DACCONTROL5, value)); @@ -163,7 +163,7 @@ bool ES8388::set_volume(float volume) { float ES8388::volume() { uint8_t value; ES8388_ERROR_CHECK(this->read_byte(ES8388_DACCONTROL4, &value)); - return remap(value, -96, 0, 0.0f, 1.0f); + return remap(value, 192, 0, 0.0f, 1.0f); } bool ES8388::set_mute_state_(bool mute_state) { @@ -209,9 +209,10 @@ bool ES8388::set_dac_output(DacOutputLine line) { }; ESP_LOGV(TAG, - "Setting ES8388_DACPOWER to 0x%02X\n" - "Setting ES8388_DACCONTROL24 / ES8388_DACCONTROL25 to 0x%02X\n" - "Setting ES8388_DACCONTROL26 / ES8388_DACCONTROL27 to 0x%02X", + "DAC output config:\n" + " DACPOWER: 0x%02X\n" + " DACCONTROL24/25: 0x%02X\n" + " DACCONTROL26/27: 0x%02X", dac_power, reg_out1, reg_out2); ES8388_ERROR_CHECK(this->write_byte(ES8388_DACCONTROL24, reg_out1)); // LOUT1VOL diff --git a/esphome/components/esp32/__init__.py b/esphome/components/esp32/__init__.py index fccf0ed09f..0ce1117262 100644 --- a/esphome/components/esp32/__init__.py +++ b/esphome/components/esp32/__init__.py @@ -14,6 +14,8 @@ from esphome.const import ( CONF_BOARD, CONF_COMPONENTS, CONF_DISABLED, + CONF_ENABLE_FULL_PRINTF, + CONF_ENABLE_OTA_ROLLBACK, CONF_ESPHOME, CONF_FRAMEWORK, CONF_IGNORE_EFUSE_CUSTOM_MAC, @@ -25,8 +27,8 @@ from esphome.const import ( CONF_PLATFORM_VERSION, CONF_PLATFORMIO_OPTIONS, CONF_REF, - CONF_REFRESH, CONF_SAFE_MODE, + CONF_SIZE, CONF_SOURCE, CONF_TYPE, CONF_VARIANT, @@ -41,21 +43,24 @@ from esphome.const import ( ThreadModel, __version__, ) -from esphome.core import CORE, HexInt, TimePeriod +from esphome.core import CORE, HexInt from esphome.coroutine import CoroPriority, coroutine_with_priority import esphome.final_validate as fv -from esphome.helpers import copy_file_if_changed, write_file_if_changed +from esphome.helpers import copy_file_if_changed, rmtree, write_file_if_changed from esphome.types import ConfigType from esphome.writer import clean_cmake_cache from .boards import BOARDS, STANDARD_BOARDS from .const import ( # noqa + KEY_ARDUINO_LIBRARIES, KEY_BOARD, KEY_COMPONENTS, KEY_ESP32, + KEY_EXCLUDE_COMPONENTS, KEY_EXTRA_BUILD_FILES, KEY_FLASH_SIZE, KEY_FULL_CERT_BUNDLE, + KEY_IDF_VERSION, KEY_PATH, KEY_REF, KEY_REPO, @@ -86,11 +91,19 @@ IS_TARGET_PLATFORM = True CONF_ASSERTION_LEVEL = "assertion_level" CONF_COMPILER_OPTIMIZATION = "compiler_optimization" CONF_ENABLE_IDF_EXPERIMENTAL_FEATURES = "enable_idf_experimental_features" +CONF_ENGINEERING_SAMPLE = "engineering_sample" +CONF_INCLUDE_BUILTIN_IDF_COMPONENTS = "include_builtin_idf_components" CONF_ENABLE_LWIP_ASSERT = "enable_lwip_assert" -CONF_ENABLE_OTA_ROLLBACK = "enable_ota_rollback" CONF_EXECUTE_FROM_PSRAM = "execute_from_psram" CONF_MINIMUM_CHIP_REVISION = "minimum_chip_revision" CONF_RELEASE = "release" +CONF_SRAM1_AS_IRAM = "sram1_as_iram" +CONF_SUBTYPE = "subtype" + +ARDUINO_FRAMEWORK_NAME = "framework-arduinoespressif32" +ARDUINO_FRAMEWORK_PKG = f"pioarduino/{ARDUINO_FRAMEWORK_NAME}" +ARDUINO_LIBS_NAME = f"{ARDUINO_FRAMEWORK_NAME}-libs" +ARDUINO_LIBS_PKG = f"pioarduino/{ARDUINO_LIBS_NAME}" LOG_LEVELS_IDF = [ "NONE", @@ -114,6 +127,205 @@ COMPILER_OPTIMIZATIONS = { "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 + "driver", # Legacy driver shim - only needed by esp32_touch, esp32_can for legacy headers + "esp_adc", # ADC driver - only needed by adc component + "esp_driver_dac", # DAC driver - only needed by esp32_dac component + "esp_driver_i2s", # I2S driver - only needed by i2s_audio component + "esp_driver_mcpwm", # MCPWM driver - ESPHome doesn't use motor control PWM + "esp_driver_pcnt", # PCNT driver - only needed by pulse_counter, hlw8012 components + "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_driver_twai", # TWAI/CAN driver - only needed by esp32_can component + "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 + "openthread", # Thread protocol - only needed by openthread component + "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) + "ulp", # ULP coprocessor - not currently used by any ESPHome component + "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 +) + +# Additional IDF managed components to exclude for Arduino framework builds +# These are pulled in by the Arduino framework's idf_component.yml but not used by ESPHome +# Note: Component names include the namespace prefix (e.g., "espressif__cbor") because +# that's how managed components are registered in the IDF build system +# List includes direct dependencies from arduino-esp32/idf_component.yml +# plus transitive dependencies from RainMaker/Insights (except espressif/mdns which we need) +ARDUINO_EXCLUDED_IDF_COMPONENTS = ( + "chmorgan__esp-libhelix-mp3", # MP3 decoder - not used + "espressif__cbor", # CBOR library - only used by RainMaker/Insights + "espressif__esp-dsp", # DSP library - not used + "espressif__esp-modbus", # Modbus - ESPHome has its own + "espressif__esp-sr", # Speech recognition - not used + "espressif__esp-zboss-lib", # Zigbee ZBOSS library - not used + "espressif__esp-zigbee-lib", # Zigbee library - not used + "espressif__esp_diag_data_store", # Diagnostics - not used + "espressif__esp_diagnostics", # Diagnostics - not used + "espressif__esp_hosted", # ESP hosted - only for ESP32-P4 + "espressif__esp_insights", # ESP Insights - not used + "espressif__esp_modem", # Modem library - not used + "espressif__esp_rainmaker", # RainMaker - not used + "espressif__esp_rcp_update", # RCP update - RainMaker transitive dep + "espressif__esp_schedule", # Schedule - RainMaker transitive dep + "espressif__esp_secure_cert_mgr", # Secure cert - RainMaker transitive dep + "espressif__esp_wifi_remote", # WiFi remote - only for ESP32-P4 + "espressif__json_generator", # JSON generator - RainMaker transitive dep + "espressif__json_parser", # JSON parser - RainMaker transitive dep + "espressif__lan867x", # Ethernet PHY - ESPHome uses ESP-IDF ethernet directly + "espressif__libsodium", # Crypto - ESPHome uses its own noise-c library + "espressif__network_provisioning", # Network provisioning - not used + "espressif__qrcode", # QR code - not used + "espressif__rmaker_common", # RainMaker common - not used + "joltwallet__littlefs", # LittleFS - ESPHome doesn't use filesystem +) + +# Mapping of Arduino libraries to IDF managed components they require +# When an Arduino library is enabled via cg.add_library(), these components +# are automatically un-stubbed from ARDUINO_EXCLUDED_IDF_COMPONENTS. +# +# Note: Some libraries (Matter, LittleFS, ESP_SR, WiFiProv, ArduinoOTA) already have +# conditional maybe_add_component() calls in arduino-esp32/CMakeLists.txt that handle +# their managed component dependencies. Our mapping is primarily needed for libraries +# that don't have such conditionals (Ethernet, PPP, Zigbee, RainMaker, Insights, etc.) +# and to ensure the stubs are removed from our idf_component.yml overrides. +ARDUINO_LIBRARY_IDF_COMPONENTS: dict[str, tuple[str, ...]] = { + "BLE": ("esp_driver_gptimer",), + "BluetoothSerial": ("esp_driver_gptimer",), + "ESP_HostedOTA": ("espressif__esp_hosted", "espressif__esp_wifi_remote"), + "ESP_SR": ("espressif__esp-sr",), + "Ethernet": ("espressif__lan867x",), + "FFat": ("fatfs",), + "Insights": ( + "espressif__cbor", + "espressif__esp_insights", + "espressif__esp_diagnostics", + "espressif__esp_diag_data_store", + "espressif__rmaker_common", # Transitive dep from esp_insights + ), + "LittleFS": ("joltwallet__littlefs",), + "Matter": ("espressif__esp_matter",), + "PPP": ("espressif__esp_modem",), + "RainMaker": ( + # Direct deps from idf_component.yml + "espressif__cbor", + "espressif__esp_rainmaker", + "espressif__esp_insights", + "espressif__esp_diagnostics", + "espressif__esp_diag_data_store", + "espressif__rmaker_common", + "espressif__qrcode", + # Transitive deps from esp_rainmaker + "espressif__esp_rcp_update", + "espressif__esp_schedule", + "espressif__esp_secure_cert_mgr", + "espressif__json_generator", + "espressif__json_parser", + "espressif__network_provisioning", + ), + "SD": ("fatfs",), + "SD_MMC": ("fatfs",), + "SPIFFS": ("spiffs",), + "WiFiProv": ("espressif__network_provisioning", "espressif__qrcode"), + "Zigbee": ("espressif__esp-zigbee-lib", "espressif__esp-zboss-lib"), +} + +# Arduino library to Arduino library dependencies +# When enabling one library, also enable its dependencies +# Kconfig "select" statements don't work with CONFIG_ARDUINO_SELECTIVE_COMPILATION +ARDUINO_LIBRARY_DEPENDENCIES: dict[str, tuple[str, ...]] = { + "Ethernet": ("Network",), + "WiFi": ("Network",), +} + + +def _idf_component_stub_name(component: str) -> str: + """Get stub directory name from IDF component name. + + Component names are typically namespace__name (e.g., espressif__cbor). + Returns just the name part (e.g., cbor). If no namespace is present, + returns the original component name. + """ + _prefix, sep, suffix = component.partition("__") + return suffix if sep else component + + +def _idf_component_dep_name(component: str) -> str: + """Convert IDF component name to dependency format. + + Converts espressif__cbor to espressif/cbor. + """ + return component.replace("__", "/") + + +# Arduino libraries to disable by default when using Arduino framework +# ESPHome uses ESP-IDF APIs directly; we only need the Arduino core +# (HardwareSerial, Print, Stream, GPIO functions which are always compiled) +# Components use cg.add_library() which auto-enables any they need +# This list must match ARDUINO_ALL_LIBRARIES from arduino-esp32/CMakeLists.txt +ARDUINO_DISABLED_LIBRARIES: frozenset[str] = frozenset( + { + "ArduinoOTA", + "AsyncUDP", + "BLE", + "BluetoothSerial", + "DNSServer", + "EEPROM", + "ESP_HostedOTA", + "ESP_I2S", + "ESP_NOW", + "ESP_SR", + "ESPmDNS", + "Ethernet", + "FFat", + "FS", + "Hash", + "HTTPClient", + "HTTPUpdate", + "Insights", + "LittleFS", + "Matter", + "NetBIOS", + "Network", + "NetworkClientSecure", + "OpenThread", + "PPP", + "Preferences", + "RainMaker", + "SD", + "SD_MMC", + "SimpleBLE", + "SPI", + "SPIFFS", + "Ticker", + "Update", + "USB", + "WebServer", + "WiFi", + "WiFiProv", + "Wire", + "Zigbee", + } +) + # ESP32 (original) chip revision options # Setting minimum revision to 3.0 or higher: # - Reduces flash size by excluding workaround code for older chip bugs @@ -167,12 +379,11 @@ FULL_CPU_FREQUENCIES = set(itertools.chain.from_iterable(CPU_FREQUENCIES.values( def set_core_data(config): cpu_frequency = config.get(CONF_CPU_FREQUENCY, None) variant = config[CONF_VARIANT] - # if not specified in config, set to 160MHz if supported, the fastest otherwise + # if not specified in config, default to the maximum supported frequency + # (ESP32-P4 engineering samples are limited to 360MHz, non-engineering can do 400MHz) if cpu_frequency is None: choices = CPU_FREQUENCIES[variant] - if "160MHZ" in choices: - cpu_frequency = "160MHZ" - elif "360MHZ" in choices: + if variant == VARIANT_ESP32P4 and config.get(CONF_ENGINEERING_SAMPLE): cpu_frequency = "360MHZ" else: cpu_frequency = choices[-1] @@ -203,9 +414,29 @@ def set_core_data(config): ) CORE.data[KEY_ESP32][KEY_SDKCONFIG_OPTIONS] = {} CORE.data[KEY_ESP32][KEY_COMPONENTS] = {} - CORE.data[KEY_CORE][KEY_FRAMEWORK_VERSION] = cv.Version.parse( - config[CONF_FRAMEWORK][CONF_VERSION] - ) + # Initialize with default exclusions - components can call include_builtin_idf_component() + # to re-enable any they need + excluded = set(DEFAULT_EXCLUDED_IDF_COMPONENTS) + # Add Arduino-specific managed component exclusions when using Arduino framework + if conf[CONF_TYPE] == FRAMEWORK_ARDUINO: + excluded.update(ARDUINO_EXCLUDED_IDF_COMPONENTS) + CORE.data[KEY_ESP32][KEY_EXCLUDE_COMPONENTS] = excluded + # Initialize Arduino library tracking - cg.add_library() auto-enables libraries + CORE.data[KEY_ESP32][KEY_ARDUINO_LIBRARIES] = set() + framework_ver = cv.Version.parse(config[CONF_FRAMEWORK][CONF_VERSION]) + CORE.data[KEY_CORE][KEY_FRAMEWORK_VERSION] = framework_ver + + # Store the underlying IDF version for framework-agnostic checks + if conf[CONF_TYPE] == FRAMEWORK_ESP_IDF: + CORE.data[KEY_ESP32][KEY_IDF_VERSION] = framework_ver + elif (idf_ver := ARDUINO_IDF_VERSION_LOOKUP.get(framework_ver)) is not None: + CORE.data[KEY_ESP32][KEY_IDF_VERSION] = idf_ver + else: + raise cv.Invalid( + f"Arduino version {framework_ver} has no known ESP-IDF version mapping. " + "Please update ARDUINO_IDF_VERSION_LOOKUP.", + path=[CONF_FRAMEWORK, CONF_VERSION], + ) CORE.data[KEY_ESP32][KEY_BOARD] = config[CONF_BOARD] CORE.data[KEY_ESP32][KEY_FLASH_SIZE] = config[CONF_FLASH_SIZE] @@ -248,6 +479,8 @@ def only_on_variant(*, supported=None, unsupported=None, msg_prefix="This featur unsupported = [unsupported] def validator_(obj): + if not CORE.is_esp32: + raise cv.Invalid(f"{msg_prefix} is only available on ESP32") variant = get_esp32_variant() if supported is not None and variant not in supported: raise cv.Invalid( @@ -283,49 +516,66 @@ def add_idf_component( repo: str | None = None, ref: str | None = None, path: str | None = None, - refresh: TimePeriod | None = None, - components: list[str] | None = None, - submodules: list[str] | None = None, ): """Add an esp-idf component to the project.""" if not repo and not ref and not path: raise ValueError("Requires at least one of repo, ref or path") - if refresh or submodules or components: - _LOGGER.warning( - "The refresh, components and submodules parameters in add_idf_component() are " - "deprecated and will be removed in ESPHome 2026.1. If you are seeing this, report " - "an issue to the external_component author and ask them to update it." - ) components_registry = CORE.data[KEY_ESP32][KEY_COMPONENTS] - if components: - for comp in components: - existing = components_registry.get(comp) - if existing and existing.get(KEY_REF) != ref: - _LOGGER.warning( - "IDF component %s version conflict %s replaced by %s", - comp, - existing.get(KEY_REF), - ref, - ) - components_registry[comp] = { - KEY_REPO: repo, - KEY_REF: ref, - KEY_PATH: f"{path}/{comp}" if path else comp, - } - else: - existing = components_registry.get(name) - if existing and existing.get(KEY_REF) != ref: - _LOGGER.warning( - "IDF component %s version conflict %s replaced by %s", - name, - existing.get(KEY_REF), - ref, - ) - components_registry[name] = { - KEY_REPO: repo, - KEY_REF: ref, - KEY_PATH: path, - } + existing = components_registry.get(name) + if existing and existing.get(KEY_REF) != ref: + _LOGGER.warning( + "IDF component %s version conflict %s replaced by %s", + name, + existing.get(KEY_REF), + ref, + ) + components_registry[name] = { + KEY_REPO: repo, + KEY_REF: ref, + KEY_PATH: path, + } + + +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 _enable_arduino_library(name: str) -> None: + """Enable an Arduino library that is disabled by default. + + This is called automatically by CORE.add_library() when a component adds + an Arduino library via cg.add_library(). Components should not call this + directly - just use cg.add_library("LibName", None). + + Args: + name: The library name (e.g., "Wire", "SPI", "WiFi") + """ + enabled_libs: set[str] = CORE.data[KEY_ESP32][KEY_ARDUINO_LIBRARIES] + enabled_libs.add(name) + # Also enable any required Arduino library dependencies + for dep_lib in ARDUINO_LIBRARY_DEPENDENCIES.get(name, ()): + enabled_libs.add(dep_lib) + # Also enable any required IDF components + for idf_component in ARDUINO_LIBRARY_IDF_COMPONENTS.get(name, ()): + include_builtin_idf_component(idf_component) def add_extra_script(stage: str, filename: str, path: Path): @@ -347,26 +597,32 @@ def add_extra_build_file(filename: str, path: Path) -> bool: def _format_framework_arduino_version(ver: cv.Version) -> str: - # format the given arduino (https://github.com/espressif/arduino-esp32/releases) version to - # a PIO pioarduino/framework-arduinoespressif32 value # 3.3.6+ changed filename from esp32-{ver}.zip to esp32-core-{ver}.tar.xz if ver >= cv.Version(3, 3, 6): filename = f"esp32-core-{ver}.tar.xz" else: filename = f"esp32-{ver}.zip" - return f"pioarduino/framework-arduinoespressif32@https://github.com/espressif/arduino-esp32/releases/download/{ver}/{filename}" + return f"{ARDUINO_FRAMEWORK_PKG}@https://github.com/espressif/arduino-esp32/releases/download/{ver}/{filename}" -def _format_framework_espidf_version(ver: cv.Version, release: str) -> str: +def _format_framework_espidf_version( + ver: cv.Version, release: str | None = None +) -> str: # format the given espidf (https://github.com/pioarduino/esp-idf/releases) version to # a PIO platformio/framework-espidf value if ver == cv.Version(5, 4, 3) or ver >= cv.Version(5, 5, 1): ext = "tar.xz" else: ext = "zip" + # Build version string with extra separator based on type: + # numeric extra uses dot (e.g., "5.5.3.1"), string extra uses dash (e.g., "6.0.0-rc1") + ver_str = f"{ver.major}.{ver.minor}.{ver.patch}" + if ver.extra: + sep = "." if str(ver.extra).isdigit() else "-" + ver_str += f"{sep}{ver.extra}" if release: - return f"pioarduino/framework-espidf@https://github.com/pioarduino/esp-idf/releases/download/v{str(ver)}.{release}/esp-idf-v{str(ver)}.{ext}" - return f"pioarduino/framework-espidf@https://github.com/pioarduino/esp-idf/releases/download/v{str(ver)}/esp-idf-v{str(ver)}.{ext}" + return f"pioarduino/framework-espidf@https://github.com/pioarduino/esp-idf/releases/download/v{ver_str}.{release}/esp-idf-v{ver_str}.{ext}" + return f"pioarduino/framework-espidf@https://github.com/pioarduino/esp-idf/releases/download/v{ver_str}/esp-idf-v{ver_str}.{ext}" def _is_framework_url(source: str) -> bool: @@ -389,11 +645,12 @@ def _is_framework_url(source: str) -> bool: # The default/recommended arduino framework version # - https://github.com/espressif/arduino-esp32/releases ARDUINO_FRAMEWORK_VERSION_LOOKUP = { - "recommended": cv.Version(3, 3, 6), - "latest": cv.Version(3, 3, 6), - "dev": cv.Version(3, 3, 6), + "recommended": cv.Version(3, 3, 7), + "latest": cv.Version(3, 3, 7), + "dev": cv.Version(3, 3, 7), } ARDUINO_PLATFORM_VERSION_LOOKUP = { + cv.Version(3, 3, 7): cv.Version(55, 3, 37), cv.Version(3, 3, 6): cv.Version(55, 3, 36), cv.Version(3, 3, 5): cv.Version(55, 3, 35), cv.Version(3, 3, 4): cv.Version(55, 3, 31, "2"), @@ -412,6 +669,7 @@ ARDUINO_PLATFORM_VERSION_LOOKUP = { # These versions correspond to pioarduino/esp-idf releases # See: https://github.com/pioarduino/esp-idf/releases ARDUINO_IDF_VERSION_LOOKUP = { + cv.Version(3, 3, 7): cv.Version(5, 5, 3, "1"), cv.Version(3, 3, 6): cv.Version(5, 5, 2), cv.Version(3, 3, 5): cv.Version(5, 5, 2), cv.Version(3, 3, 4): cv.Version(5, 5, 1), @@ -430,12 +688,20 @@ ARDUINO_IDF_VERSION_LOOKUP = { # The default/recommended esp-idf framework version # - https://github.com/espressif/esp-idf/releases ESP_IDF_FRAMEWORK_VERSION_LOOKUP = { - "recommended": cv.Version(5, 5, 2), - "latest": cv.Version(5, 5, 2), - "dev": cv.Version(5, 5, 2), + "recommended": cv.Version(5, 5, 3, "1"), + "latest": cv.Version(5, 5, 3, "1"), + "dev": cv.Version(5, 5, 4), } ESP_IDF_PLATFORM_VERSION_LOOKUP = { - cv.Version(5, 5, 2): cv.Version(55, 3, 36), + cv.Version( + 6, 0, 0 + ): "https://github.com/pioarduino/platform-espressif32.git#prep_IDF6", + cv.Version( + 5, 5, 4 + ): "https://github.com/pioarduino/platform-espressif32.git#develop", + cv.Version(5, 5, 3, "1"): cv.Version(55, 3, 37), + cv.Version(5, 5, 3): cv.Version(55, 3, 37), + cv.Version(5, 5, 2): cv.Version(55, 3, 37), cv.Version(5, 5, 1): cv.Version(55, 3, 31, "2"), cv.Version(5, 5, 0): cv.Version(55, 3, 31, "2"), cv.Version(5, 4, 3): cv.Version(55, 3, 32), @@ -452,9 +718,9 @@ ESP_IDF_PLATFORM_VERSION_LOOKUP = { # The platform-espressif32 version # - https://github.com/pioarduino/platform-espressif32/releases PLATFORM_VERSION_LOOKUP = { - "recommended": cv.Version(55, 3, 36), - "latest": cv.Version(55, 3, 36), - "dev": cv.Version(55, 3, 36), + "recommended": cv.Version(55, 3, 37), + "latest": cv.Version(55, 3, 37), + "dev": "https://github.com/pioarduino/platform-espressif32.git#develop", } @@ -489,9 +755,7 @@ def _check_versions(config): CONF_SOURCE, _format_framework_arduino_version(version) ) if _is_framework_url(value[CONF_SOURCE]): - value[CONF_SOURCE] = ( - f"pioarduino/framework-arduinoespressif32@{value[CONF_SOURCE]}" - ) + value[CONF_SOURCE] = f"{ARDUINO_FRAMEWORK_PKG}@{value[CONF_SOURCE]}" else: if version < cv.Version(5, 0, 0): raise cv.Invalid("Only ESP-IDF 5.0+ is supported.") @@ -499,7 +763,7 @@ def _check_versions(config): platform_lookup = ESP_IDF_PLATFORM_VERSION_LOOKUP.get(version) value[CONF_SOURCE] = value.get( CONF_SOURCE, - _format_framework_espidf_version(version, value.get(CONF_RELEASE, None)), + _format_framework_espidf_version(version, value.get(CONF_RELEASE)), ) if _is_framework_url(value[CONF_SOURCE]): value[CONF_SOURCE] = f"pioarduino/framework-espidf@{value[CONF_SOURCE]}" @@ -547,6 +811,22 @@ def _detect_variant(value): # variant has already been validated against the known set value = value.copy() value[CONF_BOARD] = STANDARD_BOARDS[variant] + if variant == VARIANT_ESP32P4: + engineering_sample = value.get(CONF_ENGINEERING_SAMPLE) + if engineering_sample is None: + _LOGGER.warning( + "No board specified for ESP32-P4. Defaulting to production silicon (rev3).\n" + "If you have an early engineering sample (pre-rev3), add this to your config:\n" + "\n" + " esp32:\n" + " engineering_sample: true\n" + "\n" + "To check your chip revision, look for 'chip revision: vX.Y' in the boot log.\n" + "Engineering samples will show a revision below v3.0.\n" + "The 'debug:' component also reports the revision (e.g. Revision: 100 = v1.0, 300 = v3.0)." + ) + elif engineering_sample: + value[CONF_BOARD] = "esp32-p4-evboard" elif board in BOARDS: variant = variant or BOARDS[board][KEY_VARIANT] if variant != BOARDS[board][KEY_VARIANT]: @@ -610,11 +890,42 @@ def final_validate(config): path=[CONF_FRAMEWORK, CONF_ADVANCED, CONF_MINIMUM_CHIP_REVISION], ) ) - if advanced[CONF_EXECUTE_FROM_PSRAM]: - if config[CONF_VARIANT] != VARIANT_ESP32S3: + if config[CONF_VARIANT] != VARIANT_ESP32 and advanced[CONF_SRAM1_AS_IRAM]: + errs.append( + cv.Invalid( + f"'{CONF_SRAM1_AS_IRAM}' is only supported on {VARIANT_ESP32}", + path=[CONF_FRAMEWORK, CONF_ADVANCED, CONF_SRAM1_AS_IRAM], + ) + ) + if ( + config[CONF_VARIANT] != VARIANT_ESP32P4 + and config.get(CONF_ENGINEERING_SAMPLE) is not None + ): + errs.append( + cv.Invalid( + f"'{CONF_ENGINEERING_SAMPLE}' is only supported on {VARIANT_ESP32P4}", + path=[CONF_ENGINEERING_SAMPLE], + ) + ) + if ( + config[CONF_VARIANT] == VARIANT_ESP32P4 + and config.get(CONF_ENGINEERING_SAMPLE) is not None + ): + board_is_es = BOARDS.get(config[CONF_BOARD], {}).get( + "engineering_sample", False + ) + if config[CONF_ENGINEERING_SAMPLE] != board_is_es: errs.append( cv.Invalid( - f"'{CONF_EXECUTE_FROM_PSRAM}' is only supported on {VARIANT_ESP32S3} variant", + f"'{CONF_ENGINEERING_SAMPLE}' does not match board '{config[CONF_BOARD]}'", + path=[CONF_ENGINEERING_SAMPLE], + ) + ) + if advanced[CONF_EXECUTE_FROM_PSRAM]: + if config[CONF_VARIANT] not in {VARIANT_ESP32S3, VARIANT_ESP32P4}: + errs.append( + cv.Invalid( + f"'{CONF_EXECUTE_FROM_PSRAM}' is not available on this esp32 variant", path=[CONF_FRAMEWORK, CONF_ADVANCED, CONF_EXECUTE_FROM_PSRAM], ) ) @@ -672,11 +983,27 @@ CONF_RINGBUF_IN_IRAM = "ringbuf_in_iram" CONF_HEAP_IN_IRAM = "heap_in_iram" CONF_LOOP_TASK_STACK_SIZE = "loop_task_stack_size" 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 -# 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_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" +KEY_MBEDTLS_SHA512_REQUIRED = "mbedtls_sha512_required" def require_vfs_select() -> None: @@ -697,6 +1024,15 @@ def require_vfs_dir() -> None: 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: """Request the full certificate bundle instead of the common-CAs-only bundle. @@ -709,6 +1045,62 @@ def require_full_certificate_bundle() -> None: 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_mbedtls_sha512() -> None: + """Mark that mbedTLS SHA-384/SHA-512 support is required by a component. + + Call this from components that need to verify TLS certificates or signatures + using SHA-384 or SHA-512 algorithms. This prevents CONFIG_MBEDTLS_SHA384_C + and CONFIG_MBEDTLS_SHA512_C from being disabled. + """ + CORE.data[KEY_ESP32][KEY_MBEDTLS_SHA512_REQUIRED] = True + + +def idf_version() -> cv.Version: + """Return the underlying ESP-IDF version regardless of framework choice. + + For ESP-IDF builds this is the framework version directly. + For Arduino builds this is the mapped IDF version from ARDUINO_IDF_VERSION_LOOKUP. + """ + return CORE.data[KEY_ESP32][KEY_IDF_VERSION] + + +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: """Parse IDF component shorthand syntax like 'owner/component^version'""" # Match operator followed by version-like string (digit or *) @@ -720,16 +1112,6 @@ def _parse_idf_component(value: str) -> ConfigType: ) -def _validate_idf_component(config: ConfigType) -> ConfigType: - """Validate IDF component config and warn about deprecated options.""" - if CONF_REFRESH in config: - _LOGGER.warning( - "The 'refresh' option for IDF components is deprecated and has no effect. " - "It will be removed in ESPHome 2026.1. Please remove it from your configuration." - ) - return config - - FRAMEWORK_ESP_IDF = "esp-idf" FRAMEWORK_ARDUINO = "arduino" FRAMEWORK_SCHEMA = cv.Schema( @@ -762,6 +1144,7 @@ FRAMEWORK_SCHEMA = cv.Schema( cv.Optional(CONF_MINIMUM_CHIP_REVISION): cv.one_of( *ESP32_CHIP_REVISIONS ), + cv.Optional(CONF_SRAM1_AS_IRAM, default=False): cv.boolean, # DHCP server is needed for WiFi AP mode. When WiFi component is used, # it will handle disabling DHCP server when AP is not configured. # Default to false (disabled) when WiFi is not used. @@ -793,6 +1176,20 @@ FRAMEWORK_SCHEMA = cv.Schema( cv.Optional( CONF_USE_FULL_CERTIFICATE_BUNDLE, default=False ): cv.boolean, + cv.Optional( + CONF_INCLUDE_BUILTIN_IDF_COMPONENTS, default=[] + ): cv.ensure_list(cv.string_strict), + cv.Optional(CONF_ENABLE_FULL_PRINTF, default=False): cv.boolean, + 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( @@ -805,13 +1202,9 @@ FRAMEWORK_SCHEMA = cv.Schema( cv.Optional(CONF_SOURCE): cv.git_ref, cv.Optional(CONF_REF): cv.string, cv.Optional(CONF_PATH): cv.string, - cv.Optional(CONF_REFRESH): cv.All( - cv.string, cv.source_refresh - ), } ), ), - _validate_idf_component, ) ), } @@ -842,8 +1235,8 @@ def _show_framework_migration_message(name: str, variant: str) -> None: + "(We've been warning about this change since ESPHome 2025.8.0)\n" + "\n" + "Why we made this change:\n" - + color(AnsiFore.GREEN, " ✨ Up to 40% smaller firmware binaries\n") - + color(AnsiFore.GREEN, " ⚡ 2-3x faster compile times\n") + + color(AnsiFore.GREEN, " ✨ Smaller firmware binaries\n") + + color(AnsiFore.GREEN, " ⚡ Faster compile times\n") + color(AnsiFore.GREEN, " 🚀 Better performance and newer features\n") + color(AnsiFore.GREEN, " 🔧 More actively maintained by ESPHome\n") + "\n" @@ -881,6 +1274,43 @@ def _set_default_framework(config): return config +RESERVED_PARTITION_NAMES = { + "nvs", + "app0", + "app1", + "otadata", + "eeprom", + "spiffs", + "phy_init", +} + +VALID_APP_SUBTYPES = {"factory", "test"} +VALID_DATA_SUBTYPES = { + "nvs", + "nvs_keys", + "spiffs", + "coredump", + "efuse", + "fat", + "undefined", + "littlefs", +} + + +def _validate_custom_partition(config: ConfigType) -> ConfigType: + """Voluptuous validator for custom partition schema.""" + try: + _validate_partition( + config[CONF_NAME], + config[CONF_TYPE], + config[CONF_SUBTYPE], + config[CONF_SIZE], + ) + except ValueError as e: + raise cv.Invalid(str(e)) from e + return config + + FLASH_SIZES = [ "2MB", "4MB", @@ -899,10 +1329,32 @@ CONFIG_SCHEMA = cv.All( cv.Optional(CONF_CPU_FREQUENCY): cv.one_of( *FULL_CPU_FREQUENCIES, upper=True ), + cv.Optional(CONF_ENGINEERING_SAMPLE): cv.boolean, cv.Optional(CONF_FLASH_SIZE, default="4MB"): cv.one_of( *FLASH_SIZES, upper=True ), - cv.Optional(CONF_PARTITIONS): cv.file_, + cv.Optional(CONF_PARTITIONS): cv.Any( + cv.file_, + cv.ensure_list( + cv.All( + cv.Schema( + { + cv.Required(CONF_NAME): cv.string_strict, + cv.Required(CONF_TYPE): cv.All( + cv.Any(cv.string_strict, cv.int_range(0x40, 0xFE)), + cv.int_to_hex_string, + ), + cv.Required(CONF_SUBTYPE): cv.All( + cv.Any(cv.string_strict, cv.int_range(0, 0xFE)), + cv.int_to_hex_string, + ), + cv.Required(CONF_SIZE): cv.int_range(min=0x1000), + } + ), + _validate_custom_partition, + ), + ), + ), cv.Optional(CONF_VARIANT): cv.one_of(*VARIANTS, upper=True), cv.Optional(CONF_FRAMEWORK): FRAMEWORK_SCHEMA, } @@ -925,21 +1377,15 @@ def _configure_lwip_max_sockets(conf: dict) -> None: This function runs in to_code() after all components have registered their socket needs. User-provided sdkconfig_options take precedence. """ - from esphome.components.socket import KEY_SOCKET_CONSUMERS + from esphome.components.socket import get_socket_counts # Check if user manually specified CONFIG_LWIP_MAX_SOCKETS user_max_sockets = conf[CONF_SDKCONFIG_OPTIONS].get("CONFIG_LWIP_MAX_SOCKETS") - socket_consumers: dict[str, int] = CORE.data.get(KEY_SOCKET_CONSUMERS, {}) - total_sockets = sum(socket_consumers.values()) - - # Early return if no sockets registered and no user override - if total_sockets == 0 and user_max_sockets is None: - return - - components_list = ", ".join( - f"{name}={count}" for name, count in sorted(socket_consumers.items()) - ) + # CONFIG_LWIP_MAX_SOCKETS is a single VFS socket pool shared by all socket + # types (TCP clients, TCP listeners, and UDP). Include all three counts. + sc = get_socket_counts() + total_sockets = sc.tcp + sc.udp + sc.tcp_listen # User specified their own value - respect it but warn if insufficient if user_max_sockets is not None: @@ -948,22 +1394,23 @@ def _configure_lwip_max_sockets(conf: dict) -> None: user_max_sockets, ) - # Warn if user's value is less than what components need - if total_sockets > 0: - user_sockets_int = 0 - with contextlib.suppress(ValueError, TypeError): - user_sockets_int = int(user_max_sockets) + user_sockets_int = 0 + with contextlib.suppress(ValueError, TypeError): + user_sockets_int = int(user_max_sockets) - if user_sockets_int < total_sockets: - _LOGGER.warning( - "CONFIG_LWIP_MAX_SOCKETS is set to %d but your configuration " - "needs %d sockets (registered: %s). You may experience socket " - "exhaustion errors. Consider increasing to at least %d.", - user_sockets_int, - total_sockets, - components_list, - total_sockets, - ) + if user_sockets_int < total_sockets: + _LOGGER.warning( + "CONFIG_LWIP_MAX_SOCKETS is set to %d but your configuration " + "needs %d sockets (%d TCP + %d UDP + %d TCP_LISTEN). You may " + "experience socket exhaustion errors. Consider increasing to " + "at least %d.", + user_sockets_int, + total_sockets, + sc.tcp, + sc.udp, + sc.tcp_listen, + total_sockets, + ) # User's value already added via sdkconfig_options processing return @@ -972,16 +1419,72 @@ def _configure_lwip_max_sockets(conf: dict) -> None: max_sockets = max(DEFAULT_MAX_SOCKETS, total_sockets) log_level = logging.INFO if max_sockets > DEFAULT_MAX_SOCKETS else logging.DEBUG + sock_min = " (min)" if max_sockets > total_sockets else "" _LOGGER.log( log_level, - "Setting CONFIG_LWIP_MAX_SOCKETS to %d (registered: %s)", + "Setting CONFIG_LWIP_MAX_SOCKETS to %d%s " + "(TCP=%d [%s], UDP=%d [%s], TCP_LISTEN=%d [%s])", max_sockets, - components_list, + sock_min, + sc.tcp, + sc.tcp_details, + sc.udp, + sc.udp_details, + sc.tcp_listen, + sc.tcp_listen_details, ) 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) +async def _write_arduino_libs_stub(stubs_dir: Path, idf_ver: cv.Version) -> None: + """Write stub package to skip downloading precompiled Arduino libs.""" + stubs_dir.mkdir(parents=True, exist_ok=True) + write_file_if_changed( + stubs_dir / "package.json", + f'{{"name":"{ARDUINO_LIBS_NAME}","version":"{idf_ver.major}.{idf_ver.minor}.{idf_ver.patch}"}}', + ) + write_file_if_changed( + stubs_dir / "tools.json", + '{"packages":[{"platforms":[{"toolsDependencies":[]}],"tools":[]}]}', + ) + + +@coroutine_with_priority(CoroPriority.FINAL) +async def _write_arduino_libraries_sdkconfig() -> None: + """Write Arduino selective compilation sdkconfig after all components have added libraries. + + This must run at FINAL priority so that all components have had a chance to call + cg.add_library() which auto-enables Arduino libraries via _enable_arduino_library(). + """ + if KEY_ESP32 not in CORE.data: + return + # Enable Arduino selective compilation to disable unused Arduino libraries + # ESPHome uses ESP-IDF APIs directly; we only need the Arduino core + # (HardwareSerial, Print, Stream, GPIO functions which are always compiled) + # cg.add_library() auto-enables needed libraries; users can also add + # libraries via esphome: libraries: config which calls cg.add_library() + add_idf_sdkconfig_option("CONFIG_ARDUINO_SELECTIVE_COMPILATION", True) + enabled_libs = CORE.data[KEY_ESP32].get(KEY_ARDUINO_LIBRARIES, set()) + for lib in ARDUINO_DISABLED_LIBRARIES: + # Enable if explicitly requested, disable otherwise + add_idf_sdkconfig_option(f"CONFIG_ARDUINO_SELECTIVE_{lib}", lib in enabled_libs) + + @coroutine_with_priority(CoroPriority.FINAL) async def _add_yaml_idf_components(components: list[ConfigType]): """Add IDF components from YAML config with final priority to override code-added components.""" @@ -1045,7 +1548,13 @@ async def to_code(config): cg.set_cpp_standard("gnu++20") cg.add_build_flag("-DUSE_ESP32") + cg.add_define("USE_NATIVE_64BIT_TIME") cg.add_build_flag("-Wl,-z,noexecstack") + # Arduino already wraps esp_panic_handler for its own backtrace handler, + # so only add our wrap when using ESP-IDF framework to avoid linker conflicts. + if conf[CONF_TYPE] == FRAMEWORK_ESP_IDF: + cg.add_build_flag("-Wl,--wrap=esp_panic_handler") + cg.add_define("USE_ESP32_CRASH_HANDLER") cg.add_define("ESPHOME_BOARD", config[CONF_BOARD]) variant = config[CONF_VARIANT] cg.add_build_flag(f"-DUSE_ESP32_VARIANT_{variant}") @@ -1078,6 +1587,14 @@ async def to_code(config): "_ZSt25__throw_bad_function_callv", ]: cg.add_build_flag(f"-Wl,--wrap={mangled}") + + # Wrap FILE*-based printf functions to eliminate newlib's _vfprintf_r + # (~11 KB). See printf_stubs.cpp for implementation. + if conf[CONF_ADVANCED][CONF_ENABLE_FULL_PRINTF]: + cg.add_define("USE_FULL_PRINTF") + else: + for symbol in ("vprintf", "printf", "fprintf", "vfprintf"): + cg.add_build_flag(f"-Wl,--wrap={symbol}") else: cg.add_build_flag("-DUSE_ARDUINO") cg.add_build_flag("-DUSE_ESP32_FRAMEWORK_ARDUINO") @@ -1089,8 +1606,14 @@ async def to_code(config): if (idf_ver := ARDUINO_IDF_VERSION_LOOKUP.get(framework_ver)) is not None: cg.add_platformio_option( "platform_packages", - [_format_framework_espidf_version(idf_ver, None)], + [_format_framework_espidf_version(idf_ver)], ) + # Use stub package to skip downloading precompiled libs + stubs_dir = CORE.relative_build_path("arduino_libs_stub") + cg.add_platformio_option( + "platform_packages", [f"{ARDUINO_LIBS_PKG}@file://{stubs_dir}"] + ) + CORE.add_job(_write_arduino_libs_stub, stubs_dir, idf_ver) # ESP32-S2 Arduino: Disable USB Serial on boot to avoid TinyUSB dependency if get_esp32_variant() == VARIANT_ESP32S2: @@ -1127,6 +1650,16 @@ async def to_code(config): f"CONFIG_ESPTOOLPY_FLASHSIZE_{config[CONF_FLASH_SIZE]}", True ) + # ESP32-P4: ESP-IDF 5.5.3 changed the default of ESP32P4_SELECTS_REV_LESS_V3 + # from y to n. PlatformIO uses sections.ld.in (for rev <3) or + # sections.rev3.ld.in (for rev >=3) based on board definition. + # Set the sdkconfig option to match the board's chip revision. + if variant == VARIANT_ESP32P4: + is_eng_sample = BOARDS.get(config[CONF_BOARD], {}).get( + "engineering_sample", False + ) + add_idf_sdkconfig_option("CONFIG_ESP32P4_SELECTS_REV_LESS_V3", is_eng_sample) + # Set minimum chip revision for ESP32 variant # Setting this to 3.0 or higher reduces flash size by excluding workaround code, # and for PSRAM users saves significant IRAM by keeping C library functions in ROM. @@ -1136,6 +1669,16 @@ async def to_code(config): for rev, flag in ESP32_CHIP_REVISIONS.items(): add_idf_sdkconfig_option(flag, rev == min_rev) cg.add_define("USE_ESP32_MIN_CHIP_REVISION_SET") + + # Use SRAM1 region as IRAM on ESP32 (original) variant + # This provides an additional 40KB of IRAM by using SRAM1 memory that was previously + # reserved for bootloader DRAM. Requires a bootloader from ESP-IDF v5.1 or later. + # WARNING: If the device has an old bootloader (pre-v5.1), the app will fail to boot. + # A USB flash will update the bootloader automatically. OTA updates do not. + # See: https://docs.espressif.com/projects/esp-idf/en/stable/esp32/api-guides/performance/ram-usage.html + if variant == VARIANT_ESP32 and conf[CONF_ADVANCED][CONF_SRAM1_AS_IRAM]: + add_idf_sdkconfig_option("CONFIG_ESP_SYSTEM_ESP32_SRAM1_REGION_AS_IRAM", True) + cg.add_define("USE_ESP32_SRAM1_AS_IRAM") add_idf_sdkconfig_option("CONFIG_PARTITION_TABLE_SINGLE_APP", False) add_idf_sdkconfig_option("CONFIG_PARTITION_TABLE_CUSTOM", True) add_idf_sdkconfig_option("CONFIG_PARTITION_TABLE_CUSTOM_FILENAME", "partitions.csv") @@ -1185,6 +1728,10 @@ async def to_code(config): # Disable dynamic log level control to save memory 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 add_idf_sdkconfig_option("CONFIG_ESP_PHY_REDUCE_TX_POWER", True) @@ -1195,6 +1742,11 @@ async def to_code(config): # Apply LWIP optimization settings 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 # WiFi component handles its own optimization when AP mode is not used # When using Arduino with Ethernet, DHCP server functions must be available @@ -1211,8 +1763,13 @@ async def to_code(config): _configure_lwip_max_sockets(conf) if advanced[CONF_EXECUTE_FROM_PSRAM]: - add_idf_sdkconfig_option("CONFIG_SPIRAM_FETCH_INSTRUCTIONS", True) - add_idf_sdkconfig_option("CONFIG_SPIRAM_RODATA", True) + if variant == VARIANT_ESP32S3: + add_idf_sdkconfig_option("CONFIG_SPIRAM_FETCH_INSTRUCTIONS", True) + add_idf_sdkconfig_option("CONFIG_SPIRAM_RODATA", True) + elif variant == VARIANT_ESP32P4: + add_idf_sdkconfig_option("CONFIG_SPIRAM_XIP_FROM_PSRAM", True) + else: + raise ValueError("Unhandled ESP32 variant") # Apply LWIP core locking for better socket performance # This is already enabled by default in Arduino framework, where it provides @@ -1233,11 +1790,18 @@ async def to_code(config): add_idf_sdkconfig_option("CONFIG_LIBC_LOCKS_PLACE_IN_IRAM", False) # 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). - add_idf_sdkconfig_option( - "CONFIG_VFS_SUPPORT_TERMIOS", not advanced[CONF_DISABLE_VFS_SUPPORT_TERMIOS] - ) + if CORE.data.get(KEY_VFS_TERMIOS_REQUIRED, False): + # 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 # ESPHome only uses select() with sockets via lwip_select(), which still works. @@ -1269,9 +1833,18 @@ async def to_code(config): if use_platformio: cg.add_platformio_option("board_build.partitions", "partitions.csv") if CONF_PARTITIONS in config: - add_extra_build_file( - "partitions.csv", CORE.relative_config_path(config[CONF_PARTITIONS]) - ) + if isinstance(config[CONF_PARTITIONS], list): + for partition in config[CONF_PARTITIONS]: + add_partition( + partition[CONF_NAME], + partition[CONF_TYPE], + partition[CONF_SUBTYPE], + partition[CONF_SIZE], + ) + else: + add_extra_build_file( + "partitions.csv", CORE.relative_config_path(config[CONF_PARTITIONS]) + ) if assertion_level := advanced.get(CONF_ASSERTION_LEVEL): for key, flag in ASSERTION_LEVELS.items(): @@ -1316,6 +1889,88 @@ async def to_code(config): 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 SHA-384 and SHA-512 in mbedTLS + # ESPHome doesn't use either algorithm. SHA-384 shares the same + # compression function as SHA-512 (mbedtls_internal_sha512_process), + # so both must be disabled to eliminate the ~3KB software fallback + # that IDF 6.0's PSA parallel engine always links in. + # On IDF < 6.0 these are a single config and hardware-only (no + # software fallback), so there was no code size cost to leaving + # them enabled. + # Components that need SHA-384/SHA-512 can call require_mbedtls_sha512() + if idf_version() >= cv.Version(6, 0, 0) and not CORE.data[KEY_ESP32].get( + KEY_MBEDTLS_SHA512_REQUIRED, False + ): + add_idf_sdkconfig_option("CONFIG_MBEDTLS_SHA384_C", False) + add_idf_sdkconfig_option("CONFIG_MBEDTLS_SHA512_C", False) + + # Disable PicolibC Newlib compatibility shim on IDF 6.0+ + # IDF 6.0 switched from Newlib to PicolibC. The shim provides thread-local + # stdin/stdout/stderr and getreent() for code compiled against Newlib. + # ESPHome doesn't link against Newlib-built libraries that use stdio. + # If a component needs it (e.g. precompiled Newlib binaries), re-enable via: + # esp32: + # framework: + # sdkconfig_options: + # CONFIG_LIBC_PICOLIBC_NEWLIB_COMPATIBILITY: "y" + if idf_version() >= cv.Version(6, 0, 0): + add_idf_sdkconfig_option("CONFIG_LIBC_PICOLIBC_NEWLIB_COMPATIBILITY", False) + + # 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(): add_idf_sdkconfig_option(name, RawSdkconfigValue(value)) @@ -1324,46 +1979,186 @@ async def to_code(config): if 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 = { - "2MB": 0x0C0000, # 768 KB - "4MB": 0x1C0000, # 1792 KB - "8MB": 0x3C0000, # 3840 KB - "16MB": 0x7C0000, # 7936 KB - "32MB": 0xFC0000, # 16128 KB + # Write Arduino selective compilation sdkconfig at FINAL priority after all + # components have had a chance to call cg.add_library() to enable libraries they need. + if conf[CONF_TYPE] == FRAMEWORK_ARDUINO: + CORE.add_job(_write_arduino_libraries_sdkconfig) + + +KEY_CUSTOM_PARTITIONS = "custom_partitions" + + +@dataclass +class PartitionEntry: + name: str + type: str + subtype: str + size: int + + +# Partition sizes (offsets auto-placed by gen_esp32part.py). +# These constants are the single source of truth — used in both +# the CSV generation and the overhead calculation. +BOOTLOADER_SIZE = 0x8000 +PARTITION_TABLE_SIZE = 0x1000 +FIRST_PARTITION_OFFSET = BOOTLOADER_SIZE + PARTITION_TABLE_SIZE +OTADATA_SIZE = 0x2000 +PHY_INIT_SIZE = 0x1000 +EEPROM_SIZE = 0x1000 # Arduino only +SPIFFS_SIZE = 0xF000 # Arduino only +ARDUINO_NVS_SIZE = 0x60000 +IDF_NVS_SIZE = 0x70000 + + +def _get_partition_overhead() -> int: + """Total non-app partition budget (system partitions + nvs + padding). + + Custom partitions are appended at the end and steal from app. + """ + # otadata + phy_init are followed by app0 which requires 64KB alignment, + # so pad up to the next 64KB boundary. + overhead = ( + FIRST_PARTITION_OFFSET + OTADATA_SIZE + PHY_INIT_SIZE + 0xFFFF + ) & ~0xFFFF + if CORE.using_arduino: + overhead += EEPROM_SIZE + SPIFFS_SIZE + ARDUINO_NVS_SIZE + else: + overhead += IDF_NVS_SIZE + return overhead + + +VALID_SUBTYPES: dict[str, set[str]] = { + "app": VALID_APP_SUBTYPES, + "data": VALID_DATA_SUBTYPES, } -def get_arduino_partition_csv(flash_size: str): - app_partition_size = APP_PARTITION_SIZES[flash_size] - eeprom_partition_size = 0x1000 # 4 KB - spiffs_partition_size = 0xF000 # 60 KB - - app0_partition_start = 0x010000 # 64 KB - app1_partition_start = app0_partition_start + app_partition_size - eeprom_partition_start = app1_partition_start + app_partition_size - spiffs_partition_start = eeprom_partition_start + eeprom_partition_size - - return f"""\ -nvs, data, nvs, 0x9000, 0x5000, -otadata, data, ota, 0xE000, 0x2000, -app0, app, ota_0, 0x{app0_partition_start:X}, 0x{app_partition_size:X}, -app1, app, ota_1, 0x{app1_partition_start:X}, 0x{app_partition_size:X}, -eeprom, data, 0x99, 0x{eeprom_partition_start:X}, 0x{eeprom_partition_size:X}, -spiffs, data, spiffs, 0x{spiffs_partition_start:X}, 0x{spiffs_partition_size:X} -""" +def _validate_partition( + name: str, p_type: str | int, subtype: str | int, size: int +) -> None: + """Validate partition parameters. Raises ValueError on invalid input.""" + if name in RESERVED_PARTITION_NAMES: + raise ValueError(f"Partition name '{name}' is reserved.") + if size % 0x1000 != 0: + raise ValueError("Partition size must be 4KB (0x1000) aligned.") + # Numeric or already-normalized hex types/subtypes skip string validation + if not isinstance(p_type, str) or p_type.startswith("0x"): + return + if p_type not in VALID_SUBTYPES: + raise ValueError( + f"Type '{p_type}' is invalid. Only 'app' and 'data' are allowed." + " Use numbers for custom types." + ) + if not isinstance(subtype, str) or subtype.startswith("0x"): + return + valid = VALID_SUBTYPES[p_type] + if subtype not in valid: + raise ValueError( + f"Subtype '{subtype}' is invalid for {p_type} type." + f" Only {', '.join(sorted(valid))} are allowed." + " Use numbers for custom subtypes." + ) -def get_idf_partition_csv(flash_size: str): - app_partition_size = APP_PARTITION_SIZES[flash_size] +def add_partition(name: str, p_type: str | int, subtype: str | int, size: int) -> None: + """Register a custom partition to be appended to the partition table. - return f"""\ -otadata, data, ota, , 0x2000, -phy_init, data, phy, , 0x1000, -app0, app, ota_0, , 0x{app_partition_size:X}, -app1, app, ota_1, , 0x{app_partition_size:X}, -nvs, data, nvs, , 0x6D000, -""" + Called from component to_code() to request additional flash partitions. + Size must be 4KB aligned. Integer types/subtypes are converted to hex strings. + """ + if name in CORE.data[KEY_ESP32].get(KEY_CUSTOM_PARTITIONS, {}): + raise ValueError(f"Partition name '{name}' is already defined.") + _validate_partition(name, p_type, subtype, size) + p_type_str = f"0x{p_type:X}" if isinstance(p_type, int) else p_type + subtype_str = f"0x{subtype:X}" if isinstance(subtype, int) else subtype + custom_partitions = CORE.data[KEY_ESP32].setdefault(KEY_CUSTOM_PARTITIONS, {}) + custom_partitions[name] = PartitionEntry( + name=name, type=p_type_str, subtype=subtype_str, size=size + ) + + +def _flash_size_to_bytes(flash_size_mb: str) -> int: + """Convert flash size string (e.g. '4MB') to bytes.""" + return int(flash_size_mb.removesuffix("MB")) * 1024 * 1024 + + +def _get_custom_partitions_total_size() -> int: + """Total size of custom partitions including alignment padding.""" + size = 0 + for partition in CORE.data[KEY_ESP32].get(KEY_CUSTOM_PARTITIONS, {}).values(): + if partition.type == "app": + size = (size + 0xFFFF) & ~0xFFFF # align to 64KB + size += partition.size + return size + + +def _get_app_partition_size(flash_size_mb: str) -> int: + flash_bytes = _flash_size_to_bytes(flash_size_mb) + custom_total = _get_custom_partitions_total_size() + # Align down to 64KB — app partitions require 64KB-aligned offsets, + # so the size must also be aligned to avoid unbudgeted padding. + raw_size = (flash_bytes - _get_partition_overhead() - custom_total) // 2 + app_size = raw_size & ~0xFFFF + wasted = (raw_size - app_size) * 2 + if wasted: + _LOGGER.info( + "Custom partitions cause %dKB of wasted flash due to 64KB app partition alignment.", + wasted // 1024, + ) + if app_size <= 0x10000: # 64 KB + raise ValueError( + "Custom partitions are too large to fit in the available flash size. " + "Reduce custom partition sizes." + ) + if app_size <= 0x80000: # 512 KB + _LOGGER.warning( + "App partition size is only %dKB. This may be too small for firmware with " + "many components. Consider reducing custom partition sizes or using a " + "larger flash chip.", + app_size // 1024, + ) + return app_size + + +def get_partition_csv(flash_size_mb: str) -> str: + app_size = _get_app_partition_size(flash_size_mb) + + partitions: list[PartitionEntry] = [ + PartitionEntry(name="otadata", type="data", subtype="ota", size=OTADATA_SIZE), + PartitionEntry(name="phy_init", type="data", subtype="phy", size=PHY_INIT_SIZE), + PartitionEntry(name="app0", type="app", subtype="ota_0", size=app_size), + PartitionEntry(name="app1", type="app", subtype="ota_1", size=app_size), + ] + if CORE.using_arduino: + partitions.append( + PartitionEntry(name="eeprom", type="data", subtype="0x99", size=EEPROM_SIZE) + ) + partitions.append( + PartitionEntry( + name="spiffs", type="data", subtype="spiffs", size=SPIFFS_SIZE + ) + ) + partitions.append( + PartitionEntry( + name="nvs", type="data", subtype="nvs", size=ARDUINO_NVS_SIZE + ) + ) + else: + partitions.append( + PartitionEntry(name="nvs", type="data", subtype="nvs", size=IDF_NVS_SIZE) + ) + partitions.extend(CORE.data[KEY_ESP32].get(KEY_CUSTOM_PARTITIONS, {}).values()) + + csv = "".join( + f"{p.name}, {p.type}, {p.subtype}, , 0x{p.size:X},\n" for p in partitions + ) + _LOGGER.debug("Partition table:\n%s", csv) + return csv def _format_sdkconfig_val(value: SdkconfigValueType) -> str: @@ -1404,11 +2199,49 @@ def _write_sdkconfig(): def _write_idf_component_yml(): yml_path = CORE.relative_build_path("src/idf_component.yml") + dependencies: dict[str, dict] = {} + + # For Arduino builds, override unused managed components from the Arduino framework + # by pointing them to empty stub directories using override_path + # This prevents the IDF component manager from downloading the real components + if CORE.using_arduino: + # Determine which IDF components are needed by enabled Arduino libraries + enabled_libs = CORE.data[KEY_ESP32].get(KEY_ARDUINO_LIBRARIES, set()) + required_idf_components = { + comp + for lib in enabled_libs + for comp in ARDUINO_LIBRARY_IDF_COMPONENTS.get(lib, ()) + } + + # Only stub components that are not required by any enabled Arduino library + components_to_stub = ( + set(ARDUINO_EXCLUDED_IDF_COMPONENTS) - required_idf_components + ) + + stubs_dir = CORE.relative_build_path("component_stubs") + stubs_dir.mkdir(exist_ok=True) + for component_name in components_to_stub: + # Create stub directory with minimal CMakeLists.txt + stub_path = stubs_dir / _idf_component_stub_name(component_name) + stub_path.mkdir(exist_ok=True) + stub_cmake = stub_path / "CMakeLists.txt" + if not stub_cmake.exists(): + stub_cmake.write_text("idf_component_register()\n") + dependencies[_idf_component_dep_name(component_name)] = { + "version": "*", + "override_path": str(stub_path), + } + + # Remove stubs for components that are now required by enabled libraries + for component_name in required_idf_components: + stub_path = stubs_dir / _idf_component_stub_name(component_name) + if stub_path.exists(): + rmtree(stub_path) + if CORE.data[KEY_ESP32][KEY_COMPONENTS]: components: dict = CORE.data[KEY_ESP32][KEY_COMPONENTS] - dependencies = {} for name, component in components.items(): - dependency = {} + dependency: dict[str, str] = {} if component[KEY_REF]: dependency["version"] = component[KEY_REF] if component[KEY_REPO]: @@ -1416,9 +2249,8 @@ def _write_idf_component_yml(): if component[KEY_PATH]: dependency["path"] = component[KEY_PATH] dependencies[name] = dependency - contents = yaml_util.dump({"dependencies": dependencies}) - else: - contents = "" + + contents = yaml_util.dump({"dependencies": dependencies}) if dependencies else "" if write_file_if_changed(yml_path, contents): dependencies_lock = CORE.relative_build_path("dependencies.lock") if dependencies_lock.is_file(): @@ -1433,16 +2265,10 @@ def copy_files(): if "partitions.csv" not in CORE.data[KEY_ESP32][KEY_EXTRA_BUILD_FILES]: flash_size = CORE.data[KEY_ESP32][KEY_FLASH_SIZE] - if CORE.using_arduino: - write_file_if_changed( - CORE.relative_build_path("partitions.csv"), - get_arduino_partition_csv(flash_size), - ) - else: - write_file_if_changed( - CORE.relative_build_path("partitions.csv"), - get_idf_partition_csv(flash_size), - ) + write_file_if_changed( + CORE.relative_build_path("partitions.csv"), + get_partition_csv(flash_size), + ) # IDF build scripts look for version string to put in the build. # However, if the build path does not have an initialized git repo, # and no version.txt file exists, the CMake script fails for some setups. diff --git a/esphome/components/esp32/boards.py b/esphome/components/esp32/boards.py index 8b066064f2..2bd08e7c39 100644 --- a/esphome/components/esp32/boards.py +++ b/esphome/components/esp32/boards.py @@ -20,7 +20,7 @@ STANDARD_BOARDS = { VARIANT_ESP32C6: "esp32-c6-devkitm-1", VARIANT_ESP32C61: "esp32-c61-devkitc1-n8r2", VARIANT_ESP32H2: "esp32-h2-devkitm-1", - VARIANT_ESP32P4: "esp32-p4-evboard", + VARIANT_ESP32P4: "esp32-p4_r3-evboard", VARIANT_ESP32S2: "esp32-s2-kaluga-1", VARIANT_ESP32S3: "esp32-s3-devkitc-1", } @@ -1686,6 +1686,10 @@ BOARDS = { "name": "Espressif ESP32-C6-DevKitM-1", "variant": VARIANT_ESP32C6, }, + "esp32-c61-devkitc1": { + "name": "Espressif ESP32-C61-DevKitC-1 (4 MB Flash)", + "variant": VARIANT_ESP32C61, + }, "esp32-c61-devkitc1-n8r2": { "name": "Espressif ESP32-C61-DevKitC-1 N8R2 (8 MB Flash Quad, 2 MB PSRAM Quad)", "variant": VARIANT_ESP32C61, @@ -1709,15 +1713,21 @@ BOARDS = { "esp32-p4": { "name": "Espressif ESP32-P4 ES (pre rev.300) generic", "variant": VARIANT_ESP32P4, + "engineering_sample": True, }, "esp32-p4-evboard": { "name": "Espressif ESP32-P4 Function EV Board (ES pre rev.300)", "variant": VARIANT_ESP32P4, + "engineering_sample": True, }, "esp32-p4_r3": { "name": "Espressif ESP32-P4 rev.300 generic", "variant": VARIANT_ESP32P4, }, + "esp32-p4_r3-evboard": { + "name": "Espressif ESP32-P4 Function EV Board v1.6 (rev.301)", + "variant": VARIANT_ESP32P4, + }, "esp32-pico-devkitm-2": { "name": "Espressif ESP32-PICO-DevKitM-2", "variant": VARIANT_ESP32, @@ -2133,6 +2143,7 @@ BOARDS = { "m5stack-tab5-p4": { "name": "M5STACK Tab5 esp32-p4 Board (ES pre rev.300)", "variant": VARIANT_ESP32P4, + "engineering_sample": True, }, "m5stack-timer-cam": { "name": "M5Stack Timer CAM", @@ -2554,6 +2565,10 @@ BOARDS = { "name": "XinaBox CW02", "variant": VARIANT_ESP32, }, + "yb_esp32s3_amp": { + "name": "YelloByte YB-ESP32-S3-AMP", + "variant": VARIANT_ESP32S3, + }, "yb_esp32s3_amp_v2": { "name": "YelloByte YB-ESP32-S3-AMP (Rev.2)", "variant": VARIANT_ESP32S3, @@ -2562,6 +2577,10 @@ BOARDS = { "name": "YelloByte YB-ESP32-S3-AMP (Rev.3)", "variant": VARIANT_ESP32S3, }, + "yb_esp32s3_dac": { + "name": "YelloByte YB-ESP32-S3-DAC", + "variant": VARIANT_ESP32S3, + }, "yb_esp32s3_drv": { "name": "YelloByte YB-ESP32-S3-DRV", "variant": VARIANT_ESP32S3, diff --git a/esphome/components/esp32/const.py b/esphome/components/esp32/const.py index 9f8165818b..d0d00723fc 100644 --- a/esphome/components/esp32/const.py +++ b/esphome/components/esp32/const.py @@ -6,6 +6,8 @@ KEY_FLASH_SIZE = "flash_size" KEY_VARIANT = "variant" KEY_SDKCONFIG_OPTIONS = "sdkconfig_options" KEY_COMPONENTS = "components" +KEY_EXCLUDE_COMPONENTS = "exclude_components" +KEY_ARDUINO_LIBRARIES = "arduino_libraries" KEY_REPO = "repo" KEY_REF = "ref" KEY_REFRESH = "refresh" @@ -13,6 +15,7 @@ KEY_PATH = "path" KEY_SUBMODULES = "submodules" KEY_EXTRA_BUILD_FILES = "extra_build_files" KEY_FULL_CERT_BUNDLE = "full_cert_bundle" +KEY_IDF_VERSION = "idf_version" VARIANT_ESP32 = "ESP32" VARIANT_ESP32C2 = "ESP32C2" diff --git a/esphome/components/esp32/core.cpp b/esphome/components/esp32/core.cpp index 09a45c14a6..313818e601 100644 --- a/esphome/components/esp32/core.cpp +++ b/esphome/components/esp32/core.cpp @@ -1,6 +1,8 @@ #ifdef USE_ESP32 #include "esphome/core/defines.h" +#include "crash_handler.h" +#include "esphome/core/application.h" #include "esphome/core/hal.h" #include "esphome/core/helpers.h" #include "preferences.h" @@ -14,16 +16,16 @@ #include void setup(); // NOLINT(readability-redundant-declaration) -void loop(); // NOLINT(readability-redundant-declaration) // Weak stub for initArduino - overridden when the Arduino component is present extern "C" __attribute__((weak)) void initArduino() {} namespace esphome { -void IRAM_ATTR HOT yield() { vPortYield(); } -uint32_t IRAM_ATTR HOT millis() { return (uint32_t) (esp_timer_get_time() / 1000ULL); } -void IRAM_ATTR HOT delay(uint32_t ms) { vTaskDelay(ms / portTICK_PERIOD_MS); } +void HOT yield() { vPortYield(); } +uint32_t IRAM_ATTR HOT millis() { return micros_to_millis(static_cast(esp_timer_get_time())); } +uint64_t HOT millis_64() { return micros_to_millis(static_cast(esp_timer_get_time())); } +void HOT delay(uint32_t ms) { vTaskDelay(ms / portTICK_PERIOD_MS); } uint32_t IRAM_ATTR HOT micros() { return (uint32_t) esp_timer_get_time(); } void IRAM_ATTR HOT delayMicroseconds(uint32_t us) { delay_microseconds_safe(us); } void arch_restart() { @@ -35,6 +37,11 @@ void arch_restart() { } void arch_init() { +#ifdef USE_ESP32_CRASH_HANDLER + // Read crash data from previous boot before anything else + esp32::crash_handler_read_and_clear(); +#endif + // Enable the task watchdog only on the loop task (from which we're currently running) esp_task_wdt_add(nullptr); @@ -44,9 +51,8 @@ void arch_init() { esp_ota_mark_app_valid_cancel_rollback(); #endif } -void IRAM_ATTR HOT arch_feed_wdt() { esp_task_wdt_reset(); } +void HOT arch_feed_wdt() { esp_task_wdt_reset(); } -uint8_t progmem_read_byte(const uint8_t *addr) { return *addr; } uint32_t arch_get_cpu_cycle_count() { return esp_cpu_get_cycle_count(); } uint32_t arch_get_cpu_freq_hz() { uint32_t freq = 0; @@ -59,7 +65,7 @@ TaskHandle_t loop_task_handle = nullptr; // NOLINT(cppcoreguidelines-avoid-non- void loop_task(void *pv_params) { setup(); while (true) { - loop(); + App.loop(); } } diff --git a/esphome/components/esp32/crash_handler.cpp b/esphome/components/esp32/crash_handler.cpp new file mode 100644 index 0000000000..ecf30d7878 --- /dev/null +++ b/esphome/components/esp32/crash_handler.cpp @@ -0,0 +1,355 @@ +#ifdef USE_ESP32 + +#include "esphome/core/defines.h" +#ifdef USE_ESP32_CRASH_HANDLER + +#include "crash_handler.h" +#include "esphome/core/log.h" + +#include +#include +#include +#include +#include + +#if CONFIG_IDF_TARGET_ARCH_XTENSA +#include +#include +#include +#elif CONFIG_IDF_TARGET_ARCH_RISCV +#include +#endif + +static constexpr uint32_t CRASH_MAGIC = 0xDEADBEEF; +static constexpr size_t MAX_BACKTRACE = 16; + +// Check if an address looks like code (flash-mapped or IRAM). +// Must be safe to call from panic context (no flash access needed). +static inline bool IRAM_ATTR is_code_addr(uint32_t addr) { + return (addr >= SOC_IROM_LOW && addr < SOC_IROM_HIGH) || (addr >= SOC_IRAM_LOW && addr < SOC_IRAM_HIGH); +} + +#if CONFIG_IDF_TARGET_ARCH_RISCV +// Check if a code address is a real return address by verifying the preceding +// instruction is a JAL or JALR with rd=ra (x1). Called at log time (not during +// panic) so flash cache is available and both IRAM and IROM are safely readable. +static inline bool is_return_addr(uint32_t addr) { + if (!is_code_addr(addr) || addr < 4) + return false; + // A return address on the stack points to the instruction after a call. + // Check for 4-byte JAL/JALR call instruction before this address. + // Use memcpy for alignment safety — RISC-V C extension means code addresses + // are only 2-byte aligned, so addr-4 may not be 4-byte aligned. + uint32_t inst; + memcpy(&inst, (const void *) (addr - 4), sizeof(inst)); + // RISC-V instruction encoding: bits [6:0] = opcode, bits [11:7] = rd + uint32_t opcode = inst & 0x7f; // Extract 7-bit opcode + uint32_t rd = inst & 0xf80; // Extract rd field (bits 11:7) + // Match JAL (0x6f) or JALR (0x67) with rd=ra (x1, encoded as 0x80 = 1<<7) + if ((opcode == 0x6f || opcode == 0x67) && rd == 0x80) + return true; + // Check for 2-byte compressed c.jalr before this address (C extension). + // c.jalr saves to ra implicitly: funct4=1001, rs1!=0, rs2=0, op=10 + if (addr >= 2) { + uint16_t c_inst = *(uint16_t *) (addr - 2); + if ((c_inst & 0xf07f) == 0x9002 && (c_inst & 0x0f80) != 0) + return true; + } + return false; +} +#endif + +// Raw crash data written by the panic handler wrapper. +// Lives in .noinit so it survives software reset but contains garbage after power cycle. +// Validated by magic marker. Static linkage since it's only used within this file. +// Version field is first so future firmware can always identify the struct layout. +// Magic is second to validate the data. Remaining fields can change between versions. +// Version is uint32_t because it would be padded to 4 bytes anyway before the next +// uint32_t field, so we use the full width rather than wasting 3 bytes of padding. +static constexpr uint32_t CRASH_DATA_VERSION = 1; +struct RawCrashData { + uint32_t version; + uint32_t magic; + uint32_t pc; + uint8_t backtrace_count; + uint8_t reg_frame_count; // Number of entries from registers (not stack-scanned) + uint8_t exception; // panic_exception_t enum (FAULT/ABORT/IWDT/TWDT/DEBUG) + uint8_t pseudo_excause; // Whether cause is a pseudo exception (Xtensa SoC-level panic) + uint32_t backtrace[MAX_BACKTRACE]; + uint32_t cause; // Architecture-specific: exccause (Xtensa) or mcause (RISC-V) +}; +static RawCrashData __attribute__((section(".noinit"))) +s_raw_crash_data; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) + +// Whether crash data was found and validated this boot. +static bool s_crash_data_valid = false; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) + +namespace esphome::esp32 { + +static const char *const TAG = "esp32.crash"; + +void crash_handler_read_and_clear() { + if (s_raw_crash_data.magic == CRASH_MAGIC && s_raw_crash_data.version == CRASH_DATA_VERSION) { + s_crash_data_valid = true; + // Clamp counts to prevent out-of-bounds reads from corrupt .noinit data + if (s_raw_crash_data.backtrace_count > MAX_BACKTRACE) + s_raw_crash_data.backtrace_count = MAX_BACKTRACE; + if (s_raw_crash_data.reg_frame_count > s_raw_crash_data.backtrace_count) + s_raw_crash_data.reg_frame_count = s_raw_crash_data.backtrace_count; + if (s_raw_crash_data.exception > 4) // panic_exception_t max value + s_raw_crash_data.exception = 4; // Default to PANIC_EXCEPTION_FAULT + if (s_raw_crash_data.pseudo_excause > 1) + s_raw_crash_data.pseudo_excause = 0; + } + // Clear magic regardless so we don't re-report on next normal reboot + s_raw_crash_data.magic = 0; +} + +bool crash_handler_has_data() { return s_crash_data_valid; } + +// Look up the exception cause as a human-readable string. +// Tables mirror ESP-IDF's panic_arch_fill_info() which uses local static arrays +// not exposed via any public API. +static const char *get_exception_reason() { +#if CONFIG_IDF_TARGET_ARCH_XTENSA + if (s_raw_crash_data.pseudo_excause) { + // SoC-level panic: watchdog, cache error, etc. + // Keep in sync with ESP-IDF's PANIC_RSN_* defines + static const char *const PSEUDO_REASON[] = { + "Unknown reason", // 0 + "Unhandled debug exception", // 1 + "Double exception", // 2 + "Unhandled kernel exception", // 3 + "Coprocessor exception", // 4 + "Interrupt wdt timeout on CPU0", // 5 + "Interrupt wdt timeout on CPU1", // 6 + "Cache error", // 7 + }; + uint32_t cause = s_raw_crash_data.cause; + if (cause < sizeof(PSEUDO_REASON) / sizeof(PSEUDO_REASON[0])) + return PSEUDO_REASON[cause]; + return PSEUDO_REASON[0]; + } + // Real Xtensa exception + static const char *const REASON[] = { + "IllegalInstruction", + "Syscall", + "InstructionFetchError", + "LoadStoreError", + "Level1Interrupt", + "Alloca", + "IntegerDivideByZero", + "PCValue", + "Privileged", + "LoadStoreAlignment", + nullptr, + nullptr, + "InstrPDAddrError", + "LoadStorePIFDataError", + "InstrPIFAddrError", + "LoadStorePIFAddrError", + "InstTLBMiss", + "InstTLBMultiHit", + "InstFetchPrivilege", + nullptr, + "InstrFetchProhibited", + nullptr, + nullptr, + nullptr, + "LoadStoreTLBMiss", + "LoadStoreTLBMultihit", + "LoadStorePrivilege", + nullptr, + "LoadProhibited", + "StoreProhibited", + }; + uint32_t cause = s_raw_crash_data.cause; + if (cause < sizeof(REASON) / sizeof(REASON[0]) && REASON[cause] != nullptr) + return REASON[cause]; +#elif CONFIG_IDF_TARGET_ARCH_RISCV + // For SoC-level panics (watchdog, cache error), mcause holds IDF-internal + // interrupt numbers, not standard RISC-V cause codes. The exception type + // field already identifies these, so just return null to use the type name. + if (s_raw_crash_data.pseudo_excause) + return nullptr; + static const char *const REASON[] = { + "Instruction address misaligned", + "Instruction access fault", + "Illegal instruction", + "Breakpoint", + "Load address misaligned", + "Load access fault", + "Store address misaligned", + "Store access fault", + "Environment call from U-mode", + "Environment call from S-mode", + nullptr, + "Environment call from M-mode", + "Instruction page fault", + "Load page fault", + nullptr, + "Store page fault", + }; + uint32_t cause = s_raw_crash_data.cause; + if (cause < sizeof(REASON) / sizeof(REASON[0]) && REASON[cause] != nullptr) + return REASON[cause]; +#endif + return "Unknown"; +} + +// Exception type names matching panic_exception_t enum +static const char *get_exception_type() { + static const char *const TYPES[] = { + "Debug exception", // PANIC_EXCEPTION_DEBUG + "Interrupt wdt", // PANIC_EXCEPTION_IWDT + "Task wdt", // PANIC_EXCEPTION_TWDT + "Abort", // PANIC_EXCEPTION_ABORT + "Fault", // PANIC_EXCEPTION_FAULT + }; + uint8_t exc = s_raw_crash_data.exception; + if (exc < sizeof(TYPES) / sizeof(TYPES[0])) + return TYPES[exc]; + return "Unknown"; +} + +// Intentionally uses separate ESP_LOGE calls per line instead of combining into +// one multi-line log message. This ensures each address appears as its own line +// on the serial console, making it possible to see partial output if the device +// crashes again during boot, and allowing the CLI's process_stacktrace to match +// and decode each address individually. +void crash_handler_log() { + if (!s_crash_data_valid) + return; + + ESP_LOGE(TAG, "*** CRASH DETECTED ON PREVIOUS BOOT ***"); + const char *reason = get_exception_reason(); + if (reason != nullptr) { + ESP_LOGE(TAG, " Reason: %s - %s", get_exception_type(), reason); + } else { + ESP_LOGE(TAG, " Reason: %s", get_exception_type()); + } + ESP_LOGE(TAG, " PC: 0x%08" PRIX32 " (fault location)", s_raw_crash_data.pc); + uint8_t bt_num = 0; + for (uint8_t i = 0; i < s_raw_crash_data.backtrace_count; i++) { + uint32_t addr = s_raw_crash_data.backtrace[i]; +#if CONFIG_IDF_TARGET_ARCH_RISCV + // Register-sourced entries (MEPC/RA) are trusted; only filter stack-scanned ones. + if (i >= s_raw_crash_data.reg_frame_count && !is_return_addr(addr)) + continue; +#endif +#if CONFIG_IDF_TARGET_ARCH_RISCV + const char *source = (i < s_raw_crash_data.reg_frame_count) ? "backtrace" : "stack scan"; +#else + const char *source = "backtrace"; +#endif + ESP_LOGE(TAG, " BT%d: 0x%08" PRIX32 " (%s)", bt_num++, addr, source); + } + // Build addr2line hint with all captured addresses for easy copy-paste + char hint[256]; + int pos = snprintf(hint, sizeof(hint), "Use: addr2line -pfiaC -e firmware.elf 0x%08" PRIX32, s_raw_crash_data.pc); + for (uint8_t i = 0; i < s_raw_crash_data.backtrace_count && pos < (int) sizeof(hint) - 12; i++) { + uint32_t addr = s_raw_crash_data.backtrace[i]; +#if CONFIG_IDF_TARGET_ARCH_RISCV + if (i >= s_raw_crash_data.reg_frame_count && !is_return_addr(addr)) + continue; +#endif + pos += snprintf(hint + pos, sizeof(hint) - pos, " 0x%08" PRIX32, addr); + } + ESP_LOGE(TAG, "%s", hint); +} + +} // namespace esphome::esp32 + +// --- Panic handler wrapper --- +// Intercepts esp_panic_handler() via --wrap linker flag to capture crash data +// into NOINIT memory before the normal panic handler runs. +// +extern "C" { +// NOLINTBEGIN(bugprone-reserved-identifier,cert-dcl37-c,cert-dcl51-cpp,readability-identifier-naming) +// Names are mandated by the --wrap linker mechanism +extern void __real_esp_panic_handler(panic_info_t *info); + +void IRAM_ATTR __wrap_esp_panic_handler(panic_info_t *info) { + // Save the faulting PC and exception info + s_raw_crash_data.pc = (uint32_t) info->addr; + s_raw_crash_data.backtrace_count = 0; + s_raw_crash_data.reg_frame_count = 0; + s_raw_crash_data.exception = (uint8_t) info->exception; + s_raw_crash_data.pseudo_excause = info->pseudo_excause ? 1 : 0; + +#if CONFIG_IDF_TARGET_ARCH_XTENSA + // Xtensa: walk the backtrace using the public API + if (info->frame != nullptr) { + auto *xt_frame = (XtExcFrame *) info->frame; + s_raw_crash_data.cause = xt_frame->exccause; + esp_backtrace_frame_t bt_frame = { + .pc = (uint32_t) xt_frame->pc, + .sp = (uint32_t) xt_frame->a1, + .next_pc = (uint32_t) xt_frame->a0, + .exc_frame = xt_frame, + }; + + uint8_t count = 0; + // First frame PC + uint32_t first_pc = esp_cpu_process_stack_pc(bt_frame.pc); + if (is_code_addr(first_pc)) { + s_raw_crash_data.backtrace[count++] = first_pc; + } + // Walk remaining frames + while (count < MAX_BACKTRACE && bt_frame.next_pc != 0) { + if (!esp_backtrace_get_next_frame(&bt_frame)) { + break; + } + uint32_t pc = esp_cpu_process_stack_pc(bt_frame.pc); + if (is_code_addr(pc)) { + s_raw_crash_data.backtrace[count++] = pc; + } + } + s_raw_crash_data.backtrace_count = count; + } + +#elif CONFIG_IDF_TARGET_ARCH_RISCV + // RISC-V: capture MEPC + RA, then scan stack for code addresses + if (info->frame != nullptr) { + auto *rv_frame = (RvExcFrame *) info->frame; + s_raw_crash_data.cause = rv_frame->mcause; + uint8_t count = 0; + + // Save MEPC (fault PC) and RA (return address) + if (is_code_addr(rv_frame->mepc)) { + s_raw_crash_data.backtrace[count++] = rv_frame->mepc; + } + if (is_code_addr(rv_frame->ra) && rv_frame->ra != rv_frame->mepc) { + s_raw_crash_data.backtrace[count++] = rv_frame->ra; + } + + // Track how many entries came from registers (MEPC/RA) so we can + // skip return-address validation for them at log time. + s_raw_crash_data.reg_frame_count = count; + + // Scan stack for code addresses — captures broadly during panic, + // filtered by is_return_addr() at log time when flash is accessible. + auto *scan_start = (uint32_t *) rv_frame->sp; + for (uint32_t i = 0; i < 64 && count < MAX_BACKTRACE; i++) { + uint32_t val = scan_start[i]; + if (is_code_addr(val) && val != rv_frame->mepc && val != rv_frame->ra) { + s_raw_crash_data.backtrace[count++] = val; + } + } + s_raw_crash_data.backtrace_count = count; + } +#endif + + // Write version and magic last — ensures all data is written before we mark it valid + s_raw_crash_data.version = CRASH_DATA_VERSION; + s_raw_crash_data.magic = CRASH_MAGIC; + + // Call the real panic handler (prints to UART, does core dump, reboots, etc.) + __real_esp_panic_handler(info); +} + +// NOLINTEND(bugprone-reserved-identifier,cert-dcl37-c,cert-dcl51-cpp,readability-identifier-naming) +} // extern "C" + +#endif // USE_ESP32_CRASH_HANDLER +#endif // USE_ESP32 diff --git a/esphome/components/esp32/crash_handler.h b/esphome/components/esp32/crash_handler.h new file mode 100644 index 0000000000..97a4d4e116 --- /dev/null +++ b/esphome/components/esp32/crash_handler.h @@ -0,0 +1,18 @@ +#pragma once + +#ifdef USE_ESP32_CRASH_HANDLER + +namespace esphome::esp32 { + +/// Read crash data from NOINIT memory and clear the magic marker. +void crash_handler_read_and_clear(); + +/// Log crash data if a crash was detected on previous boot. +void crash_handler_log(); + +/// Returns true if crash data was found this boot. +bool crash_handler_has_data(); + +} // namespace esphome::esp32 + +#endif // USE_ESP32_CRASH_HANDLER diff --git a/esphome/components/esp32/gpio.py b/esphome/components/esp32/gpio.py index c0803f40a8..a7180cbcd7 100644 --- a/esphome/components/esp32/gpio.py +++ b/esphome/components/esp32/gpio.py @@ -88,8 +88,8 @@ def _translate_pin(value): @dataclass class ESP32ValidationFunctions: - pin_validation: Callable[[Any], Any] - usage_validation: Callable[[Any], Any] + pin_validation: Callable[[int], int] + usage_validation: Callable[[dict[str, Any]], dict[str, Any]] _esp32_validations = { diff --git a/esphome/components/esp32/gpio_esp32.py b/esphome/components/esp32/gpio_esp32.py index 973d2dc0ef..fec257f90b 100644 --- a/esphome/components/esp32/gpio_esp32.py +++ b/esphome/components/esp32/gpio_esp32.py @@ -1,4 +1,5 @@ import logging +from typing import Any import esphome.config_validation as cv from esphome.const import ( @@ -22,12 +23,16 @@ _ESP32_STRAPPING_PINS = {0, 2, 5, 12, 15} _LOGGER = logging.getLogger(__name__) -def esp32_validate_gpio_pin(value): +def esp32_validate_gpio_pin(value: int) -> int: if value < 0 or value > 39: raise cv.Invalid(f"Invalid pin number: {value} (must be 0-39)") if value in _ESP_SDIO_PINS: raise cv.Invalid( - f"This pin cannot be used on ESP32s and is already used by the flash interface (function: {_ESP_SDIO_PINS[value]})" + f"This pin cannot be used on ESP32s and is already used by the flash interface" + f" (function: {_ESP_SDIO_PINS[value]})." + f" If you are using an ESP32 module that uses a different flash pin" + f" configuration (e.g. ESP32-PICO-V3-02), you can set" + f" 'ignore_pin_validation_error: true' to bypass this check." ) if 9 <= value <= 10: _LOGGER.warning( @@ -41,7 +46,7 @@ def esp32_validate_gpio_pin(value): return value -def esp32_validate_supports(value): +def esp32_validate_supports(value: dict[str, Any]) -> dict[str, Any]: num = value[CONF_NUMBER] mode = value[CONF_MODE] is_input = mode[CONF_INPUT] diff --git a/esphome/components/esp32/gpio_esp32_c2.py b/esphome/components/esp32/gpio_esp32_c2.py index 32a24050ca..2d6e3a4a4e 100644 --- a/esphome/components/esp32/gpio_esp32_c2.py +++ b/esphome/components/esp32/gpio_esp32_c2.py @@ -1,4 +1,5 @@ import logging +from typing import Any import esphome.config_validation as cv from esphome.const import CONF_INPUT, CONF_MODE, CONF_NUMBER @@ -9,14 +10,14 @@ _ESP32C2_STRAPPING_PINS = {8, 9} _LOGGER = logging.getLogger(__name__) -def esp32_c2_validate_gpio_pin(value): +def esp32_c2_validate_gpio_pin(value: int) -> int: if value < 0 or value > 20: raise cv.Invalid(f"Invalid pin number: {value} (must be 0-20)") return value -def esp32_c2_validate_supports(value): +def esp32_c2_validate_supports(value: dict[str, Any]) -> dict[str, Any]: num = value[CONF_NUMBER] mode = value[CONF_MODE] is_input = mode[CONF_INPUT] diff --git a/esphome/components/esp32/gpio_esp32_c3.py b/esphome/components/esp32/gpio_esp32_c3.py index c1427cc02a..6eb002f3f0 100644 --- a/esphome/components/esp32/gpio_esp32_c3.py +++ b/esphome/components/esp32/gpio_esp32_c3.py @@ -1,4 +1,5 @@ import logging +from typing import Any import esphome.config_validation as cv from esphome.const import CONF_INPUT, CONF_MODE, CONF_NUMBER @@ -13,23 +14,31 @@ _ESP32C3_SPI_PSRAM_PINS = { 17: "SPIQ", } +_ESP32C3_USB_JTAG_PINS = {18, 19} + _ESP32C3_STRAPPING_PINS = {2, 8, 9} _LOGGER = logging.getLogger(__name__) -def esp32_c3_validate_gpio_pin(value): +def esp32_c3_validate_gpio_pin(value: int) -> int: if value < 0 or value > 21: raise cv.Invalid(f"Invalid pin number: {value} (must be 0-21)") if value in _ESP32C3_SPI_PSRAM_PINS: raise cv.Invalid( f"This pin cannot be used on ESP32-C3s and is already used by the SPI/PSRAM interface (function: {_ESP32C3_SPI_PSRAM_PINS[value]})" ) + if value in _ESP32C3_USB_JTAG_PINS: + _LOGGER.warning( + "GPIO%d is used by the USB-Serial-JTAG interface." + " Using this pin as GPIO will conflict with USB-Serial-JTAG.", + value, + ) return value -def esp32_c3_validate_supports(value): +def esp32_c3_validate_supports(value: dict[str, Any]) -> dict[str, Any]: num = value[CONF_NUMBER] mode = value[CONF_MODE] is_input = mode[CONF_INPUT] diff --git a/esphome/components/esp32/gpio_esp32_c5.py b/esphome/components/esp32/gpio_esp32_c5.py index fa2ce1a689..639ed64c9e 100644 --- a/esphome/components/esp32/gpio_esp32_c5.py +++ b/esphome/components/esp32/gpio_esp32_c5.py @@ -1,4 +1,5 @@ import logging +from typing import Any import esphome.config_validation as cv from esphome.const import CONF_INPUT, CONF_MODE, CONF_NUMBER, CONF_SCL, CONF_SDA @@ -22,7 +23,7 @@ _ESP32C5_STRAPPING_PINS = {2, 7, 27, 28} _LOGGER = logging.getLogger(__name__) -def esp32_c5_validate_gpio_pin(value): +def esp32_c5_validate_gpio_pin(value: int) -> int: if value < 0 or value > 28: raise cv.Invalid(f"Invalid pin number: {value} (must be 0-28)") if value in _ESP32C5_SPI_PSRAM_PINS: @@ -33,7 +34,7 @@ def esp32_c5_validate_gpio_pin(value): return value -def esp32_c5_validate_supports(value): +def esp32_c5_validate_supports(value: dict[str, Any]) -> dict[str, Any]: num = value[CONF_NUMBER] mode = value[CONF_MODE] is_input = mode[CONF_INPUT] diff --git a/esphome/components/esp32/gpio_esp32_c6.py b/esphome/components/esp32/gpio_esp32_c6.py index 5d679dede2..993606d9de 100644 --- a/esphome/components/esp32/gpio_esp32_c6.py +++ b/esphome/components/esp32/gpio_esp32_c6.py @@ -1,4 +1,5 @@ import logging +from typing import Any import esphome.config_validation as cv from esphome.const import CONF_INPUT, CONF_MODE, CONF_NUMBER, CONF_SCL, CONF_SDA @@ -17,23 +18,31 @@ _ESP32C6_SPI_PSRAM_PINS = { 30: "SPID", } -_ESP32C6_STRAPPING_PINS = {8, 9, 15} +_ESP32C6_USB_JTAG_PINS = {12, 13} + +_ESP32C6_STRAPPING_PINS = {4, 5, 8, 9, 15} _LOGGER = logging.getLogger(__name__) -def esp32_c6_validate_gpio_pin(value): +def esp32_c6_validate_gpio_pin(value: int) -> int: if value < 0 or value > 23: raise cv.Invalid(f"Invalid pin number: {value} (must be 0-23)") if value in _ESP32C6_SPI_PSRAM_PINS: raise cv.Invalid( f"This pin cannot be used on ESP32-C6s and is already used by the SPI/PSRAM interface (function: {_ESP32C6_SPI_PSRAM_PINS[value]})" ) + if value in _ESP32C6_USB_JTAG_PINS: + _LOGGER.warning( + "GPIO%d is used by the USB-Serial-JTAG interface." + " Using this pin as GPIO will conflict with USB-Serial-JTAG.", + value, + ) return value -def esp32_c6_validate_supports(value): +def esp32_c6_validate_supports(value: dict[str, Any]) -> dict[str, Any]: num = value[CONF_NUMBER] mode = value[CONF_MODE] is_input = mode[CONF_INPUT] diff --git a/esphome/components/esp32/gpio_esp32_c61.py b/esphome/components/esp32/gpio_esp32_c61.py index 77be42db3e..2f3abe6a0f 100644 --- a/esphome/components/esp32/gpio_esp32_c61.py +++ b/esphome/components/esp32/gpio_esp32_c61.py @@ -1,4 +1,5 @@ import logging +from typing import Any import esphome.config_validation as cv from esphome.const import CONF_INPUT, CONF_MODE, CONF_NUMBER @@ -20,7 +21,7 @@ _ESP32C61_STRAPPING_PINS = {8, 9} _LOGGER = logging.getLogger(__name__) -def esp32_c61_validate_gpio_pin(value): +def esp32_c61_validate_gpio_pin(value: int) -> int: if value < 0 or value > 29: raise cv.Invalid(f"Invalid pin number: {value} (must be 0-29)") if value in _ESP32C61_SPI_PSRAM_PINS: @@ -31,7 +32,7 @@ def esp32_c61_validate_gpio_pin(value): return value -def esp32_c61_validate_supports(value): +def esp32_c61_validate_supports(value: dict[str, Any]) -> dict[str, Any]: num = value[CONF_NUMBER] mode = value[CONF_MODE] is_input = mode[CONF_INPUT] diff --git a/esphome/components/esp32/gpio_esp32_h2.py b/esphome/components/esp32/gpio_esp32_h2.py index f37297764b..9dd6537694 100644 --- a/esphome/components/esp32/gpio_esp32_h2.py +++ b/esphome/components/esp32/gpio_esp32_h2.py @@ -1,4 +1,5 @@ import logging +from typing import Any import esphome.config_validation as cv from esphome.const import CONF_INPUT, CONF_MODE, CONF_NUMBER @@ -8,12 +9,12 @@ _ESP32H2_SPI_FLASH_PINS = {6, 7, 15, 16, 17, 18, 19, 20, 21} _ESP32H2_USB_JTAG_PINS = {26, 27} -_ESP32H2_STRAPPING_PINS = {2, 3, 8, 9, 25} +_ESP32H2_STRAPPING_PINS = {8, 9, 25} _LOGGER = logging.getLogger(__name__) -def esp32_h2_validate_gpio_pin(value): +def esp32_h2_validate_gpio_pin(value: int) -> int: if value < 0 or value > 27: raise cv.Invalid(f"Invalid pin number: {value} (must be 0-27)") if value in _ESP32H2_SPI_FLASH_PINS: @@ -25,15 +26,15 @@ def esp32_h2_validate_gpio_pin(value): ) if value in _ESP32H2_USB_JTAG_PINS: _LOGGER.warning( - "GPIO%d is reserved for the USB-Serial-JTAG interface.\n" - "To use this pin as GPIO, USB-Serial-JTAG will be disabled.", + "GPIO%d is used by the USB-Serial-JTAG interface." + " Using this pin as GPIO will conflict with USB-Serial-JTAG.", value, ) return value -def esp32_h2_validate_supports(value): +def esp32_h2_validate_supports(value: dict[str, Any]) -> dict[str, Any]: num = value[CONF_NUMBER] mode = value[CONF_MODE] is_input = mode[CONF_INPUT] diff --git a/esphome/components/esp32/gpio_esp32_p4.py b/esphome/components/esp32/gpio_esp32_p4.py index b98b567da2..6e9227c501 100644 --- a/esphome/components/esp32/gpio_esp32_p4.py +++ b/esphome/components/esp32/gpio_esp32_p4.py @@ -1,4 +1,5 @@ import logging +from typing import Any import esphome.config_validation as cv from esphome.const import CONF_INPUT, CONF_MODE, CONF_NUMBER, CONF_SCL, CONF_SDA @@ -14,26 +15,26 @@ _ESP32P4_STRAPPING_PINS = {34, 35, 36, 37, 38} _LOGGER = logging.getLogger(__name__) -def esp32_p4_validate_gpio_pin(value): +def esp32_p4_validate_gpio_pin(value: int) -> int: if value < 0 or value > 54: raise cv.Invalid(f"Invalid pin number: {value} (must be 0-54)") if value in _ESP32P4_USB_JTAG_PINS: _LOGGER.warning( - "GPIO%d is reserved for the USB-Serial-JTAG interface.\n" - "To use this pin as GPIO, USB-Serial-JTAG will be disabled.", + "GPIO%d is used by the USB-Serial-JTAG interface." + " Using this pin as GPIO will conflict with USB-Serial-JTAG.", value, ) return value -def esp32_p4_validate_supports(value): +def esp32_p4_validate_supports(value: dict[str, Any]) -> dict[str, Any]: num = value[CONF_NUMBER] mode = value[CONF_MODE] is_input = mode[CONF_INPUT] if num < 0 or num > 54: - raise cv.Invalid(f"Invalid pin number: {value} (must be 0-54)") + raise cv.Invalid(f"Invalid pin number: {num} (must be 0-54)") if is_input: # All ESP32 pins support input mode pass diff --git a/esphome/components/esp32/gpio_esp32_s2.py b/esphome/components/esp32/gpio_esp32_s2.py index 331aeb9d94..4978f48a1c 100644 --- a/esphome/components/esp32/gpio_esp32_s2.py +++ b/esphome/components/esp32/gpio_esp32_s2.py @@ -1,4 +1,5 @@ import logging +from typing import Any import esphome.config_validation as cv from esphome.const import ( @@ -26,7 +27,7 @@ _ESP32S2_STRAPPING_PINS = {0, 45, 46} _LOGGER = logging.getLogger(__name__) -def esp32_s2_validate_gpio_pin(value): +def esp32_s2_validate_gpio_pin(value: int) -> int: if value < 0 or value > 46: raise cv.Invalid(f"Invalid pin number: {value} (must be 0-46)") @@ -43,7 +44,7 @@ def esp32_s2_validate_gpio_pin(value): return value -def esp32_s2_validate_supports(value): +def esp32_s2_validate_supports(value: dict[str, Any]) -> dict[str, Any]: num = value[CONF_NUMBER] mode = value[CONF_MODE] is_input = mode[CONF_INPUT] diff --git a/esphome/components/esp32/gpio_esp32_s3.py b/esphome/components/esp32/gpio_esp32_s3.py index 7120504693..f528de4ccd 100644 --- a/esphome/components/esp32/gpio_esp32_s3.py +++ b/esphome/components/esp32/gpio_esp32_s3.py @@ -1,10 +1,11 @@ import logging +from typing import Any import esphome.config_validation as cv from esphome.const import CONF_INPUT, CONF_MODE, CONF_NUMBER from esphome.pins import check_strapping_pin -_ESP_32S3_SPI_PSRAM_PINS = { +_ESP32S3_SPI_PSRAM_PINS = { 26: "SPICS1", 27: "SPIHD", 28: "SPIWP", @@ -14,7 +15,7 @@ _ESP_32S3_SPI_PSRAM_PINS = { 32: "SPID", } -_ESP_32_ESP32_S3R8_PSRAM_PINS = { +_ESP32S3R8_PSRAM_PINS = { 33: "SPIIO4", 34: "SPIIO5", 35: "SPIIO6", @@ -22,20 +23,22 @@ _ESP_32_ESP32_S3R8_PSRAM_PINS = { 37: "SPIDQS", } -_ESP_32S3_STRAPPING_PINS = {0, 3, 45, 46} +_ESP32S3_USB_JTAG_PINS = {19, 20} + +_ESP32S3_STRAPPING_PINS = {0, 3, 45, 46} _LOGGER = logging.getLogger(__name__) -def esp32_s3_validate_gpio_pin(value): +def esp32_s3_validate_gpio_pin(value: int) -> int: if value < 0 or value > 48: - raise cv.Invalid(f"Invalid pin number: {value} (must be 0-46)") + raise cv.Invalid(f"Invalid pin number: {value} (must be 0-48)") - if value in _ESP_32S3_SPI_PSRAM_PINS: + if value in _ESP32S3_SPI_PSRAM_PINS: raise cv.Invalid( - f"This pin cannot be used on ESP32-S3s and is already used by the SPI/PSRAM interface(function: {_ESP_32S3_SPI_PSRAM_PINS[value]})" + f"This pin cannot be used on ESP32-S3s and is already used by the SPI/PSRAM interface(function: {_ESP32S3_SPI_PSRAM_PINS[value]})" ) - if value in _ESP_32_ESP32_S3R8_PSRAM_PINS: + if value in _ESP32S3R8_PSRAM_PINS: _LOGGER.warning( "GPIO%d is used by the PSRAM interface on ESP32-S3R8 / ESP32-S3R8V and should be avoided on these models", value, @@ -45,20 +48,26 @@ def esp32_s3_validate_gpio_pin(value): # These pins are not exposed in GPIO mux (reason unknown) # but they're missing from IO_MUX list in datasheet raise cv.Invalid(f"The pin GPIO{value} is not usable on ESP32-S3s.") + if value in _ESP32S3_USB_JTAG_PINS: + _LOGGER.warning( + "GPIO%d is used by the USB-Serial-JTAG interface." + " Using this pin as GPIO will conflict with USB-Serial-JTAG.", + value, + ) return value -def esp32_s3_validate_supports(value): +def esp32_s3_validate_supports(value: dict[str, Any]) -> dict[str, Any]: num = value[CONF_NUMBER] mode = value[CONF_MODE] is_input = mode[CONF_INPUT] if num < 0 or num > 48: - raise cv.Invalid(f"Invalid pin number: {num} (must be 0-46)") + raise cv.Invalid(f"Invalid pin number: {num} (must be 0-48)") if is_input: # All ESP32 pins support input mode pass - check_strapping_pin(value, _ESP_32S3_STRAPPING_PINS, _LOGGER) + check_strapping_pin(value, _ESP32S3_STRAPPING_PINS, _LOGGER) return value diff --git a/esphome/components/esp32/helpers.cpp b/esphome/components/esp32/helpers.cpp index 051b7ce162..afcec8bfc7 100644 --- a/esphome/components/esp32/helpers.cpp +++ b/esphome/components/esp32/helpers.cpp @@ -9,23 +9,19 @@ #include #include +#include "esphome/core/log.h" #include "esp_random.h" #include "esp_system.h" namespace esphome { -uint32_t random_uint32() { return esp_random(); } +static const char *const TAG = "esp32"; + bool random_bytes(uint8_t *data, size_t len) { esp_fill_random(data, len); return true; } -Mutex::Mutex() { handle_ = xSemaphoreCreateMutex(); } -Mutex::~Mutex() {} -void Mutex::lock() { xSemaphoreTake(this->handle_, portMAX_DELAY); } -bool Mutex::try_lock() { return xSemaphoreTake(this->handle_, 0) == pdTRUE; } -void Mutex::unlock() { xSemaphoreGive(this->handle_); } - // only affects the executing core // so should not be used as a mutex lock, only to get accurate timing IRAM_ATTR InterruptLock::InterruptLock() { portDISABLE_INTERRUPTS(); } @@ -70,22 +66,43 @@ LwIPLock::~LwIPLock() { #endif } +/// Read MAC and validate both the return code and content. +static bool read_valid_mac(uint8_t *mac, esp_err_t err) { return err == ESP_OK && mac_address_is_valid(mac); } + +static constexpr size_t MAC_ADDRESS_SIZE_BITS = MAC_ADDRESS_SIZE * 8; // 48 bits + void get_mac_address_raw(uint8_t *mac) { // NOLINT(readability-non-const-parameter) #if defined(CONFIG_SOC_IEEE802154_SUPPORTED) // When CONFIG_SOC_IEEE802154_SUPPORTED is defined, esp_efuse_mac_get_default // returns the 802.15.4 EUI-64 address, so we read directly from eFuse instead. - if (has_custom_mac_address()) { - esp_efuse_read_field_blob(ESP_EFUSE_MAC_CUSTOM, mac, 48); - } else { - esp_efuse_read_field_blob(ESP_EFUSE_MAC_FACTORY, mac, 48); + // Both paths already read raw eFuse bytes, so there is no CRC-bypass fallback + // (unlike the non-IEEE802154 path where esp_efuse_mac_get_default does CRC checks). + if (has_custom_mac_address() && + read_valid_mac(mac, esp_efuse_read_field_blob(ESP_EFUSE_MAC_CUSTOM, mac, MAC_ADDRESS_SIZE_BITS))) { + return; + } + if (read_valid_mac(mac, esp_efuse_read_field_blob(ESP_EFUSE_MAC_FACTORY, mac, MAC_ADDRESS_SIZE_BITS))) { + return; } #else - if (has_custom_mac_address()) { - esp_efuse_mac_get_custom(mac); - } else { - esp_efuse_mac_get_default(mac); + if (has_custom_mac_address() && read_valid_mac(mac, esp_efuse_mac_get_custom(mac))) { + return; + } + if (read_valid_mac(mac, esp_efuse_mac_get_default(mac))) { + return; + } + // Default MAC read failed (e.g., eFuse CRC error) - try reading raw eFuse bytes + // directly, bypassing CRC validation. A MAC that passes mac_address_is_valid() + // (non-zero, non-broadcast, unicast) is almost certainly the real factory MAC + // with a corrupted CRC byte, which is far better than returning garbage or zeros. + if (read_valid_mac(mac, esp_efuse_read_field_blob(ESP_EFUSE_MAC_FACTORY, mac, MAC_ADDRESS_SIZE_BITS))) { + ESP_LOGW(TAG, "eFuse MAC CRC failed but raw bytes appear valid - using raw eFuse MAC"); + return; } #endif + // All methods failed - zero the MAC rather than returning garbage + ESP_LOGE(TAG, "Failed to read a valid MAC address from eFuse"); + memset(mac, 0, MAC_ADDRESS_SIZE); } void set_mac_address(uint8_t *mac) { esp_base_mac_addr_set(mac); } @@ -95,9 +112,11 @@ bool has_custom_mac_address() { uint8_t mac[6]; // do not use 'esp_efuse_mac_get_custom(mac)' because it drops an error in the logs whenever it fails #ifndef USE_ESP32_VARIANT_ESP32 - return (esp_efuse_read_field_blob(ESP_EFUSE_USER_DATA_MAC_CUSTOM, mac, 48) == ESP_OK) && mac_address_is_valid(mac); + return (esp_efuse_read_field_blob(ESP_EFUSE_USER_DATA_MAC_CUSTOM, mac, MAC_ADDRESS_SIZE_BITS) == ESP_OK) && + mac_address_is_valid(mac); #else - return (esp_efuse_read_field_blob(ESP_EFUSE_MAC_CUSTOM, mac, 48) == ESP_OK) && mac_address_is_valid(mac); + return (esp_efuse_read_field_blob(ESP_EFUSE_MAC_CUSTOM, mac, MAC_ADDRESS_SIZE_BITS) == ESP_OK) && + mac_address_is_valid(mac); #endif #else return false; diff --git a/esphome/components/esp32/iram_fix.py.script b/esphome/components/esp32/iram_fix.py.script index 0d23f9a81b..b656d7d1a6 100644 --- a/esphome/components/esp32/iram_fix.py.script +++ b/esphome/components/esp32/iram_fix.py.script @@ -4,12 +4,40 @@ import re # pylint: disable=E0602 Import("env") # noqa -# IRAM size for testing mode (2MB - large enough to accommodate grouped tests) -TESTING_IRAM_SIZE = 0x200000 +# Memory sizes for testing mode (large enough to accommodate grouped tests) +TESTING_IRAM_SIZE = 0x200000 # 2MB +TESTING_DRAM_SIZE = 0x200000 # 2MB + + +def patch_segment(content, segment_name, new_size): + """Patch a memory segment's length in linker script content. + + Handles both single-line and multi-line segment definitions, e.g.: + iram0_0_seg (RX) : org = 0x40080000, len = 0x20000 + 0x0 + or split across lines: + dram0_0_seg (RW) : org = 0x3FFB0000 + 0xdb5c, + len = 0x2c200 - 0xdb5c + + Args: + content: Full linker script content as string + segment_name: Name of the segment (e.g., 'iram0_0_seg') + new_size: New size as integer + + Returns: + Tuple of (new_content, was_patched) + """ + # Match segment name through to "len = " allowing newlines between org and len + pattern = rf'({re.escape(segment_name)}\s*\([^)]*\)\s*:\s*org\s*=\s*.+?,\s*len\s*=\s*)(\S+[^\n]*)' + if match := re.search(pattern, content, re.DOTALL): + replacement = f"{match.group(1)}{new_size:#x}" + new_content = content[:match.start()] + replacement + content[match.end():] + if new_content != content: + return new_content, True + return content, False def patch_idf_linker_script(source, target, env): - """Patch ESP-IDF linker script to increase IRAM size for testing mode.""" + """Patch ESP-IDF linker script to increase IRAM and DRAM size for testing mode.""" # Check if we're in testing mode by looking for the define build_flags = env.get("BUILD_FLAGS", []) testing_mode = any("-DESPHOME_TESTING_MODE" in flag for flag in build_flags) @@ -34,36 +62,22 @@ def patch_idf_linker_script(source, target, env): print(f"ESPHome: Error reading linker script: {e}") return - # Check if this file contains iram0_0_seg - if 'iram0_0_seg' not in content: - print(f"ESPHome: Warning - iram0_0_seg not found in {memory_ld}") - return + patches = [] - # Look for iram0_0_seg definition and increase its length - # ESP-IDF format can be: - # iram0_0_seg (RX) : org = 0x40080000, len = 0x20000 + 0x0 - # or more complex with nested parentheses: - # iram0_0_seg (RX) : org = (0x40370000 + 0x4000), len = (((0x403CB700 - (0x40378000 - 0x3FC88000)) - 0x3FC88000) + 0x8000 - 0x4000) - # We want to change len to TESTING_IRAM_SIZE for testing + content, patched = patch_segment(content, 'iram0_0_seg', TESTING_IRAM_SIZE) + if patched: + patches.append(f"IRAM={TESTING_IRAM_SIZE:#x}") - # Use a more robust approach: find the line and manually parse it - lines = content.split('\n') - for i, line in enumerate(lines): - if 'iram0_0_seg' in line and 'len' in line: - # Find the position of "len = " and replace everything after it until the end of the statement - match = re.search(r'(iram0_0_seg\s*\([^)]*\)\s*:\s*org\s*=\s*(?:\([^)]+\)|0x[0-9a-fA-F]+)\s*,\s*len\s*=\s*)(.+?)(\s*)$', line) - if match: - lines[i] = f"{match.group(1)}{TESTING_IRAM_SIZE:#x}{match.group(3)}" - break + content, patched = patch_segment(content, 'dram0_0_seg', TESTING_DRAM_SIZE) + if patched: + patches.append(f"DRAM={TESTING_DRAM_SIZE:#x}") - updated = '\n'.join(lines) - - if updated != content: + if patches: with open(memory_ld, "w") as f: - f.write(updated) - print(f"ESPHome: Patched IRAM size to {TESTING_IRAM_SIZE:#x} in {memory_ld} for testing mode") + f.write(content) + print(f"ESPHome: Patched {', '.join(patches)} in {memory_ld} for testing mode") else: - print(f"ESPHome: Warning - could not patch iram0_0_seg in {memory_ld}") + print(f"ESPHome: Warning - could not patch memory segments in {memory_ld}") # Hook into the build process before linking diff --git a/esphome/components/esp32/preference_backend.h b/esphome/components/esp32/preference_backend.h new file mode 100644 index 0000000000..893bc35f0c --- /dev/null +++ b/esphome/components/esp32/preference_backend.h @@ -0,0 +1,27 @@ +#pragma once +#ifdef USE_ESP32 + +#include +#include + +namespace esphome::esp32 { + +class ESP32PreferenceBackend final { + public: + bool save(const uint8_t *data, size_t len); + bool load(uint8_t *data, size_t len); + + uint32_t key; + uint32_t nvs_handle; +}; + +class ESP32Preferences; +ESP32Preferences *get_preferences(); + +} // namespace esphome::esp32 + +namespace esphome { +using PreferenceBackend = esp32::ESP32PreferenceBackend; +} // namespace esphome + +#endif // USE_ESP32 diff --git a/esphome/components/esp32/preferences.cpp b/esphome/components/esp32/preferences.cpp index 4e0bb68133..bc0a34ebe8 100644 --- a/esphome/components/esp32/preferences.cpp +++ b/esphome/components/esp32/preferences.cpp @@ -1,218 +1,200 @@ #ifdef USE_ESP32 +#include "preferences.h" #include "esphome/core/helpers.h" #include "esphome/core/log.h" -#include "esphome/core/preferences.h" #include #include #include -#include #include -namespace esphome { -namespace esp32 { +namespace esphome::esp32 { -static const char *const TAG = "esp32.preferences"; +static const char *const TAG = "preferences"; // Buffer size for converting uint32_t to string: max "4294967295" (10 chars) + null terminator + 1 padding static constexpr size_t KEY_BUFFER_SIZE = 12; struct NVSData { uint32_t key; - std::unique_ptr data; - size_t len; - - void set_data(const uint8_t *src, size_t size) { - if (!this->data || this->len != size) { - this->data = std::make_unique(size); - this->len = size; - } - memcpy(this->data.get(), src, size); - } + SmallInlineBuffer<8> data; // Most prefs fit in 8 bytes (covers fan, cover, select, etc.) }; static std::vector s_pending_save; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) -class ESP32PreferenceBackend : public ESPPreferenceBackend { - public: - uint32_t key; - uint32_t nvs_handle; - bool save(const uint8_t *data, size_t len) override { - // try find in pending saves and update that - for (auto &obj : s_pending_save) { - if (obj.key == this->key) { - obj.set_data(data, len); - return true; - } - } - NVSData save{}; - save.key = this->key; - save.set_data(data, len); - s_pending_save.emplace_back(std::move(save)); - ESP_LOGVV(TAG, "s_pending_save: key: %" PRIu32 ", len: %zu", this->key, len); - return true; - } - bool load(uint8_t *data, size_t len) override { - // try find in pending saves and load from that - for (auto &obj : s_pending_save) { - if (obj.key == this->key) { - if (obj.len != len) { - // size mismatch - return false; - } - memcpy(data, obj.data.get(), len); - return true; - } - } - - char key_str[KEY_BUFFER_SIZE]; - snprintf(key_str, sizeof(key_str), "%" PRIu32, this->key); - size_t actual_len; - esp_err_t err = nvs_get_blob(this->nvs_handle, key_str, nullptr, &actual_len); - if (err != 0) { - ESP_LOGV(TAG, "nvs_get_blob('%s'): %s - the key might not be set yet", key_str, esp_err_to_name(err)); - return false; - } - if (actual_len != len) { - ESP_LOGVV(TAG, "NVS length does not match (%zu!=%zu)", actual_len, len); - return false; - } - err = nvs_get_blob(this->nvs_handle, key_str, data, &len); - if (err != 0) { - ESP_LOGV(TAG, "nvs_get_blob('%s') failed: %s", key_str, esp_err_to_name(err)); - return false; - } else { - ESP_LOGVV(TAG, "nvs_get_blob: key: %s, len: %zu", key_str, len); - } - return true; - } -}; - -class ESP32Preferences : public ESPPreferences { - public: - uint32_t nvs_handle; - - void open() { - nvs_flash_init(); - esp_err_t err = nvs_open("esphome", NVS_READWRITE, &nvs_handle); - if (err == 0) - return; - - ESP_LOGW(TAG, "nvs_open failed: %s - erasing NVS", esp_err_to_name(err)); - nvs_flash_deinit(); - nvs_flash_erase(); - nvs_flash_init(); - - err = nvs_open("esphome", NVS_READWRITE, &nvs_handle); - if (err != 0) { - nvs_handle = 0; - } - } - ESPPreferenceObject make_preference(size_t length, uint32_t type, bool in_flash) override { - return this->make_preference(length, type); - } - ESPPreferenceObject make_preference(size_t length, uint32_t type) override { - auto *pref = new ESP32PreferenceBackend(); // NOLINT(cppcoreguidelines-owning-memory) - pref->nvs_handle = this->nvs_handle; - pref->key = type; - - return ESPPreferenceObject(pref); - } - - bool sync() override { - if (s_pending_save.empty()) - return true; - - ESP_LOGV(TAG, "Saving %zu items...", s_pending_save.size()); - // goal try write all pending saves even if one fails - int cached = 0, written = 0, failed = 0; - esp_err_t last_err = ESP_OK; - uint32_t last_key = 0; - - // go through vector from back to front (makes erase easier/more efficient) - for (ssize_t i = s_pending_save.size() - 1; i >= 0; i--) { - const auto &save = s_pending_save[i]; - char key_str[KEY_BUFFER_SIZE]; - snprintf(key_str, sizeof(key_str), "%" PRIu32, save.key); - ESP_LOGVV(TAG, "Checking if NVS data %s has changed", key_str); - if (this->is_changed_(this->nvs_handle, save, key_str)) { - esp_err_t err = nvs_set_blob(this->nvs_handle, key_str, save.data.get(), save.len); - ESP_LOGV(TAG, "sync: key: %s, len: %zu", key_str, save.len); - if (err != 0) { - ESP_LOGV(TAG, "nvs_set_blob('%s', len=%zu) failed: %s", key_str, save.len, esp_err_to_name(err)); - failed++; - last_err = err; - last_key = save.key; - continue; - } - written++; - } else { - ESP_LOGV(TAG, "NVS data not changed skipping %" PRIu32 " len=%zu", save.key, save.len); - cached++; - } - s_pending_save.erase(s_pending_save.begin() + i); - } - ESP_LOGD(TAG, "Writing %d items: %d cached, %d written, %d failed", cached + written + failed, cached, written, - failed); - if (failed > 0) { - ESP_LOGE(TAG, "Writing %d items failed. Last error=%s for key=%" PRIu32, failed, esp_err_to_name(last_err), - last_key); - } - - // note: commit on esp-idf currently is a no-op, nvs_set_blob always writes - esp_err_t err = nvs_commit(this->nvs_handle); - if (err != 0) { - ESP_LOGV(TAG, "nvs_commit() failed: %s", esp_err_to_name(err)); - return false; - } - - return failed == 0; - } - - protected: - bool is_changed_(uint32_t nvs_handle, const NVSData &to_save, const char *key_str) { - size_t actual_len; - esp_err_t err = nvs_get_blob(nvs_handle, key_str, nullptr, &actual_len); - if (err != 0) { - ESP_LOGV(TAG, "nvs_get_blob('%s'): %s - the key might not be set yet", key_str, esp_err_to_name(err)); +bool ESP32PreferenceBackend::save(const uint8_t *data, size_t len) { + // try find in pending saves and update that + for (auto &obj : s_pending_save) { + if (obj.key == this->key) { + obj.data.set(data, len); return true; } - // Check size first before allocating memory - if (actual_len != to_save.len) { - return true; - } - // Most preferences are small, use stack buffer with heap fallback for large ones - SmallBufferWithHeapFallback<256> stored_data(actual_len); - err = nvs_get_blob(nvs_handle, key_str, stored_data.get(), &actual_len); - if (err != 0) { - ESP_LOGV(TAG, "nvs_get_blob('%s') failed: %s", key_str, esp_err_to_name(err)); - return true; - } - return memcmp(to_save.data.get(), stored_data.get(), to_save.len) != 0; } - - bool reset() override { - ESP_LOGD(TAG, "Erasing storage"); - s_pending_save.clear(); - - nvs_flash_deinit(); - nvs_flash_erase(); - // Make the handle invalid to prevent any saves until restart - nvs_handle = 0; - return true; - } -}; - -void setup_preferences() { - auto *prefs = new ESP32Preferences(); // NOLINT(cppcoreguidelines-owning-memory) - prefs->open(); - global_preferences = prefs; + NVSData save{}; + save.key = this->key; + save.data.set(data, len); + s_pending_save.push_back(std::move(save)); + ESP_LOGVV(TAG, "s_pending_save: key: %" PRIu32 ", len: %zu", this->key, len); + return true; } -} // namespace esp32 +bool ESP32PreferenceBackend::load(uint8_t *data, size_t len) { + // try find in pending saves and load from that + for (auto &obj : s_pending_save) { + if (obj.key == this->key) { + if (obj.data.size() != len) { + // size mismatch + return false; + } + memcpy(data, obj.data.data(), len); + return true; + } + } + char key_str[KEY_BUFFER_SIZE]; + snprintf(key_str, sizeof(key_str), "%" PRIu32, this->key); + size_t actual_len; + esp_err_t err = nvs_get_blob(this->nvs_handle, key_str, nullptr, &actual_len); + if (err != 0) { + ESP_LOGV(TAG, "nvs_get_blob('%s'): %s - the key might not be set yet", key_str, esp_err_to_name(err)); + return false; + } + if (actual_len != len) { + ESP_LOGVV(TAG, "NVS length does not match (%zu!=%zu)", actual_len, len); + return false; + } + err = nvs_get_blob(this->nvs_handle, key_str, data, &len); + if (err != 0) { + ESP_LOGV(TAG, "nvs_get_blob('%s') failed: %s", key_str, esp_err_to_name(err)); + return false; + } else { + ESP_LOGVV(TAG, "nvs_get_blob: key: %s, len: %zu", key_str, len); + } + return true; +} + +void ESP32Preferences::open() { + nvs_flash_init(); + esp_err_t err = nvs_open("esphome", NVS_READWRITE, &this->nvs_handle); + if (err == 0) + return; + + ESP_LOGW(TAG, "nvs_open failed: %s - erasing NVS", esp_err_to_name(err)); + nvs_flash_deinit(); + nvs_flash_erase(); + nvs_flash_init(); + + err = nvs_open("esphome", NVS_READWRITE, &this->nvs_handle); + if (err != 0) { + this->nvs_handle = 0; + } +} + +ESPPreferenceObject ESP32Preferences::make_preference(size_t length, uint32_t type) { + auto *pref = new ESP32PreferenceBackend(); // NOLINT(cppcoreguidelines-owning-memory) + pref->nvs_handle = this->nvs_handle; + pref->key = type; + + return ESPPreferenceObject(pref); +} + +bool ESP32Preferences::sync() { + if (s_pending_save.empty()) + return true; + + ESP_LOGV(TAG, "Saving %zu items...", s_pending_save.size()); + int cached = 0, written = 0, failed = 0; + esp_err_t last_err = ESP_OK; + uint32_t last_key = 0; + + for (const auto &save : s_pending_save) { + char key_str[KEY_BUFFER_SIZE]; + snprintf(key_str, sizeof(key_str), "%" PRIu32, save.key); + ESP_LOGVV(TAG, "Checking if NVS data %s has changed", key_str); + if (this->is_changed_(this->nvs_handle, save, key_str)) { + esp_err_t err = nvs_set_blob(this->nvs_handle, key_str, save.data.data(), save.data.size()); + ESP_LOGV(TAG, "sync: key: %s, len: %zu", key_str, save.data.size()); + if (err != 0) { + ESP_LOGV(TAG, "nvs_set_blob('%s', len=%zu) failed: %s", key_str, save.data.size(), esp_err_to_name(err)); + failed++; + last_err = err; + last_key = save.key; + continue; + } + written++; + } else { + ESP_LOGV(TAG, "NVS data not changed skipping %" PRIu32 " len=%zu", save.key, save.data.size()); + cached++; + } + } + s_pending_save.clear(); + + if (failed > 0) { + ESP_LOGE(TAG, "Writing %d items: %d cached, %d written, %d failed. Last error=%s for key=%" PRIu32, + cached + written + failed, cached, written, failed, esp_err_to_name(last_err), last_key); + } else if (written > 0) { + ESP_LOGD(TAG, "Writing %d items: %d cached, %d written, %d failed", cached + written + failed, cached, written, + failed); + } else { + ESP_LOGV(TAG, "Writing %d items: %d cached, %d written, %d failed", cached + written + failed, cached, written, + failed); + } + + // note: commit on esp-idf currently is a no-op, nvs_set_blob always writes + esp_err_t err = nvs_commit(this->nvs_handle); + if (err != 0) { + ESP_LOGV(TAG, "nvs_commit() failed: %s", esp_err_to_name(err)); + return false; + } + + return failed == 0; +} + +bool ESP32Preferences::is_changed_(uint32_t nvs_handle, const NVSData &to_save, const char *key_str) { + size_t actual_len; + esp_err_t err = nvs_get_blob(nvs_handle, key_str, nullptr, &actual_len); + if (err != 0) { + ESP_LOGV(TAG, "nvs_get_blob('%s'): %s - the key might not be set yet", key_str, esp_err_to_name(err)); + return true; + } + // Check size first before allocating memory + if (actual_len != to_save.data.size()) { + return true; + } + // Most preferences are small, use stack buffer with heap fallback for large ones + SmallBufferWithHeapFallback<256> stored_data(actual_len); + err = nvs_get_blob(nvs_handle, key_str, stored_data.get(), &actual_len); + if (err != 0) { + ESP_LOGV(TAG, "nvs_get_blob('%s') failed: %s", key_str, esp_err_to_name(err)); + return true; + } + return memcmp(to_save.data.data(), stored_data.get(), to_save.data.size()) != 0; +} + +bool ESP32Preferences::reset() { + ESP_LOGD(TAG, "Erasing storage"); + s_pending_save.clear(); + + nvs_flash_deinit(); + nvs_flash_erase(); + // Make the handle invalid to prevent any saves until restart + this->nvs_handle = 0; + return true; +} + +static ESP32Preferences s_preferences; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) + +ESP32Preferences *get_preferences() { return &s_preferences; } + +void setup_preferences() { + s_preferences.open(); + global_preferences = &s_preferences; +} + +} // namespace esphome::esp32 + +namespace esphome { ESPPreferences *global_preferences; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) - } // namespace esphome #endif // USE_ESP32 diff --git a/esphome/components/esp32/preferences.h b/esphome/components/esp32/preferences.h index e44213e4cf..0e187d87a9 100644 --- a/esphome/components/esp32/preferences.h +++ b/esphome/components/esp32/preferences.h @@ -1,12 +1,33 @@ #pragma once #ifdef USE_ESP32 -namespace esphome { -namespace esp32 { +#include "esphome/core/preference_backend.h" + +namespace esphome::esp32 { + +struct NVSData; + +class ESP32Preferences final : public PreferencesMixin { + public: + using PreferencesMixin::make_preference; + void open(); + ESPPreferenceObject make_preference(size_t length, uint32_t type, bool in_flash) { + return this->make_preference(length, type); + } + ESPPreferenceObject make_preference(size_t length, uint32_t type); + bool sync(); + bool reset(); + + uint32_t nvs_handle; + + protected: + bool is_changed_(uint32_t nvs_handle, const NVSData &to_save, const char *key_str); +}; void setup_preferences(); -} // namespace esp32 -} // namespace esphome +} // namespace esphome::esp32 + +DECLARE_PREFERENCE_ALIASES(esphome::esp32::ESP32Preferences) #endif // USE_ESP32 diff --git a/esphome/components/esp32/printf_stubs.cpp b/esphome/components/esp32/printf_stubs.cpp new file mode 100644 index 0000000000..386fbbd79d --- /dev/null +++ b/esphome/components/esp32/printf_stubs.cpp @@ -0,0 +1,90 @@ +/* + * Linker wrap stubs for FILE*-based printf functions. + * + * ESP-IDF SDK components (gpio driver, ringbuf, log_write) reference + * fprintf(), printf(), vprintf(), and vfprintf() which pull in the full + * printf implementation (~11 KB on newlib's _vfprintf_r, ~2.8 KB on + * picolibc's vfprintf). This is a separate implementation from the one + * used by snprintf/vsnprintf that handles FILE* stream I/O with buffering + * and locking. + * + * ESPHome replaces the ESP-IDF log handler via esp_log_set_vprintf_(), + * so the SDK's vprintf() path is dead code at runtime. The fprintf() + * and printf() calls in SDK components are only in debug/assert paths + * (gpio_dump_io_configuration, ringbuf diagnostics) that are either + * GC'd or never called. Crash backtraces and panic output are + * unaffected — they use esp_rom_printf() which is a ROM function + * and does not go through libc. + * + * These stubs redirect through vsnprintf() (which uses _svfprintf_r + * already in the binary) and fwrite(), allowing the linker to + * dead-code eliminate _vfprintf_r. + * + * Saves ~11 KB of flash. + * + * To disable these wraps, set enable_full_printf: true in the esp32 + * advanced config section. + */ + +#if defined(USE_ESP_IDF) && !defined(USE_FULL_PRINTF) +#include +#include + +#include "esp_system.h" + +namespace esphome::esp32 {} + +static constexpr size_t PRINTF_BUFFER_SIZE = 512; + +// These stubs are essentially dead code at runtime — ESPHome replaces the +// ESP-IDF log handler, and the SDK's printf/fprintf calls only exist in +// debug/assert paths that are never reached in normal operation. +// The buffer overflow check is purely defensive and should never trigger. +static int write_printf_buffer(FILE *stream, char *buf, int len) { + if (len < 0) { + return len; + } + size_t write_len = len; + if (write_len >= PRINTF_BUFFER_SIZE) { + fwrite(buf, 1, PRINTF_BUFFER_SIZE - 1, stream); + esp_system_abort("printf buffer overflow; set enable_full_printf: true in esp32 framework advanced config"); + } + if (fwrite(buf, 1, write_len, stream) < write_len || ferror(stream)) { + return -1; + } + return len; +} + +// NOLINTBEGIN(bugprone-reserved-identifier,cert-dcl37-c,cert-dcl51-cpp,readability-identifier-naming) +extern "C" { + +int __wrap_vprintf(const char *fmt, va_list ap) { + char buf[PRINTF_BUFFER_SIZE]; + return write_printf_buffer(stdout, buf, vsnprintf(buf, sizeof(buf), fmt, ap)); +} + +int __wrap_printf(const char *fmt, ...) { + va_list ap; + va_start(ap, fmt); + int len = __wrap_vprintf(fmt, ap); + va_end(ap); + return len; +} + +int __wrap_vfprintf(FILE *stream, const char *fmt, va_list ap) { + char buf[PRINTF_BUFFER_SIZE]; + return write_printf_buffer(stream, buf, vsnprintf(buf, sizeof(buf), fmt, ap)); +} + +int __wrap_fprintf(FILE *stream, const char *fmt, ...) { + va_list ap; + va_start(ap, fmt); + int len = __wrap_vfprintf(stream, fmt, ap); + va_end(ap); + return len; +} + +} // extern "C" +// NOLINTEND(bugprone-reserved-identifier,cert-dcl37-c,cert-dcl51-cpp,readability-identifier-naming) + +#endif // USE_ESP_IDF && !USE_FULL_PRINTF diff --git a/esphome/components/esp32_ble/__init__.py b/esphome/components/esp32_ble/__init__.py index dcc3ce71cf..2e5e358753 100644 --- a/esphome/components/esp32_ble/__init__.py +++ b/esphome/components/esp32_ble/__init__.py @@ -9,6 +9,7 @@ from esphome import automation import esphome.codegen as cg from esphome.components import socket from esphome.components.esp32 import add_idf_sdkconfig_option, const, get_esp32_variant +from esphome.components.esp32.const import VARIANT_ESP32C2 import esphome.config_validation as cv from esphome.const import ( CONF_ENABLE_ON_BOOT, @@ -20,6 +21,7 @@ from esphome.const import ( ) from esphome.core import CORE, CoroPriority, TimePeriod, coroutine_with_priority import esphome.final_validate as fv +from esphome.types import ConfigType DEPENDENCIES = ["esp32"] CODEOWNERS = ["@jesserockz", "@Rapsssito", "@bdraco"] @@ -132,10 +134,38 @@ class HandlerCounts: _handler_counts = HandlerCounts() +def _add_callback( + parent_var: cg.MockObj, + method: str, + handler_var: cg.MockObj, + params: str, + call_args: str, +) -> None: + """Generate a lambda callback that forwards to a handler method. + + Uses a braced scope with a local pointer variable so the generated C++ + lambda captures only that pointer, avoiding GCC warnings about capturing + variables with static storage duration. + """ + cg.add( + cg.RawStatement( + f"{{ auto *h = {handler_var}; " + f"{parent_var}->{method}(" + f"[h]({params}) {{ h->{call_args}; }}); }}" + ) + ) + + def register_gap_event_handler(parent_var: cg.MockObj, handler_var: cg.MockObj) -> None: """Register a GAP event handler and track the count.""" _handler_counts.gap_event += 1 - cg.add(parent_var.register_gap_event_handler(handler_var)) + _add_callback( + parent_var, + "add_gap_event_callback", + handler_var, + "esp_gap_ble_cb_event_t event, esp_ble_gap_cb_param_t *param", + "gap_event_handler(event, param)", + ) def register_gap_scan_event_handler( @@ -143,7 +173,13 @@ def register_gap_scan_event_handler( ) -> None: """Register a GAP scan event handler and track the count.""" _handler_counts.gap_scan_event += 1 - cg.add(parent_var.register_gap_scan_event_handler(handler_var)) + _add_callback( + parent_var, + "add_gap_scan_event_callback", + handler_var, + "const esphome::esp32_ble::BLEScanResult &scan_result", + "gap_scan_event_handler(scan_result)", + ) def register_gattc_event_handler( @@ -151,7 +187,13 @@ def register_gattc_event_handler( ) -> None: """Register a GATTc event handler and track the count.""" _handler_counts.gattc_event += 1 - cg.add(parent_var.register_gattc_event_handler(handler_var)) + _add_callback( + parent_var, + "add_gattc_event_callback", + handler_var, + "esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if, esp_ble_gattc_cb_param_t *param", + "gattc_event_handler(event, gattc_if, param)", + ) def register_gatts_event_handler( @@ -159,7 +201,13 @@ def register_gatts_event_handler( ) -> None: """Register a GATTs event handler and track the count.""" _handler_counts.gatts_event += 1 - cg.add(parent_var.register_gatts_event_handler(handler_var)) + _add_callback( + parent_var, + "add_gatts_event_callback", + handler_var, + "esp_gatts_cb_event_t event, esp_gatt_if_t gatts_if, esp_ble_gatts_cb_param_t *param", + "gatts_event_handler(event, gatts_if, param)", + ) def register_ble_status_event_handler( @@ -167,7 +215,13 @@ def register_ble_status_event_handler( ) -> None: """Register a BLE status event handler and track the count.""" _handler_counts.ble_status_event += 1 - cg.add(parent_var.register_ble_status_event_handler(handler_var)) + _add_callback( + parent_var, + "add_ble_status_event_callback", + handler_var, + "", + "ble_before_disabled_event_handler()", + ) def register_bt_logger(*loggers: BTLoggers) -> None: @@ -187,6 +241,9 @@ def register_bt_logger(*loggers: BTLoggers) -> None: CONF_BLE_ID = "ble_id" CONF_IO_CAPABILITY = "io_capability" +CONF_AUTH_REQ_MODE = "auth_req_mode" +CONF_MAX_KEY_SIZE = "max_key_size" +CONF_MIN_KEY_SIZE = "min_key_size" CONF_ADVERTISING = "advertising" CONF_ADVERTISING_CYCLE_TIME = "advertising_cycle_time" CONF_DISABLE_BT_LOGS = "disable_bt_logs" @@ -220,10 +277,6 @@ NO_BLUETOOTH_VARIANTS = [const.VARIANT_ESP32S2] esp32_ble_ns = cg.esphome_ns.namespace("esp32_ble") ESP32BLE = esp32_ble_ns.class_("ESP32BLE", cg.Component) -GAPEventHandler = esp32_ble_ns.class_("GAPEventHandler") -GATTcEventHandler = esp32_ble_ns.class_("GATTcEventHandler") -GATTsEventHandler = esp32_ble_ns.class_("GATTsEventHandler") - BLEEnabledCondition = esp32_ble_ns.class_("BLEEnabledCondition", automation.Condition) BLEEnableAction = esp32_ble_ns.class_("BLEEnableAction", automation.Action) BLEDisableAction = esp32_ble_ns.class_("BLEDisableAction", automation.Action) @@ -237,6 +290,18 @@ IO_CAPABILITY = { "display_yes_no": IoCapability.IO_CAP_IO, } +AuthReqMode = esp32_ble_ns.enum("AuthReqMode") +AUTH_REQ_MODE = { + "no_bond": AuthReqMode.AUTH_REQ_NO_BOND, + "bond": AuthReqMode.AUTH_REQ_BOND, + "mitm": AuthReqMode.AUTH_REQ_MITM, + "bond_mitm": AuthReqMode.AUTH_REQ_BOND_MITM, + "sc_only": AuthReqMode.AUTH_REQ_SC_ONLY, + "sc_bond": AuthReqMode.AUTH_REQ_SC_BOND, + "sc_mitm": AuthReqMode.AUTH_REQ_SC_MITM, + "sc_mitm_bond": AuthReqMode.AUTH_REQ_SC_MITM_BOND, +} + esp_power_level_t = cg.global_ns.enum("esp_power_level_t") TX_POWER_LEVELS = { @@ -257,6 +322,10 @@ CONFIG_SCHEMA = cv.Schema( cv.Optional(CONF_IO_CAPABILITY, default="none"): cv.enum( IO_CAPABILITY, lower=True ), + # note: no defaults so we can action them not being present + cv.Optional(CONF_AUTH_REQ_MODE): cv.enum(AUTH_REQ_MODE, lower=True), + cv.Optional(CONF_MAX_KEY_SIZE): cv.int_range(min=7, max=16), + cv.Optional(CONF_MIN_KEY_SIZE): cv.int_range(min=7, max=16), cv.Optional(CONF_ENABLE_ON_BOOT, default=True): cv.boolean, cv.Optional(CONF_ADVERTISING, default=False): cv.boolean, cv.Optional( @@ -278,6 +347,23 @@ CONFIG_SCHEMA = cv.Schema( ).extend(cv.COMPONENT_SCHEMA) +def _validate_key_sizes(config: ConfigType) -> ConfigType: + if ( + CONF_MIN_KEY_SIZE in config + and CONF_MAX_KEY_SIZE in config + and config[CONF_MIN_KEY_SIZE] > config[CONF_MAX_KEY_SIZE] + ): + raise cv.Invalid( + f"min_key_size ({config[CONF_MIN_KEY_SIZE]}) must be " + f"less than or equal to " + f"max_key_size ({config[CONF_MAX_KEY_SIZE]})" + ) + return config + + +CONFIG_SCHEMA = cv.All(CONFIG_SCHEMA, _validate_key_sizes) + + bt_uuid16_format = "XXXX" bt_uuid32_format = "XXXXXXXX" bt_uuid128_format = "XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX" @@ -387,6 +473,15 @@ def final_validation(config): f"Name '{name}' is too long, maximum length is {max_length} characters" ) + # ESP32-C2 has very limited RAM (~272KB). Without releasing BLE IRAM, + # esp_bt_controller_init fails with ESP_ERR_NO_MEM. + # CONFIG_BT_RELEASE_IRAM changes the memory layout so IRAM and DRAM share + # space more flexibly, giving the BT controller enough contiguous memory. + # This requires CONFIG_ESP_SYSTEM_PMP_IDRAM_SPLIT to be disabled. + if get_esp32_variant() == VARIANT_ESP32C2: + add_idf_sdkconfig_option("CONFIG_BT_RELEASE_IRAM", True) + add_idf_sdkconfig_option("CONFIG_ESP_SYSTEM_PMP_IDRAM_SPLIT", False) + # Set GATT Client/Server sdkconfig options based on which components are loaded full_config = fv.full_config.get() @@ -403,16 +498,16 @@ def final_validation(config): add_idf_sdkconfig_option("CONFIG_ESP_HOSTED_ENABLE_BT_BLUEDROID", True) add_idf_sdkconfig_option("CONFIG_ESP_HOSTED_BLUEDROID_HCI_VHCI", True) - # Check if BLE Server is needed - has_ble_server = "esp32_ble_server" in full_config - # Check if BLE Client is needed (via esp32_ble_tracker or esp32_ble_client) has_ble_client = ( "esp32_ble_tracker" in full_config or "esp32_ble_client" in full_config ) + # Check if BLE Server is needed + has_ble_server = "esp32_ble_server" in full_config + # ESP-IDF BLE stack requires GATT Server to be enabled when GATT Client is enabled - # This is an internal dependency in the Bluedroid stack (tested ESP-IDF 5.4.2-5.5.1) + # This is an internal dependency in the Bluedroid stack # See: https://github.com/espressif/esp-idf/issues/17724 add_idf_sdkconfig_option("CONFIG_BT_GATTS_ENABLE", has_ble_server or has_ble_client) add_idf_sdkconfig_option("CONFIG_BT_GATTC_ENABLE", has_ble_client) @@ -477,6 +572,21 @@ async def to_code(config): var = cg.new_Pvariable(config[CONF_ID]) cg.add(var.set_enable_on_boot(config[CONF_ENABLE_ON_BOOT])) cg.add(var.set_io_capability(config[CONF_IO_CAPABILITY])) + + if ( + CONF_AUTH_REQ_MODE in config + or CONF_MAX_KEY_SIZE in config + or CONF_MIN_KEY_SIZE in config + ): + cg.add_define("ESPHOME_ESP32_BLE_EXTENDED_AUTH_PARAMS", None) + + if CONF_AUTH_REQ_MODE in config: + cg.add(var.set_auth_req(config[CONF_AUTH_REQ_MODE])) + if CONF_MAX_KEY_SIZE in config: + cg.add(var.set_max_key_size(config[CONF_MAX_KEY_SIZE])) + if CONF_MIN_KEY_SIZE in config: + cg.add(var.set_min_key_size(config[CONF_MIN_KEY_SIZE])) + cg.add(var.set_advertising_cycle_time(config[CONF_ADVERTISING_CYCLE_TIME])) if (name := config.get(CONF_NAME)) is not None: cg.add(var.set_name(name)) @@ -542,11 +652,15 @@ async def ble_enabled_to_code(config, condition_id, template_arg, args): return cg.new_Pvariable(condition_id, template_arg) -@automation.register_action("ble.enable", BLEEnableAction, cv.Schema({})) +@automation.register_action( + "ble.enable", BLEEnableAction, cv.Schema({}), synchronous=True +) async def ble_enable_to_code(config, action_id, template_arg, args): return cg.new_Pvariable(action_id, template_arg) -@automation.register_action("ble.disable", BLEDisableAction, cv.Schema({})) +@automation.register_action( + "ble.disable", BLEDisableAction, cv.Schema({}), synchronous=True +) async def ble_disable_to_code(config, action_id, template_arg, args): return cg.new_Pvariable(action_id, template_arg) diff --git a/esphome/components/esp32_ble/ble.cpp b/esphome/components/esp32_ble/ble.cpp index 87b5e2b738..2cd2ec67f7 100644 --- a/esphome/components/esp32_ble/ble.cpp +++ b/esphome/components/esp32_ble/ble.cpp @@ -81,8 +81,6 @@ void ESP32BLE::disable() { this->state_ = BLE_COMPONENT_STATE_DISABLE; } -bool ESP32BLE::is_active() { return this->state_ == BLE_COMPONENT_STATE_ACTIVE; } - #ifdef USE_ESP32_BLE_ADVERTISING void ESP32BLE::advertising_start() { this->advertising_init_(); @@ -273,7 +271,7 @@ bool ESP32BLE::ble_setup_() { device_name = this->name_; } } else { - const std::string &app_name = App.get_name(); + const auto &app_name = App.get_name(); size_t name_len = app_name.length(); if (name_len > 20) { if (App.is_name_add_mac_suffix_enabled()) { @@ -296,12 +294,39 @@ bool ESP32BLE::ble_setup_() { return false; } - err = esp_ble_gap_set_security_param(ESP_BLE_SM_IOCAP_MODE, &(this->io_cap_), sizeof(uint8_t)); + err = esp_ble_gap_set_security_param(ESP_BLE_SM_IOCAP_MODE, &(this->io_cap_), sizeof(esp_ble_io_cap_t)); if (err != ESP_OK) { - ESP_LOGE(TAG, "esp_ble_gap_set_security_param failed: %d", err); + ESP_LOGE(TAG, "esp_ble_gap_set_security_param iocap_mode failed: %d", err); return false; } +#ifdef ESPHOME_ESP32_BLE_EXTENDED_AUTH_PARAMS + if (this->max_key_size_) { + err = esp_ble_gap_set_security_param(ESP_BLE_SM_MAX_KEY_SIZE, &(this->max_key_size_), sizeof(uint8_t)); + if (err != ESP_OK) { + ESP_LOGE(TAG, "esp_ble_gap_set_security_param max_key_size failed: %d", err); + return false; + } + } + + if (this->min_key_size_) { + err = esp_ble_gap_set_security_param(ESP_BLE_SM_MIN_KEY_SIZE, &(this->min_key_size_), sizeof(uint8_t)); + if (err != ESP_OK) { + ESP_LOGE(TAG, "esp_ble_gap_set_security_param min_key_size failed: %d", err); + return false; + } + } + + if (this->auth_req_mode_) { + err = esp_ble_gap_set_security_param(ESP_BLE_SM_AUTHEN_REQ_MODE, &(this->auth_req_mode_.value()), + sizeof(esp_ble_auth_req_t)); + if (err != ESP_OK) { + ESP_LOGE(TAG, "esp_ble_gap_set_security_param authen_req_mode failed: %d", err); + return false; + } + } +#endif // ESPHOME_ESP32_BLE_EXTENDED_AUTH_PARAMS + // BLE takes some time to be fully set up, 200ms should be more than enough delay(200); // NOLINT @@ -369,42 +394,9 @@ bool ESP32BLE::ble_dismantle_() { } void ESP32BLE::loop() { - switch (this->state_) { - case BLE_COMPONENT_STATE_OFF: - case BLE_COMPONENT_STATE_DISABLED: - return; - case BLE_COMPONENT_STATE_DISABLE: { - ESP_LOGD(TAG, "Disabling"); - -#ifdef ESPHOME_ESP32_BLE_BLE_STATUS_EVENT_HANDLER_COUNT - for (auto *ble_event_handler : this->ble_status_event_handlers_) { - ble_event_handler->ble_before_disabled_event_handler(); - } -#endif - - if (!ble_dismantle_()) { - ESP_LOGE(TAG, "Could not be dismantled"); - this->mark_failed(); - return; - } - this->state_ = BLE_COMPONENT_STATE_DISABLED; - return; - } - case BLE_COMPONENT_STATE_ENABLE: { - ESP_LOGD(TAG, "Enabling"); - this->state_ = BLE_COMPONENT_STATE_OFF; - - if (!ble_setup_()) { - ESP_LOGE(TAG, "Could not be set up"); - this->mark_failed(); - return; - } - - this->state_ = BLE_COMPONENT_STATE_ACTIVE; - return; - } - case BLE_COMPONENT_STATE_ACTIVE: - break; + if (this->state_ != BLE_COMPONENT_STATE_ACTIVE) { + this->loop_handle_state_transition_not_active_(); + return; } BLEEvent *ble_event = this->ble_events_.pop(); @@ -416,9 +408,7 @@ void ESP32BLE::loop() { esp_gatt_if_t gatts_if = ble_event->event_.gatts.gatts_if; esp_ble_gatts_cb_param_t *param = &ble_event->event_.gatts.gatts_param; ESP_LOGV(TAG, "gatts_event [esp_gatt_if: %d] - %d", gatts_if, event); - for (auto *gatts_handler : this->gatts_event_handlers_) { - gatts_handler->gatts_event_handler(event, gatts_if, param); - } + this->gatts_event_callbacks_.call(event, gatts_if, param); break; } #endif @@ -428,9 +418,7 @@ void ESP32BLE::loop() { esp_gatt_if_t gattc_if = ble_event->event_.gattc.gattc_if; esp_ble_gattc_cb_param_t *param = &ble_event->event_.gattc.gattc_param; ESP_LOGV(TAG, "gattc_event [esp_gatt_if: %d] - %d", gattc_if, event); - for (auto *gattc_handler : this->gattc_event_handlers_) { - gattc_handler->gattc_event_handler(event, gattc_if, param); - } + this->gattc_event_callbacks_.call(event, gattc_if, param); break; } #endif @@ -439,10 +427,7 @@ void ESP32BLE::loop() { switch (gap_event) { case ESP_GAP_BLE_SCAN_RESULT_EVT: #ifdef ESPHOME_ESP32_BLE_GAP_SCAN_EVENT_HANDLER_COUNT - // Use the new scan event handler - no memcpy! - for (auto *scan_handler : this->gap_scan_event_handlers_) { - scan_handler->gap_scan_event_handler(ble_event->scan_result()); - } + this->gap_scan_event_callbacks_.call(ble_event->scan_result()); #endif break; @@ -457,7 +442,7 @@ void ESP32BLE::loop() { ESP_LOGV(TAG, "gap_event_handler - %d", gap_event); #ifdef ESPHOME_ESP32_BLE_GAP_EVENT_HANDLER_COUNT { - esp_ble_gap_cb_param_t *param; + esp_ble_gap_cb_param_t *param = NULL; // clang-format off switch (gap_event) { // All three scan complete events have the same structure with just status @@ -486,9 +471,7 @@ void ESP32BLE::loop() { } // clang-format on // Dispatch to all registered handlers - for (auto *gap_handler : this->gap_event_handlers_) { - gap_handler->gap_event_handler(gap_event, param); - } + this->gap_event_callbacks_.call(gap_event, param); } #endif break; @@ -520,6 +503,35 @@ void ESP32BLE::loop() { } } +void ESP32BLE::loop_handle_state_transition_not_active_() { + // Caller ensures state_ != ACTIVE + if (this->state_ == BLE_COMPONENT_STATE_DISABLE) { + ESP_LOGD(TAG, "Disabling"); + +#ifdef ESPHOME_ESP32_BLE_BLE_STATUS_EVENT_HANDLER_COUNT + this->ble_status_event_callbacks_.call(); +#endif + + if (!ble_dismantle_()) { + ESP_LOGE(TAG, "Could not be dismantled"); + this->mark_failed(); + return; + } + this->state_ = BLE_COMPONENT_STATE_DISABLED; + } else if (this->state_ == BLE_COMPONENT_STATE_ENABLE) { + ESP_LOGD(TAG, "Enabling"); + this->state_ = BLE_COMPONENT_STATE_OFF; + + if (!ble_setup_()) { + ESP_LOGE(TAG, "Could not be set up"); + this->mark_failed(); + return; + } + + this->state_ = BLE_COMPONENT_STATE_ACTIVE; + } +} + // Helper function to load new event data based on type void load_ble_event(BLEEvent *event, esp_gap_ble_cb_event_t e, esp_ble_gap_cb_param_t *p) { event->load_gap_event(e, p); @@ -550,8 +562,9 @@ template void enqueue_ble_event(Args... args) { load_ble_event(event, args...); // Push the event to the queue + // Push always succeeds: pool is sized to queue capacity (N-1), so if + // allocate() returned non-null, the queue is guaranteed to have room. global_ble->ble_events_.push(event); - // Push always succeeds because we're the only producer and the pool ensures we never exceed queue size } // Explicit template instantiations for the friend function @@ -647,6 +660,7 @@ void ESP32BLE::dump_config() { io_capability_s = "invalid"; break; } + char mac_s[18]; format_mac_addr_upper(mac_address, mac_s); ESP_LOGCONFIG(TAG, @@ -654,22 +668,53 @@ void ESP32BLE::dump_config() { " MAC address: %s\n" " IO Capability: %s", mac_s, io_capability_s); + +#ifdef ESPHOME_ESP32_BLE_EXTENDED_AUTH_PARAMS + const char *auth_req_mode_s = ""; + if (this->auth_req_mode_) { + switch (this->auth_req_mode_.value()) { + case AUTH_REQ_NO_BOND: + auth_req_mode_s = "no_bond"; + break; + case AUTH_REQ_BOND: + auth_req_mode_s = "bond"; + break; + case AUTH_REQ_MITM: + auth_req_mode_s = "mitm"; + break; + case AUTH_REQ_BOND_MITM: + auth_req_mode_s = "bond_mitm"; + break; + case AUTH_REQ_SC_ONLY: + auth_req_mode_s = "sc_only"; + break; + case AUTH_REQ_SC_BOND: + auth_req_mode_s = "sc_bond"; + break; + case AUTH_REQ_SC_MITM: + auth_req_mode_s = "sc_mitm"; + break; + case AUTH_REQ_SC_MITM_BOND: + auth_req_mode_s = "sc_mitm_bond"; + break; + } + } + + ESP_LOGCONFIG(TAG, " Auth Req Mode: %s", auth_req_mode_s); + if (this->max_key_size_ && this->min_key_size_) { + ESP_LOGCONFIG(TAG, " Key Size: %u - %u", this->min_key_size_, this->max_key_size_); + } else if (this->max_key_size_) { + ESP_LOGCONFIG(TAG, " Key Size: - %u", this->max_key_size_); + } else if (this->min_key_size_) { + ESP_LOGCONFIG(TAG, " Key Size: %u - ", this->min_key_size_); + } +#endif // ESPHOME_ESP32_BLE_EXTENDED_AUTH_PARAMS + } else { ESP_LOGCONFIG(TAG, "Bluetooth stack is not enabled"); } } -uint64_t ble_addr_to_uint64(const esp_bd_addr_t address) { - uint64_t u = 0; - u |= uint64_t(address[0] & 0xFF) << 40; - u |= uint64_t(address[1] & 0xFF) << 32; - u |= uint64_t(address[2] & 0xFF) << 24; - u |= uint64_t(address[3] & 0xFF) << 16; - u |= uint64_t(address[4] & 0xFF) << 8; - u |= uint64_t(address[5] & 0xFF) << 0; - return u; -} - ESP32BLE *global_ble = nullptr; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) } // namespace esphome::esp32_ble diff --git a/esphome/components/esp32_ble/ble.h b/esphome/components/esp32_ble/ble.h index 1999c870f8..de8c8c2343 100644 --- a/esphome/components/esp32_ble/ble.h +++ b/esphome/components/esp32_ble/ble.h @@ -35,7 +35,16 @@ static constexpr uint8_t MAX_BLE_QUEUE_SIZE = 100; // 64 + 36 (ring buffer size static constexpr uint8_t MAX_BLE_QUEUE_SIZE = 88; // 64 + 24 (ring buffer size without PSRAM) #endif -uint64_t ble_addr_to_uint64(const esp_bd_addr_t address); +inline uint64_t ble_addr_to_uint64(const esp_bd_addr_t address) { + uint64_t u = 0; + u |= uint64_t(address[0] & 0xFF) << 40; + u |= uint64_t(address[1] & 0xFF) << 32; + u |= uint64_t(address[2] & 0xFF) << 24; + u |= uint64_t(address[3] & 0xFF) << 16; + u |= uint64_t(address[4] & 0xFF) << 8; + u |= uint64_t(address[5] & 0xFF) << 0; + return u; +} // NOLINTNEXTLINE(modernize-use-using) typedef struct { @@ -52,6 +61,19 @@ enum IoCapability { IO_CAP_KBDISP = ESP_IO_CAP_KBDISP, }; +#ifdef ESPHOME_ESP32_BLE_EXTENDED_AUTH_PARAMS +enum AuthReqMode { + AUTH_REQ_NO_BOND = ESP_LE_AUTH_NO_BOND, + AUTH_REQ_BOND = ESP_LE_AUTH_BOND, + AUTH_REQ_MITM = ESP_LE_AUTH_REQ_MITM, + AUTH_REQ_BOND_MITM = ESP_LE_AUTH_REQ_BOND_MITM, + AUTH_REQ_SC_ONLY = ESP_LE_AUTH_REQ_SC_ONLY, + AUTH_REQ_SC_BOND = ESP_LE_AUTH_REQ_SC_BOND, + AUTH_REQ_SC_MITM = ESP_LE_AUTH_REQ_SC_MITM, + AUTH_REQ_SC_MITM_BOND = ESP_LE_AUTH_REQ_SC_MITM_BOND, +}; +#endif + enum BLEComponentState : uint8_t { /** Nothing has been initialized yet. */ BLE_COMPONENT_STATE_OFF = 0, @@ -65,41 +87,16 @@ enum BLEComponentState : uint8_t { BLE_COMPONENT_STATE_ACTIVE, }; -class GAPEventHandler { - public: - virtual void gap_event_handler(esp_gap_ble_cb_event_t event, esp_ble_gap_cb_param_t *param) = 0; -}; - -class GAPScanEventHandler { - public: - virtual void gap_scan_event_handler(const BLEScanResult &scan_result) = 0; -}; - -#ifdef USE_ESP32_BLE_CLIENT -class GATTcEventHandler { - public: - virtual void gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if, - esp_ble_gattc_cb_param_t *param) = 0; -}; -#endif - -#ifdef USE_ESP32_BLE_SERVER -class GATTsEventHandler { - public: - virtual void gatts_event_handler(esp_gatts_cb_event_t event, esp_gatt_if_t gatts_if, - esp_ble_gatts_cb_param_t *param) = 0; -}; -#endif - -class BLEStatusEventHandler { - public: - virtual void ble_before_disabled_event_handler() = 0; -}; - class ESP32BLE : public Component { public: void set_io_capability(IoCapability io_capability) { this->io_cap_ = (esp_ble_io_cap_t) io_capability; } +#ifdef ESPHOME_ESP32_BLE_EXTENDED_AUTH_PARAMS + void set_max_key_size(uint8_t key_size) { this->max_key_size_ = key_size; } + void set_min_key_size(uint8_t key_size) { this->min_key_size_ = key_size; } + void set_auth_req(AuthReqMode req) { this->auth_req_mode_ = (esp_ble_auth_req_t) req; } +#endif + void set_advertising_cycle_time(uint32_t advertising_cycle_time) { this->advertising_cycle_time_ = advertising_cycle_time; } @@ -107,7 +104,7 @@ class ESP32BLE : public Component { void enable(); void disable(); - bool is_active(); + ESPHOME_ALWAYS_INLINE bool is_active() { return this->state_ == BLE_COMPONENT_STATE_ACTIVE; } void setup() override; void loop() override; void dump_config() override; @@ -126,22 +123,28 @@ class ESP32BLE : public Component { #endif #ifdef ESPHOME_ESP32_BLE_GAP_EVENT_HANDLER_COUNT - void register_gap_event_handler(GAPEventHandler *handler) { this->gap_event_handlers_.push_back(handler); } + template void add_gap_event_callback(F &&callback) { + this->gap_event_callbacks_.add(std::forward(callback)); + } #endif #ifdef ESPHOME_ESP32_BLE_GAP_SCAN_EVENT_HANDLER_COUNT - void register_gap_scan_event_handler(GAPScanEventHandler *handler) { - this->gap_scan_event_handlers_.push_back(handler); + template void add_gap_scan_event_callback(F &&callback) { + this->gap_scan_event_callbacks_.add(std::forward(callback)); } #endif #if defined(USE_ESP32_BLE_CLIENT) && defined(ESPHOME_ESP32_BLE_GATTC_EVENT_HANDLER_COUNT) - void register_gattc_event_handler(GATTcEventHandler *handler) { this->gattc_event_handlers_.push_back(handler); } + template void add_gattc_event_callback(F &&callback) { + this->gattc_event_callbacks_.add(std::forward(callback)); + } #endif #if defined(USE_ESP32_BLE_SERVER) && defined(ESPHOME_ESP32_BLE_GATTS_EVENT_HANDLER_COUNT) - void register_gatts_event_handler(GATTsEventHandler *handler) { this->gatts_event_handlers_.push_back(handler); } + template void add_gatts_event_callback(F &&callback) { + this->gatts_event_callbacks_.add(std::forward(callback)); + } #endif #ifdef ESPHOME_ESP32_BLE_BLE_STATUS_EVENT_HANDLER_COUNT - void register_ble_status_event_handler(BLEStatusEventHandler *handler) { - this->ble_status_event_handlers_.push_back(handler); + template void add_ble_status_event_callback(F &&callback) { + this->ble_status_event_callbacks_.add(std::forward(callback)); } #endif void set_enable_on_boot(bool enable_on_boot) { this->enable_on_boot_ = enable_on_boot; } @@ -155,6 +158,10 @@ class ESP32BLE : public Component { #endif static void gap_event_handler(esp_gap_ble_cb_event_t event, esp_ble_gap_cb_param_t *param); + // Handle DISABLE and ENABLE transitions when not in the ACTIVE state. + // Other non-ACTIVE states (e.g. OFF, DISABLED) are currently treated as no-ops. + void __attribute__((noinline)) loop_handle_state_transition_not_active_(); + bool ble_setup_(); bool ble_dismantle_(); bool ble_pre_setup_(); @@ -170,26 +177,38 @@ class ESP32BLE : public Component { private: template friend void enqueue_ble_event(Args... args); - // Handler vectors - use StaticVector when counts are known at compile time #ifdef ESPHOME_ESP32_BLE_GAP_EVENT_HANDLER_COUNT - StaticVector gap_event_handlers_; + StaticCallbackManager + gap_event_callbacks_; #endif #ifdef ESPHOME_ESP32_BLE_GAP_SCAN_EVENT_HANDLER_COUNT - StaticVector gap_scan_event_handlers_; + StaticCallbackManager + gap_scan_event_callbacks_; #endif #if defined(USE_ESP32_BLE_CLIENT) && defined(ESPHOME_ESP32_BLE_GATTC_EVENT_HANDLER_COUNT) - StaticVector gattc_event_handlers_; + StaticCallbackManager + gattc_event_callbacks_; #endif #if defined(USE_ESP32_BLE_SERVER) && defined(ESPHOME_ESP32_BLE_GATTS_EVENT_HANDLER_COUNT) - StaticVector gatts_event_handlers_; + StaticCallbackManager + gatts_event_callbacks_; #endif #ifdef ESPHOME_ESP32_BLE_BLE_STATUS_EVENT_HANDLER_COUNT - StaticVector ble_status_event_handlers_; + StaticCallbackManager ble_status_event_callbacks_; #endif // Large objects (size depends on template parameters, but typically aligned to 4 bytes) esphome::LockFreeQueue ble_events_; - esphome::EventPool ble_event_pool_; + // Pool sized to queue capacity (SIZE-1) because LockFreeQueue is a ring + // buffer that holds N-1 elements (one slot distinguishes full from empty). + // This guarantees allocate() returns nullptr before push() can fail, which: + // 1. Prevents leaking a pool slot (the Nth allocate succeeds but push fails) + // 2. Avoids needing release() on the producer path after a failed push(), + // preserving the SPSC contract on the pool's internal free list + esphome::EventPool ble_event_pool_; // 4-byte aligned members #ifdef USE_ESP32_BLE_ADVERTISING @@ -205,6 +224,13 @@ class ESP32BLE : public Component { // 1-byte aligned members (grouped together to minimize padding) BLEComponentState state_{BLE_COMPONENT_STATE_OFF}; // 1 byte (uint8_t enum) bool enable_on_boot_{}; // 1 byte + +#ifdef ESPHOME_ESP32_BLE_EXTENDED_AUTH_PARAMS + optional auth_req_mode_; + + uint8_t max_key_size_{0}; // range is 7..16, 0 is unset + uint8_t min_key_size_{0}; // range is 7..16, 0 is unset +#endif }; // NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables) diff --git a/esphome/components/esp32_ble/ble_advertising.h b/esphome/components/esp32_ble/ble_advertising.h index d7f1eeac9d..3cfa6f548a 100644 --- a/esphome/components/esp32_ble/ble_advertising.h +++ b/esphome/components/esp32_ble/ble_advertising.h @@ -10,20 +10,11 @@ #ifdef USE_ESP32 #ifdef USE_ESP32_BLE_ADVERTISING -#ifndef CONFIG_ESP_HOSTED_ENABLE_BT_BLUEDROID -#include -#endif #include #include namespace esphome::esp32_ble { -using raw_adv_data_t = struct { - uint8_t *data; - size_t length; - esp_power_level_t power_level; -}; - class ESPBTUUID; class BLEAdvertising { diff --git a/esphome/components/esp32_ble/ble_event.h b/esphome/components/esp32_ble/ble_event.h index 299fd7705f..ba87fd8805 100644 --- a/esphome/components/esp32_ble/ble_event.h +++ b/esphome/components/esp32_ble/ble_event.h @@ -155,44 +155,38 @@ class BLEEvent { void release() { switch (this->type_) { case GAP: - // GAP events don't have heap allocations + // GAP events never have heap allocations break; case GATTC: - // Param is now stored inline, only delete heap data if it was heap-allocated if (!this->event_.gattc.is_inline && this->event_.gattc.data.heap_data != nullptr) { delete[] this->event_.gattc.data.heap_data; + this->event_.gattc.data.heap_data = nullptr; } - // Clear critical fields to prevent issues if type changes - this->event_.gattc.is_inline = false; - this->event_.gattc.data.heap_data = nullptr; break; case GATTS: - // Param is now stored inline, only delete heap data if it was heap-allocated if (!this->event_.gatts.is_inline && this->event_.gatts.data.heap_data != nullptr) { delete[] this->event_.gatts.data.heap_data; + this->event_.gatts.data.heap_data = nullptr; } - // Clear critical fields to prevent issues if type changes - this->event_.gatts.is_inline = false; - this->event_.gatts.data.heap_data = nullptr; break; } } // Load new event data for reuse (replaces previous event data) + // Note: release() is NOT called here because EventPool::release() already + // calls event->release() before returning to the free list. Every event + // from allocate() is already in a clean state. void load_gap_event(esp_gap_ble_cb_event_t e, esp_ble_gap_cb_param_t *p) { - this->release(); this->type_ = GAP; this->init_gap_data_(e, p); } void load_gattc_event(esp_gattc_cb_event_t e, esp_gatt_if_t i, esp_ble_gattc_cb_param_t *p) { - this->release(); this->type_ = GATTC; this->init_gattc_data_(e, i, p); } void load_gatts_event(esp_gatts_cb_event_t e, esp_gatt_if_t i, esp_ble_gatts_cb_param_t *p) { - this->release(); this->type_ = GATTS; this->init_gatts_data_(e, i, p); } diff --git a/esphome/components/esp32_ble/ble_uuid.h b/esphome/components/esp32_ble/ble_uuid.h index 6c8ef7bfd9..503fde6945 100644 --- a/esphome/components/esp32_ble/ble_uuid.h +++ b/esphome/components/esp32_ble/ble_uuid.h @@ -48,7 +48,7 @@ class ESPBTUUID { // Remove before 2026.8.0 ESPDEPRECATED("Use to_str() instead. Removed in 2026.8.0", "2026.2.0") - std::string to_string() const; + std::string to_string() const; // NOLINT const char *to_str(std::span output) const; protected: diff --git a/esphome/components/esp32_ble_beacon/__init__.py b/esphome/components/esp32_ble_beacon/__init__.py index ba5ae4331c..e2e790164e 100644 --- a/esphome/components/esp32_ble_beacon/__init__.py +++ b/esphome/components/esp32_ble_beacon/__init__.py @@ -13,7 +13,6 @@ esp32_ble_beacon_ns = cg.esphome_ns.namespace("esp32_ble_beacon") ESP32BLEBeacon = esp32_ble_beacon_ns.class_( "ESP32BLEBeacon", cg.Component, - esp32_ble.GAPEventHandler, cg.Parented.template(esp32_ble.ESP32BLE), ) CONF_MAJOR = "major" @@ -53,8 +52,10 @@ CONFIG_SCHEMA = cv.All( cv.Optional(CONF_MEASURED_POWER, default=-59): cv.int_range( min=-128, max=0 ), - cv.Optional(CONF_TX_POWER, default="3dBm"): cv.All( - cv.decibel, cv.enum(esp32_ble.TX_POWER_LEVELS, int=True) + cv.OnlyWithout(CONF_TX_POWER, "esp32_hosted", default="3dBm"): cv.All( + cv.conflicts_with_component("esp32_hosted"), + cv.decibel, + cv.enum(esp32_ble.TX_POWER_LEVELS, int=True), ), } ).extend(cv.COMPONENT_SCHEMA), @@ -82,7 +83,10 @@ async def to_code(config): cg.add(var.set_min_interval(config[CONF_MIN_INTERVAL])) cg.add(var.set_max_interval(config[CONF_MAX_INTERVAL])) cg.add(var.set_measured_power(config[CONF_MEASURED_POWER])) - cg.add(var.set_tx_power(config[CONF_TX_POWER])) + + # TX power control only available on native Bluetooth (not ESP-Hosted) + if CONF_TX_POWER in config: + cg.add(var.set_tx_power(config[CONF_TX_POWER])) cg.add_define("USE_ESP32_BLE_ADVERTISING") diff --git a/esphome/components/esp32_ble_beacon/esp32_ble_beacon.cpp b/esphome/components/esp32_ble_beacon/esp32_ble_beacon.cpp index f2aa7e762e..093273b399 100644 --- a/esphome/components/esp32_ble_beacon/esp32_ble_beacon.cpp +++ b/esphome/components/esp32_ble_beacon/esp32_ble_beacon.cpp @@ -36,11 +36,16 @@ void ESP32BLEBeacon::dump_config() { } } *bpos = '\0'; +#ifndef CONFIG_ESP_HOSTED_ENABLE_BT_BLUEDROID ESP_LOGCONFIG(TAG, " UUID: %s, Major: %u, Minor: %u, Min Interval: %ums, Max Interval: %ums, Measured Power: %d" ", TX Power: %ddBm", uuid, this->major_, this->minor_, this->min_interval_, this->max_interval_, this->measured_power_, (this->tx_power_ * 3) - 12); +#else + ESP_LOGCONFIG(TAG, " UUID: %s, Major: %u, Minor: %u, Min Interval: %ums, Max Interval: %ums, Measured Power: %d", + uuid, this->major_, this->minor_, this->min_interval_, this->max_interval_, this->measured_power_); +#endif } float ESP32BLEBeacon::get_setup_priority() const { return setup_priority::AFTER_BLUETOOTH; } @@ -74,11 +79,14 @@ void ESP32BLEBeacon::on_advertise_() { ibeacon_adv_data.ibeacon_vendor.major = byteswap(this->major_); ibeacon_adv_data.ibeacon_vendor.measured_power = static_cast(this->measured_power_); + esp_err_t err; +#ifndef CONFIG_ESP_HOSTED_ENABLE_BT_BLUEDROID ESP_LOGD(TAG, "Setting BLE TX power"); - esp_err_t err = esp_ble_tx_power_set(ESP_BLE_PWR_TYPE_ADV, this->tx_power_); + err = esp_ble_tx_power_set(ESP_BLE_PWR_TYPE_ADV, this->tx_power_); if (err != ESP_OK) { ESP_LOGW(TAG, "esp_ble_tx_power_set failed: %s", esp_err_to_name(err)); } +#endif err = esp_ble_gap_config_adv_data_raw((uint8_t *) &ibeacon_adv_data, sizeof(ibeacon_adv_data)); if (err != ESP_OK) { ESP_LOGE(TAG, "esp_ble_gap_config_adv_data_raw failed: %s", esp_err_to_name(err)); diff --git a/esphome/components/esp32_ble_beacon/esp32_ble_beacon.h b/esphome/components/esp32_ble_beacon/esp32_ble_beacon.h index 05afdc7379..e16c413179 100644 --- a/esphome/components/esp32_ble_beacon/esp32_ble_beacon.h +++ b/esphome/components/esp32_ble_beacon/esp32_ble_beacon.h @@ -35,7 +35,7 @@ using esp_ble_ibeacon_t = struct { using namespace esp32_ble; -class ESP32BLEBeacon : public Component, public GAPEventHandler, public Parented { +class ESP32BLEBeacon : public Component, public Parented { public: explicit ESP32BLEBeacon(const std::array &uuid) : uuid_(uuid) {} @@ -48,8 +48,10 @@ class ESP32BLEBeacon : public Component, public GAPEventHandler, public Parented void set_min_interval(uint16_t val) { this->min_interval_ = val; } void set_max_interval(uint16_t val) { this->max_interval_ = val; } void set_measured_power(int8_t val) { this->measured_power_ = val; } +#ifndef CONFIG_ESP_HOSTED_ENABLE_BT_BLUEDROID void set_tx_power(esp_power_level_t val) { this->tx_power_ = val; } - void gap_event_handler(esp_gap_ble_cb_event_t event, esp_ble_gap_cb_param_t *param) override; +#endif + void gap_event_handler(esp_gap_ble_cb_event_t event, esp_ble_gap_cb_param_t *param); protected: void on_advertise_(); @@ -60,7 +62,9 @@ class ESP32BLEBeacon : public Component, public GAPEventHandler, public Parented uint16_t min_interval_{}; uint16_t max_interval_{}; int8_t measured_power_{}; +#ifndef CONFIG_ESP_HOSTED_ENABLE_BT_BLUEDROID esp_power_level_t tx_power_{}; +#endif esp_ble_adv_params_t ble_adv_params_; bool advertising_{false}; }; diff --git a/esphome/components/esp32_ble_client/ble_client_base.cpp b/esphome/components/esp32_ble_client/ble_client_base.cpp index c464c89390..7f0f2c624d 100644 --- a/esphome/components/esp32_ble_client/ble_client_base.cpp +++ b/esphome/components/esp32_ble_client/ble_client_base.cpp @@ -16,17 +16,18 @@ static const char *const TAG = "esp32_ble_client"; // Intermediate connection parameters for standard operation // ESP-IDF defaults (12.5-15ms) are too slow for stable connections through WiFi-based BLE proxies, // causing disconnections. These medium parameters balance responsiveness with bandwidth usage. -static const uint16_t MEDIUM_MIN_CONN_INTERVAL = 0x07; // 7 * 1.25ms = 8.75ms -static const uint16_t MEDIUM_MAX_CONN_INTERVAL = 0x09; // 9 * 1.25ms = 11.25ms +static constexpr uint16_t MEDIUM_MIN_CONN_INTERVAL = 0x07; // 7 * 1.25ms = 8.75ms +static constexpr uint16_t MEDIUM_MAX_CONN_INTERVAL = 0x09; // 9 * 1.25ms = 11.25ms // The timeout value was increased from 6s to 8s to address stability issues observed // in certain BLE devices when operating through WiFi-based BLE proxies. The longer // timeout reduces the likelihood of disconnections during periods of high latency. -static const uint16_t MEDIUM_CONN_TIMEOUT = 800; // 800 * 10ms = 8s +static constexpr uint16_t MEDIUM_CONN_TIMEOUT = 800; // 800 * 10ms = 8s // Fastest connection parameters for devices with short discovery timeouts -static const uint16_t FAST_MIN_CONN_INTERVAL = 0x06; // 6 * 1.25ms = 7.5ms (BLE minimum) -static const uint16_t FAST_MAX_CONN_INTERVAL = 0x06; // 6 * 1.25ms = 7.5ms -static const uint16_t FAST_CONN_TIMEOUT = 1000; // 1000 * 10ms = 10s +static constexpr uint16_t FAST_MIN_CONN_INTERVAL = 0x06; // 6 * 1.25ms = 7.5ms (BLE minimum) +static constexpr uint16_t FAST_MAX_CONN_INTERVAL = 0x06; // 6 * 1.25ms = 7.5ms +static constexpr uint16_t FAST_CONN_TIMEOUT = 1000; // 1000 * 10ms = 10s +static constexpr uint32_t DISCONNECTING_TIMEOUT = 10000; // 10s static const esp_bt_uuid_t NOTIFY_DESC_UUID = { .len = ESP_UUID_LEN_16, .uuid = @@ -62,6 +63,15 @@ void BLEClientBase::loop() { // will enable it again when a connection is needed. else if (this->state() == espbt::ClientState::IDLE) { this->disable_loop(); + } else if (this->state() == espbt::ClientState::DISCONNECTING && + (millis() - this->disconnecting_started_) > DISCONNECTING_TIMEOUT) { + ESP_LOGE(TAG, "[%d] [%s] Timeout waiting for CLOSE_EVT after disconnect, forcing IDLE", this->connection_index_, + this->address_str_); + // release_services() must be called before set_idle_() — if we entered DISCONNECTING + // via unconditional_disconnect() (which doesn't call release_services()), and ESP-IDF + // never delivered CLOSE_EVT/DISCONNECT_EVT, services would leak without this call. + this->release_services(); + this->set_idle_(); } } @@ -101,12 +111,16 @@ bool BLEClientBase::parse_device(const espbt::ESPBTDevice &device) { #endif void BLEClientBase::connect() { - // Prevent duplicate connection attempts + // Prevent duplicate connection attempts or connecting while still disconnecting if (this->state() == espbt::ClientState::CONNECTING || this->state() == espbt::ClientState::CONNECTED || this->state() == espbt::ClientState::ESTABLISHED) { ESP_LOGW(TAG, "[%d] [%s] Connection already in progress, state=%s", this->connection_index_, this->address_str_, espbt::client_state_to_string(this->state())); return; + } else if (this->state() == espbt::ClientState::DISCONNECTING) { + ESP_LOGW(TAG, "[%d] [%s] Cannot connect, still waiting for CLOSE_EVT to complete disconnect", + this->connection_index_, this->address_str_); + return; } ESP_LOGI(TAG, "[%d] [%s] 0x%02x Connecting", this->connection_index_, this->address_str_, this->remote_addr_type_); this->paired_ = false; @@ -174,7 +188,7 @@ void BLEClientBase::unconditional_disconnect() { this->set_address(0); this->set_state(espbt::ClientState::IDLE); } else { - this->set_state(espbt::ClientState::DISCONNECTING); + this->set_disconnecting_(); } } @@ -220,6 +234,7 @@ void BLEClientBase::log_connection_params_(const char *param_type) { void BLEClientBase::handle_connection_result_(esp_err_t ret) { if (ret) { this->log_gattc_warning_("esp_ble_gattc_open", ret); + // Don't use set_idle_() here — CONNECT_EVT never fired so conn_id_ is still UNSET_CONN_ID. this->set_state(espbt::ClientState::IDLE); } } @@ -236,8 +251,8 @@ void BLEClientBase::log_warning_(const char *message) { ESP_LOGW(TAG, "[%d] [%s] %s", this->connection_index_, this->address_str_, message); } -void BLEClientBase::update_conn_params_(uint16_t min_interval, uint16_t max_interval, uint16_t latency, - uint16_t timeout, const char *param_type) { +esp_err_t BLEClientBase::update_conn_params_(uint16_t min_interval, uint16_t max_interval, uint16_t latency, + uint16_t timeout, const char *param_type) { esp_ble_conn_update_params_t conn_params = {{0}}; memcpy(conn_params.bda, this->remote_bda_, sizeof(esp_bd_addr_t)); conn_params.min_int = min_interval; @@ -249,6 +264,7 @@ void BLEClientBase::update_conn_params_(uint16_t min_interval, uint16_t max_inte if (err != ESP_OK) { this->log_gattc_warning_("esp_ble_gap_update_conn_params", err); } + return err; } void BLEClientBase::set_conn_params_(uint16_t min_interval, uint16_t max_interval, uint16_t latency, uint16_t timeout, @@ -310,15 +326,16 @@ bool BLEClientBase::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_ } if (param->open.status != ESP_GATT_OK && param->open.status != ESP_GATT_ALREADY_OPEN) { this->log_gattc_warning_("Connection open", param->open.status); - this->set_state(espbt::ClientState::IDLE); + // Connection was never established so CLOSE_EVT may not follow + this->set_idle_(); break; } if (this->want_disconnect_) { // Disconnect was requested after connecting started, // but before the connection was established. Now that we have // this->conn_id_ set, we can disconnect it. + // Don't reset conn_id_ here — CLOSE_EVT needs it to match and call set_idle_(). this->unconditional_disconnect(); - this->conn_id_ = UNSET_CONN_ID; break; } // MTU negotiation already started in ESP_GATTC_CONNECT_EVT @@ -333,7 +350,7 @@ bool BLEClientBase::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_ // For V3_WITHOUT_CACHE, we already set fast params before connecting // No need to update them again here this->log_event_("Searching for services"); - esp_ble_gattc_search_service(esp_gattc_if, param->cfg_mtu.conn_id, nullptr); + esp_ble_gattc_search_service(esp_gattc_if, param->open.conn_id, nullptr); break; } case ESP_GATTC_CONNECT_EVT: { @@ -362,8 +379,22 @@ bool BLEClientBase::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_ ESP_LOGD(TAG, "[%d] [%s] ESP_GATTC_DISCONNECT_EVT, reason 0x%02x", this->connection_index_, this->address_str_, param->disconnect.reason); } + // For active disconnects (esp_ble_gattc_close), CLOSE_EVT arrives before + // DISCONNECT_EVT. If CLOSE_EVT already transitioned us to IDLE, don't go + // backwards to DISCONNECTING — the connection is already fully cleaned up. + if (this->state() == espbt::ClientState::IDLE) { + this->log_event_("DISCONNECT_EVT after CLOSE_EVT, already IDLE"); + break; + } + // For passive disconnects (remote device disconnected or link lost), + // DISCONNECT_EVT arrives first. Don't transition to IDLE yet — wait for + // CLOSE_EVT to ensure the controller has fully freed resources (L2CAP + // channels, ATT resources, HCI connection handle). Transitioning to IDLE + // here would allow reconnection before cleanup is complete, causing the + // controller to reject the new connection (status=133) or crash with + // ASSERT_PARAM in lld_evt.c. this->release_services(); - this->set_state(espbt::ClientState::IDLE); + this->set_disconnecting_(); break; } @@ -386,8 +417,7 @@ bool BLEClientBase::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_ return false; this->log_gattc_lifecycle_event_("CLOSE"); this->release_services(); - this->set_state(espbt::ClientState::IDLE); - this->conn_id_ = UNSET_CONN_ID; + this->set_idle_(); break; } case ESP_GATTC_SEARCH_RES_EVT: { @@ -423,10 +453,8 @@ bool BLEClientBase::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_ for (auto &svc : this->services_) { char uuid_buf[espbt::UUID_STR_LEN]; svc->uuid.to_str(uuid_buf); - ESP_LOGV(TAG, - "[%d] [%s] Service UUID: %s\n" - "[%d] [%s] start_handle: 0x%x end_handle: 0x%x", - this->connection_index_, this->address_str_, uuid_buf, this->connection_index_, this->address_str_, + ESP_LOGV(TAG, "[%d] [%s] Service UUID: %s", this->connection_index_, this->address_str_, uuid_buf); + ESP_LOGV(TAG, "[%d] [%s] start_handle: 0x%x end_handle: 0x%x", this->connection_index_, this->address_str_, svc->start_handle, svc->end_handle); } #endif diff --git a/esphome/components/esp32_ble_client/ble_client_base.h b/esphome/components/esp32_ble_client/ble_client_base.h index c2336b2349..4e0b22cc29 100644 --- a/esphome/components/esp32_ble_client/ble_client_base.h +++ b/esphome/components/esp32_ble_client/ble_client_base.h @@ -113,11 +113,14 @@ class BLEClientBase : public espbt::ESPBTClient, public Component { char address_str_[MAC_ADDRESS_PRETTY_BUFFER_SIZE]{}; esp_bd_addr_t remote_bda_; // 6 bytes - // Group 5: 2-byte types + // Group 5: 4-byte types + uint32_t disconnecting_started_{0}; + + // Group 6: 2-byte types uint16_t conn_id_{UNSET_CONN_ID}; uint16_t mtu_{23}; - // Group 6: 1-byte types and small enums + // Group 7: 1-byte types and small enums esp_ble_addr_type_t remote_addr_type_{BLE_ADDR_TYPE_PUBLIC}; espbt::ConnectionType connection_type_{espbt::ConnectionType::V1}; uint8_t connection_index_; @@ -129,14 +132,24 @@ class BLEClientBase : public espbt::ESPBTClient, public Component { void log_event_(const char *name); void log_gattc_lifecycle_event_(const char *name); void log_gattc_data_event_(const char *name); - void update_conn_params_(uint16_t min_interval, uint16_t max_interval, uint16_t latency, uint16_t timeout, - const char *param_type); + esp_err_t update_conn_params_(uint16_t min_interval, uint16_t max_interval, uint16_t latency, uint16_t timeout, + const char *param_type); void set_conn_params_(uint16_t min_interval, uint16_t max_interval, uint16_t latency, uint16_t timeout, const char *param_type); void log_gattc_warning_(const char *operation, esp_gatt_status_t status); void log_gattc_warning_(const char *operation, esp_err_t err); void log_connection_params_(const char *param_type); void handle_connection_result_(esp_err_t ret); + /// Transition to IDLE and reset conn_id — call when the connection is fully dead. + void set_idle_() { + this->set_state(espbt::ClientState::IDLE); + this->conn_id_ = UNSET_CONN_ID; + } + /// Transition to DISCONNECTING and start the safety timeout. + void set_disconnecting_() { + this->disconnecting_started_ = millis(); + this->set_state(espbt::ClientState::DISCONNECTING); + } // Compact error logging helpers to reduce flash usage void log_error_(const char *message); void log_error_(const char *message, int code); diff --git a/esphome/components/esp32_ble_server/__init__.py b/esphome/components/esp32_ble_server/__init__.py index a7e2522fac..57106cd93b 100644 --- a/esphome/components/esp32_ble_server/__init__.py +++ b/esphome/components/esp32_ble_server/__init__.py @@ -24,6 +24,7 @@ from esphome.const import ( __version__ as ESPHOME_VERSION, ) from esphome.core import CORE +import esphome.final_validate as fv from esphome.schema_extractors import SCHEMA_EXTRACT AUTO_LOAD = ["esp32_ble", "bytebuffer"] @@ -42,6 +43,7 @@ CONF_FIRMWARE_VERSION = "firmware_version" CONF_INDICATE = "indicate" CONF_MANUFACTURER = "manufacturer" CONF_MANUFACTURER_DATA = "manufacturer_data" +CONF_MAX_CLIENTS = "max_clients" CONF_ON_WRITE = "on_write" CONF_READ = "read" CONF_STRING = "string" @@ -70,7 +72,6 @@ BLECharacteristic_ns = esp32_ble_server_ns.namespace("BLECharacteristic") BLEServer = esp32_ble_server_ns.class_( "BLEServer", cg.Component, - esp32_ble.GATTsEventHandler, cg.Parented.template(esp32_ble.ESP32BLE), ) esp32_ble_server_automations_ns = esp32_ble_server_ns.namespace( @@ -287,6 +288,22 @@ def create_device_information_service(config): def final_validate_config(config): + # Validate max_clients does not exceed esp32_ble max_connections + max_clients = config[CONF_MAX_CLIENTS] + if max_clients > 1: + full_config = fv.full_config.get() + ble_config = full_config.get("esp32_ble", {}) + max_connections = ble_config.get( + "max_connections", esp32_ble.DEFAULT_MAX_CONNECTIONS + ) + if max_clients > max_connections: + raise cv.Invalid( + f"'max_clients' ({max_clients}) cannot exceed esp32_ble " + f"'max_connections' ({max_connections}). " + f"Please set 'max_connections: {max_clients}' in the " + f"'esp32_ble' component." + ) + # Check if all characteristics that require notifications have the notify property set for char_id in CORE.data.get(DOMAIN, {}).get(KEY_NOTIFY_REQUIRED, set()): # Look for the characteristic in the configuration @@ -428,6 +445,7 @@ CONFIG_SCHEMA = cv.Schema( cv.Optional(CONF_MODEL): value_schema("string", templatable=False), cv.Optional(CONF_FIRMWARE_VERSION): value_schema("string", templatable=False), cv.Optional(CONF_MANUFACTURER_DATA): cv.Schema([cv.uint8_t]), + cv.Optional(CONF_MAX_CLIENTS, default=1): cv.int_range(min=1, max=9), cv.Optional(CONF_SERVICES, default=[]): cv.ensure_list(SERVICE_SCHEMA), cv.Optional(CONF_ON_CONNECT): automation.validate_automation(single=True), cv.Optional(CONF_ON_DISCONNECT): automation.validate_automation(single=True), @@ -527,7 +545,7 @@ async def to_code_characteristic(service_var, char_conf): action_conf, char_conf[CONF_CHAR_VALUE_ACTION_ID_], cg.TemplateArguments(), - {}, + [], ) cg.add(value_action.play()) else: @@ -552,6 +570,7 @@ async def to_code(config): esp32_ble.register_ble_status_event_handler(parent, var) cg.add(var.set_parent(parent)) cg.add(parent.advertising_set_appearance(config[CONF_APPEARANCE])) + cg.add(var.set_max_clients(config[CONF_MAX_CLIENTS])) if CONF_MANUFACTURER_DATA in config: cg.add(var.set_manufacturer_data(config[CONF_MANUFACTURER_DATA])) for service_config in config[CONF_SERVICES]: @@ -602,6 +621,7 @@ async def to_code(config): ), validate_set_value_action, ), + synchronous=True, ) async def ble_server_characteristic_set_value(config, action_id, template_arg, args): paren = await cg.get_variable(config[CONF_ID]) @@ -621,6 +641,7 @@ async def ble_server_characteristic_set_value(config, action_id, template_arg, a cv.Required(CONF_VALUE): value_schema(), } ), + synchronous=True, ) async def ble_server_descriptor_set_value(config, action_id, template_arg, args): paren = await cg.get_variable(config[CONF_ID]) @@ -642,6 +663,7 @@ async def ble_server_descriptor_set_value(config, action_id, template_arg, args) ), validate_notify_action, ), + synchronous=True, ) async def ble_server_characteristic_notify(config, action_id, template_arg, args): paren = await cg.get_variable(config[CONF_ID]) diff --git a/esphome/components/esp32_ble_server/ble_characteristic.cpp b/esphome/components/esp32_ble_server/ble_characteristic.cpp index 0482848ea0..aa82b773ba 100644 --- a/esphome/components/esp32_ble_server/ble_characteristic.cpp +++ b/esphome/components/esp32_ble_server/ble_characteristic.cpp @@ -16,13 +16,9 @@ BLECharacteristic::~BLECharacteristic() { for (auto *descriptor : this->descriptors_) { delete descriptor; // NOLINT(cppcoreguidelines-owning-memory) } - vSemaphoreDelete(this->set_value_lock_); } BLECharacteristic::BLECharacteristic(const ESPBTUUID uuid, uint32_t properties) : uuid_(uuid) { - this->set_value_lock_ = xSemaphoreCreateBinary(); - xSemaphoreGive(this->set_value_lock_); - this->properties_ = (esp_gatt_char_prop_t) 0; this->set_broadcast_property((properties & PROPERTY_BROADCAST) != 0); @@ -35,11 +31,7 @@ BLECharacteristic::BLECharacteristic(const ESPBTUUID uuid, uint32_t properties) void BLECharacteristic::set_value(ByteBuffer buffer) { this->set_value(buffer.get_data()); } -void BLECharacteristic::set_value(std::vector &&buffer) { - xSemaphoreTake(this->set_value_lock_, 0L); - this->value_ = std::move(buffer); - xSemaphoreGive(this->set_value_lock_); -} +void BLECharacteristic::set_value(std::vector &&buffer) { this->value_ = std::move(buffer); } void BLECharacteristic::set_value(std::initializer_list data) { this->set_value(std::vector(data)); // Delegate to move overload @@ -209,7 +201,11 @@ void BLECharacteristic::gatts_event_handler(esp_gatts_cb_event_t event, esp_gatt esp_gatt_rsp_t response; if (param->read.is_long) { - if (this->value_.size() - this->value_read_offset_ < max_offset) { + if (this->value_read_offset_ >= this->value_.size()) { + response.attr_value.len = 0; + response.attr_value.offset = this->value_read_offset_; + this->value_read_offset_ = 0; + } else if (this->value_.size() - this->value_read_offset_ < max_offset) { // Last message in the chain response.attr_value.len = this->value_.size() - this->value_read_offset_; response.attr_value.offset = this->value_read_offset_; @@ -246,9 +242,27 @@ void BLECharacteristic::gatts_event_handler(esp_gatts_cb_event_t event, esp_gatt if (this->handle_ != param->write.handle) break; + esp_gatt_status_t status = ESP_GATT_OK; + if (param->write.is_prep) { - this->value_.insert(this->value_.end(), param->write.value, param->write.value + param->write.len); - this->write_event_ = true; + const size_t offset = param->write.offset; + const size_t write_len = param->write.len; + const size_t new_size = offset + write_len; + // Clean the buffer on the first prepared write event + if (offset == 0) { + this->value_.clear(); + } + + if (offset != this->value_.size()) { + status = ESP_GATT_INVALID_OFFSET; + } else if (new_size > ESP_GATT_MAX_ATTR_LEN) { + status = ESP_GATT_INVALID_ATTR_LEN; + } else { + if (this->value_.size() < new_size) { + this->value_.resize(new_size); + } + memcpy(this->value_.data() + offset, param->write.value, write_len); + } } else { this->set_value(ByteBuffer::wrap(param->write.value, param->write.len)); } @@ -263,7 +277,7 @@ void BLECharacteristic::gatts_event_handler(esp_gatts_cb_event_t event, esp_gatt memcpy(response.attr_value.value, param->write.value, param->write.len); esp_err_t err = - esp_ble_gatts_send_response(gatts_if, param->write.conn_id, param->write.trans_id, ESP_GATT_OK, &response); + esp_ble_gatts_send_response(gatts_if, param->write.conn_id, param->write.trans_id, status, &response); if (err != ESP_OK) { ESP_LOGE(TAG, "esp_ble_gatts_send_response failed: %d", err); @@ -280,16 +294,16 @@ void BLECharacteristic::gatts_event_handler(esp_gatts_cb_event_t event, esp_gatt } case ESP_GATTS_EXEC_WRITE_EVT: { - if (!this->write_event_) + // BLE stack will guarantee that ESP_GATTS_EXEC_WRITE_EVT is only received after prepared writes + if (this->value_.empty()) break; - this->write_event_ = false; if (param->exec_write.exec_write_flag == ESP_GATT_PREP_WRITE_EXEC) { if (this->on_write_callback_) { (*this->on_write_callback_)(this->value_, param->exec_write.conn_id); } } - esp_err_t err = - esp_ble_gatts_send_response(gatts_if, param->write.conn_id, param->write.trans_id, ESP_GATT_OK, nullptr); + esp_err_t err = esp_ble_gatts_send_response(gatts_if, param->exec_write.conn_id, param->exec_write.trans_id, + ESP_GATT_OK, nullptr); if (err != ESP_OK) { ESP_LOGE(TAG, "esp_ble_gatts_send_response failed: %d", err); } diff --git a/esphome/components/esp32_ble_server/ble_characteristic.h b/esphome/components/esp32_ble_server/ble_characteristic.h index b913915789..062052cdf8 100644 --- a/esphome/components/esp32_ble_server/ble_characteristic.h +++ b/esphome/components/esp32_ble_server/ble_characteristic.h @@ -16,8 +16,6 @@ #include #include #include -#include -#include namespace esphome { namespace esp32_ble_server { @@ -57,12 +55,12 @@ class BLECharacteristic { ESPBTUUID get_uuid() { return this->uuid_; } std::vector &get_value() { return this->value_; } - static const uint32_t PROPERTY_READ = 1 << 0; - static const uint32_t PROPERTY_WRITE = 1 << 1; - static const uint32_t PROPERTY_NOTIFY = 1 << 2; - static const uint32_t PROPERTY_BROADCAST = 1 << 3; - static const uint32_t PROPERTY_INDICATE = 1 << 4; - static const uint32_t PROPERTY_WRITE_NR = 1 << 5; + static constexpr uint32_t PROPERTY_READ = 1 << 0; + static constexpr uint32_t PROPERTY_WRITE = 1 << 1; + static constexpr uint32_t PROPERTY_NOTIFY = 1 << 2; + static constexpr uint32_t PROPERTY_BROADCAST = 1 << 3; + static constexpr uint32_t PROPERTY_INDICATE = 1 << 4; + static constexpr uint32_t PROPERTY_WRITE_NR = 1 << 5; bool is_created(); bool is_failed(); @@ -77,7 +75,6 @@ class BLECharacteristic { } protected: - bool write_event_{false}; BLEService *service_{}; ESPBTUUID uuid_; esp_gatt_char_prop_t properties_; @@ -85,8 +82,6 @@ class BLECharacteristic { uint16_t value_read_offset_{0}; std::vector value_; - SemaphoreHandle_t set_value_lock_; - std::vector descriptors_; struct ClientNotificationEntry { diff --git a/esphome/components/esp32_ble_server/ble_server.cpp b/esphome/components/esp32_ble_server/ble_server.cpp index 2c13a8ac36..be0691dc06 100644 --- a/esphome/components/esp32_ble_server/ble_server.cpp +++ b/esphome/components/esp32_ble_server/ble_server.cpp @@ -38,21 +38,17 @@ void BLEServer::loop() { case RUNNING: { // Start all services that are pending to start if (!this->services_to_start_.empty()) { - uint16_t index_to_remove = 0; - // Iterate over the services to start - for (unsigned i = 0; i < this->services_to_start_.size(); i++) { - BLEService *service = this->services_to_start_[i]; + size_t write_idx = 0; + for (auto *service : this->services_to_start_) { if (service->is_created()) { service->start(); // Needs to be called once per characteristic in the service - } else { - index_to_remove = i + 1; + } + // Remove services that have started or are starting + if (!service->is_starting() && !service->is_running()) { + this->services_to_start_[write_idx++] = service; } } - // Remove the services that have been started - if (index_to_remove > 0) { - this->services_to_start_.erase(this->services_to_start_.begin(), - this->services_to_start_.begin() + index_to_remove - 1); - } + this->services_to_start_.erase(this->services_to_start_.begin() + write_idx, this->services_to_start_.end()); } break; } @@ -95,8 +91,6 @@ void BLEServer::loop() { } } -bool BLEServer::is_running() { return this->parent_->is_active() && this->state_ == RUNNING; } - bool BLEServer::can_proceed() { return this->is_running() || !this->parent_->is_active(); } void BLEServer::restart_advertising_() { @@ -175,6 +169,10 @@ void BLEServer::gatts_event_handler(esp_gatts_cb_event_t event, esp_gatt_if_t ga case ESP_GATTS_CONNECT_EVT: { ESP_LOGD(TAG, "BLE Client connected"); this->add_client_(param->connect.conn_id); + // Resume advertising so additional clients can discover and connect + if (this->client_count_ < this->max_clients_) { + this->parent_->advertising_start(); + } this->dispatch_callbacks_(CallbackType::ON_CONNECT, param->connect.conn_id); break; } @@ -241,7 +239,12 @@ void BLEServer::ble_before_disabled_event_handler() { float BLEServer::get_setup_priority() const { return setup_priority::AFTER_BLUETOOTH + 10; } -void BLEServer::dump_config() { ESP_LOGCONFIG(TAG, "ESP32 BLE Server:"); } +void BLEServer::dump_config() { + ESP_LOGCONFIG(TAG, + "ESP32 BLE Server:\n" + " Max clients: %u", + this->max_clients_); +} BLEServer *global_ble_server = nullptr; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) diff --git a/esphome/components/esp32_ble_server/ble_server.h b/esphome/components/esp32_ble_server/ble_server.h index 6fa86dd67f..9708ed40c8 100644 --- a/esphome/components/esp32_ble_server/ble_server.h +++ b/esphome/components/esp32_ble_server/ble_server.h @@ -24,7 +24,7 @@ namespace esp32_ble_server { using namespace esp32_ble; using namespace bytebuffer; -class BLEServer : public Component, public GATTsEventHandler, public BLEStatusEventHandler, public Parented { +class BLEServer : public Component, public Parented { public: void setup() override; void loop() override; @@ -32,13 +32,16 @@ class BLEServer : public Component, public GATTsEventHandler, public BLEStatusEv float get_setup_priority() const override; bool can_proceed() override; - bool is_running(); + ESPHOME_ALWAYS_INLINE bool is_running() { return this->parent_->is_active() && this->state_ == RUNNING; } void set_manufacturer_data(const std::vector &data) { this->manufacturer_data_ = data; this->restart_advertising_(); } + void set_max_clients(uint8_t max_clients) { this->max_clients_ = max_clients; } + uint8_t get_max_clients() const { return this->max_clients_; } + BLEService *create_service(ESPBTUUID uuid, bool advertise = false, uint16_t num_handles = 15); void remove_service(ESPBTUUID uuid, uint8_t inst_id = 0); BLEService *get_service(ESPBTUUID uuid, uint8_t inst_id = 0); @@ -50,10 +53,9 @@ class BLEServer : public Component, public GATTsEventHandler, public BLEStatusEv const uint16_t *get_clients() const { return this->clients_; } uint8_t get_client_count() const { return this->client_count_; } - void gatts_event_handler(esp_gatts_cb_event_t event, esp_gatt_if_t gatts_if, - esp_ble_gatts_cb_param_t *param) override; + void gatts_event_handler(esp_gatts_cb_event_t event, esp_gatt_if_t gatts_if, esp_ble_gatts_cb_param_t *param); - void ble_before_disabled_event_handler() override; + void ble_before_disabled_event_handler(); // Direct callback registration - supports multiple callbacks void on_connect(std::function &&callback) { @@ -95,6 +97,7 @@ class BLEServer : public Component, public GATTsEventHandler, public BLEStatusEv uint16_t clients_[USE_ESP32_BLE_MAX_CONNECTIONS]{}; uint8_t client_count_{0}; + uint8_t max_clients_{1}; std::vector services_{}; std::vector services_to_start_{}; BLEService *device_information_service_{}; diff --git a/esphome/components/esp32_ble_server/ble_server_automations.h b/esphome/components/esp32_ble_server/ble_server_automations.h index fe18600280..0bbfdffd5b 100644 --- a/esphome/components/esp32_ble_server/ble_server_automations.h +++ b/esphome/components/esp32_ble_server/ble_server_automations.h @@ -70,6 +70,7 @@ template class BLECharacteristicSetValueAction : public Action, buffer) + void set_buffer(std::initializer_list buffer) { this->buffer_ = std::vector(buffer); } void set_buffer(ByteBuffer buffer) { this->set_buffer(buffer.get_data()); } void play(const Ts &...x) override { // If the listener is already set, do nothing @@ -115,6 +116,7 @@ template class BLEDescriptorSetValueAction : public Action, buffer) + void set_buffer(std::initializer_list buffer) { this->buffer_ = std::vector(buffer); } void set_buffer(ByteBuffer buffer) { this->set_buffer(buffer.get_data()); } void play(const Ts &...x) override { this->parent_->set_value(this->buffer_.value(x...)); } diff --git a/esphome/components/esp32_ble_server/ble_service.cpp b/esphome/components/esp32_ble_server/ble_service.cpp index 96fedf2346..8956c87b3e 100644 --- a/esphome/components/esp32_ble_server/ble_service.cpp +++ b/esphome/components/esp32_ble_server/ble_service.cpp @@ -159,7 +159,7 @@ void BLEService::gatts_event_handler(esp_gatts_cb_event_t event, esp_gatt_if_t g break; } case ESP_GATTS_STOP_EVT: { - if (param->start.service_handle == this->handle_) { + if (param->stop.service_handle == this->handle_) { this->state_ = STOPPED; } break; diff --git a/esphome/components/esp32_ble_tracker/__init__.py b/esphome/components/esp32_ble_tracker/__init__.py index 37e74672ed..b9c4c28ccf 100644 --- a/esphome/components/esp32_ble_tracker/__init__.py +++ b/esphome/components/esp32_ble_tracker/__init__.py @@ -90,8 +90,6 @@ esp32_ble_tracker_ns = cg.esphome_ns.namespace("esp32_ble_tracker") ESP32BLETracker = esp32_ble_tracker_ns.class_( "ESP32BLETracker", cg.Component, - esp32_ble.GAPEventHandler, - esp32_ble.GATTcEventHandler, cg.Parented.template(esp32_ble.ESP32BLE), ) ESPBTClient = esp32_ble_tracker_ns.class_("ESPBTClient") @@ -373,6 +371,7 @@ ESP32_BLE_START_SCAN_ACTION_SCHEMA = cv.Schema( "esp32_ble_tracker.start_scan", ESP32BLEStartScanAction, ESP32_BLE_START_SCAN_ACTION_SCHEMA, + synchronous=True, ) async def esp32_ble_tracker_start_scan_action_to_code( config, action_id, template_arg, args @@ -396,6 +395,7 @@ ESP32_BLE_STOP_SCAN_ACTION_SCHEMA = automation.maybe_simple_id( "esp32_ble_tracker.stop_scan", ESP32BLEStopScanAction, ESP32_BLE_STOP_SCAN_ACTION_SCHEMA, + synchronous=True, ) async def esp32_ble_tracker_stop_scan_action_to_code( config, action_id, template_arg, args diff --git a/esphome/components/esp32_ble_tracker/esp32_ble_tracker.cpp b/esphome/components/esp32_ble_tracker/esp32_ble_tracker.cpp index 73a298d279..c7f2319d69 100644 --- a/esphome/components/esp32_ble_tracker/esp32_ble_tracker.cpp +++ b/esphome/components/esp32_ble_tracker/esp32_ble_tracker.cpp @@ -27,8 +27,14 @@ #include #endif +#ifdef USE_ESP32_BLE_DEVICE +#ifdef USE_BLE_TRACKER_PSA_AES +#include +#else #define MBEDTLS_AES_ALT #include +#endif +#endif // USE_ESP32_BLE_DEVICE // bt_trace.h #undef TAG @@ -82,12 +88,18 @@ void ESP32BLETracker::setup() { #ifdef USE_OTA_STATE_LISTENER void ESP32BLETracker::on_ota_global_state(ota::OTAState state, float progress, uint8_t error, ota::OTAComponent *comp) { if (state == ota::OTA_STARTED) { + this->scan_continuous_before_ota_ = this->scan_continuous_; this->stop_scan(); #ifdef ESPHOME_ESP32_BLE_TRACKER_CLIENT_COUNT for (auto *client : this->clients_) { client->disconnect(); } #endif + } else if ((state == ota::OTA_ERROR || state == ota::OTA_ABORT) && this->scan_continuous_before_ota_) { + this->scan_continuous_before_ota_ = false; + this->scan_continuous_ = true; + // Do not restart scanning immediately here; allow loop() to + // safely restart scanning once the scanner and all clients are idle. } } #endif @@ -237,7 +249,7 @@ void ESP32BLETracker::start_scan_(bool first) { return; } this->set_scanner_state_(ScannerState::STARTING); - ESP_LOGD(TAG, "Starting scan, set scanner state to STARTING."); + ESP_LOGV(TAG, "Starting scan, set scanner state to STARTING."); if (!first) { #ifdef ESPHOME_ESP32_BLE_TRACKER_LISTENER_COUNT for (auto *listener : this->listeners_) @@ -514,6 +526,11 @@ void ESPBTDevice::parse_adv_(const uint8_t *payload, uint8_t len) { continue; // Possible zero padded advertisement data } + // Validate field fits in remaining payload + if (offset + field_length > len) { + break; + } + // first byte of adv record is adv record type const uint8_t record_type = payload[offset++]; const uint8_t *record = &payload[offset]; @@ -544,7 +561,7 @@ void ESPBTDevice::parse_adv_(const uint8_t *payload, uint8_t len) { // CSS 1.5 TX POWER LEVEL // "The TX Power Level data type indicates the transmitted power level of the packet containing the data type." // CSS 1: Optional in this context (may appear more than once in a block). - this->tx_powers_.push_back(*payload); + this->tx_powers_.push_back(*record); break; } case ESP_BLE_AD_TYPE_APPEARANCE: { @@ -733,23 +750,48 @@ void ESP32BLETracker::print_bt_device_info(const ESPBTDevice &device) { } bool ESPBTDevice::resolve_irk(const uint8_t *irk) const { - uint8_t ecb_key[16]; - uint8_t ecb_plaintext[16]; - uint8_t ecb_ciphertext[16]; + static constexpr size_t AES_BLOCK_SIZE = 16; + static constexpr size_t AES_KEY_BITS = 128; + + uint8_t ecb_key[AES_BLOCK_SIZE]; + uint8_t ecb_plaintext[AES_BLOCK_SIZE]; + uint8_t ecb_ciphertext[AES_BLOCK_SIZE]; uint64_t addr64 = esp32_ble::ble_addr_to_uint64(this->address_); - memcpy(&ecb_key, irk, 16); - memset(&ecb_plaintext, 0, 16); + memcpy(&ecb_key, irk, AES_BLOCK_SIZE); + memset(&ecb_plaintext, 0, AES_BLOCK_SIZE); ecb_plaintext[13] = (addr64 >> 40) & 0xff; ecb_plaintext[14] = (addr64 >> 32) & 0xff; ecb_plaintext[15] = (addr64 >> 24) & 0xff; +#ifdef USE_BLE_TRACKER_PSA_AES + // Use PSA Crypto API (mbedtls 4.0 / IDF 6.0+) + psa_key_attributes_t attributes = PSA_KEY_ATTRIBUTES_INIT; + psa_set_key_type(&attributes, PSA_KEY_TYPE_AES); + psa_set_key_bits(&attributes, AES_KEY_BITS); + psa_set_key_usage_flags(&attributes, PSA_KEY_USAGE_ENCRYPT); + psa_set_key_algorithm(&attributes, PSA_ALG_ECB_NO_PADDING); + + mbedtls_svc_key_id_t key_id; + if (psa_import_key(&attributes, ecb_key, AES_BLOCK_SIZE, &key_id) != PSA_SUCCESS) { + return false; + } + + size_t output_length; + psa_status_t status = psa_cipher_encrypt(key_id, PSA_ALG_ECB_NO_PADDING, ecb_plaintext, AES_BLOCK_SIZE, + ecb_ciphertext, AES_BLOCK_SIZE, &output_length); + psa_destroy_key(key_id); + if (status != PSA_SUCCESS || output_length != AES_BLOCK_SIZE) { + return false; + } +#else + // Use legacy mbedtls AES API (IDF < 6.0) mbedtls_aes_context ctx = {0, 0, {0}}; mbedtls_aes_init(&ctx); - if (mbedtls_aes_setkey_enc(&ctx, ecb_key, 128) != 0) { + if (mbedtls_aes_setkey_enc(&ctx, ecb_key, AES_KEY_BITS) != 0) { mbedtls_aes_free(&ctx); return false; } @@ -760,6 +802,7 @@ bool ESPBTDevice::resolve_irk(const uint8_t *irk) const { } mbedtls_aes_free(&ctx); +#endif return ecb_ciphertext[15] == (addr64 & 0xff) && ecb_ciphertext[14] == ((addr64 >> 8) & 0xff) && ecb_ciphertext[13] == ((addr64 >> 16) & 0xff); @@ -812,7 +855,7 @@ void ESP32BLETracker::process_scan_result_(const BLEScanResult &scan_result) { } void ESP32BLETracker::cleanup_scan_state_(bool is_stop_complete) { - ESP_LOGD(TAG, "Scan %scomplete, set scanner state to IDLE.", is_stop_complete ? "stop " : ""); + ESP_LOGV(TAG, "Scan %scomplete, set scanner state to IDLE.", is_stop_complete ? "stop " : ""); #ifdef USE_ESP32_BLE_DEVICE this->already_discovered_.clear(); #endif diff --git a/esphome/components/esp32_ble_tracker/esp32_ble_tracker.h b/esphome/components/esp32_ble_tracker/esp32_ble_tracker.h index fa0cdb6f45..43405b02b7 100644 --- a/esphome/components/esp32_ble_tracker/esp32_ble_tracker.h +++ b/esphome/components/esp32_ble_tracker/esp32_ble_tracker.h @@ -12,6 +12,13 @@ #ifdef USE_ESP32 +#include +#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(6, 0, 0) +// mbedtls 4.0 (IDF 6.0) removed the legacy mbedtls AES API. +// Use the PSA Crypto API instead. +#define USE_BLE_TRACKER_PSA_AES +#endif + #include #include #include @@ -107,7 +114,7 @@ class ESPBTDevice { for (auto &it : this->manufacturer_datas_) { auto res = ESPBLEiBeacon::from_manufacturer_data(it); if (res.has_value()) - return *res; + return res; } return {}; } @@ -284,10 +291,6 @@ class ESPBTClient : public ESPBTDeviceListener { }; class ESP32BLETracker : public Component, - public GAPEventHandler, - public GAPScanEventHandler, - public GATTcEventHandler, - public BLEStatusEventHandler, #ifdef USE_OTA_STATE_LISTENER public ota::OTAGlobalStateListener, #endif @@ -318,11 +321,10 @@ class ESP32BLETracker : public Component, void start_scan(); void stop_scan(); - void gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if, - esp_ble_gattc_cb_param_t *param) override; - void gap_event_handler(esp_gap_ble_cb_event_t event, esp_ble_gap_cb_param_t *param) override; - void gap_scan_event_handler(const BLEScanResult &scan_result) override; - void ble_before_disabled_event_handler() override; + void gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if, esp_ble_gattc_cb_param_t *param); + void gap_event_handler(esp_gap_ble_cb_event_t event, esp_ble_gap_cb_param_t *param); + void gap_scan_event_handler(const BLEScanResult &scan_result); + void ble_before_disabled_event_handler(); #ifdef USE_OTA_STATE_LISTENER void on_ota_global_state(ota::OTAState state, float progress, uint8_t error, ota::OTAComponent *comp) override; @@ -429,6 +431,9 @@ class ESP32BLETracker : public Component, ScannerState scanner_state_{ScannerState::IDLE}; bool scan_continuous_; bool scan_active_; +#ifdef USE_OTA_STATE_LISTENER + bool scan_continuous_before_ota_{false}; +#endif bool ble_was_disabled_{true}; bool raw_advertisements_{false}; bool parse_advertisements_{false}; diff --git a/esphome/components/esp32_camera/__init__.py b/esphome/components/esp32_camera/__init__.py index db6244fb3f..66af321e4e 100644 --- a/esphome/components/esp32_camera/__init__.py +++ b/esphome/components/esp32_camera/__init__.py @@ -22,8 +22,10 @@ from esphome.const import ( CONF_TRIGGER_ID, CONF_VSYNC_PIN, ) +from esphome.core import CORE from esphome.core.entity_helpers import setup_entity import esphome.final_validate as fv +from esphome.types import ConfigType _LOGGER = logging.getLogger(__name__) @@ -84,6 +86,18 @@ FRAME_SIZES = { "2560X1920": ESP32CameraFrameSize.ESP32_CAMERA_SIZE_2560X1920, "QSXGA": ESP32CameraFrameSize.ESP32_CAMERA_SIZE_2560X1920, } +ESP32CameraPixelFormat = esp32_camera_ns.enum("ESP32CameraPixelFormat") +PIXEL_FORMATS = { + "RGB565": ESP32CameraPixelFormat.ESP32_PIXEL_FORMAT_RGB565, + "YUV422": ESP32CameraPixelFormat.ESP32_PIXEL_FORMAT_YUV422, + "YUV420": ESP32CameraPixelFormat.ESP32_PIXEL_FORMAT_YUV420, + "GRAYSCALE": ESP32CameraPixelFormat.ESP32_PIXEL_FORMAT_GRAYSCALE, + "JPEG": ESP32CameraPixelFormat.ESP32_PIXEL_FORMAT_JPEG, + "RGB888": ESP32CameraPixelFormat.ESP32_PIXEL_FORMAT_RGB888, + "RAW": ESP32CameraPixelFormat.ESP32_PIXEL_FORMAT_RAW, + "RGB444": ESP32CameraPixelFormat.ESP32_PIXEL_FORMAT_RGB444, + "RGB555": ESP32CameraPixelFormat.ESP32_PIXEL_FORMAT_RGB555, +} ESP32GainControlMode = esp32_camera_ns.enum("ESP32GainControlMode") ENUM_GAIN_CONTROL_MODE = { "MANUAL": ESP32GainControlMode.ESP32_GC_MODE_MANU, @@ -131,6 +145,7 @@ CONF_EXTERNAL_CLOCK = "external_clock" CONF_I2C_PINS = "i2c_pins" CONF_POWER_DOWN_PIN = "power_down_pin" # image +CONF_PIXEL_FORMAT = "pixel_format" CONF_JPEG_QUALITY = "jpeg_quality" CONF_VERTICAL_FLIP = "vertical_flip" CONF_HORIZONTAL_MIRROR = "horizontal_mirror" @@ -171,6 +186,21 @@ def validate_fb_location_(value): return validator(value) +def validate_jpeg_quality(config: ConfigType) -> ConfigType: + quality = config.get(CONF_JPEG_QUALITY) + pixel_format = config.get(CONF_PIXEL_FORMAT, "JPEG") + + if quality == 0: + # Set default JPEG quality if not specified for backwards compatibility + if pixel_format == "JPEG": + config[CONF_JPEG_QUALITY] = 10 + # For pixel formats other than JPEG, the valid 0 means no conversion + elif quality < 6 or quality > 63: + raise cv.Invalid(f"jpeg_quality must be between 6 and 63, got {quality}") + + return config + + CONFIG_SCHEMA = cv.All( cv.ENTITY_BASE_SCHEMA.extend( { @@ -206,7 +236,12 @@ CONFIG_SCHEMA = cv.All( cv.Optional(CONF_RESOLUTION, default="640X480"): cv.enum( FRAME_SIZES, upper=True ), - cv.Optional(CONF_JPEG_QUALITY, default=10): cv.int_range(min=6, max=63), + cv.Optional(CONF_PIXEL_FORMAT, default="JPEG"): cv.enum( + PIXEL_FORMATS, upper=True + ), + cv.Optional(CONF_JPEG_QUALITY, default=0): cv.Any( + cv.one_of(0), cv.int_range(min=6, max=63) + ), cv.Optional(CONF_CONTRAST, default=0): camera_range_param, cv.Optional(CONF_BRIGHTNESS, default=0): camera_range_param, cv.Optional(CONF_SATURATION, default=0): camera_range_param, @@ -270,11 +305,21 @@ CONFIG_SCHEMA = cv.All( ), } ).extend(cv.COMPONENT_SCHEMA), + validate_jpeg_quality, cv.has_exactly_one_key(CONF_I2C_PINS, CONF_I2C_ID), ) def _final_validate(config): + # Check psram requirement for non-JPEG formats + if ( + config.get(CONF_PIXEL_FORMAT, "JPEG") != "JPEG" + and psram_domain not in CORE.loaded_integrations + ): + raise cv.Invalid( + f"Non-JPEG pixel formats require the '{psram_domain}' component for JPEG conversion" + ) + if CONF_I2C_PINS not in config: return fconf = fv.full_config.get() @@ -298,6 +343,7 @@ SETTERS = { CONF_RESET_PIN: "set_reset_pin", CONF_POWER_DOWN_PIN: "set_power_down_pin", # image + CONF_PIXEL_FORMAT: "set_pixel_format", CONF_JPEG_QUALITY: "set_jpeg_quality", CONF_VERTICAL_FLIP: "set_vertical_flip", CONF_HORIZONTAL_MIRROR: "set_horizontal_mirror", @@ -351,8 +397,10 @@ async def to_code(config): cg.add(var.set_frame_size(config[CONF_RESOLUTION])) cg.add_define("USE_CAMERA") + if config[CONF_JPEG_QUALITY] != 0 and config[CONF_PIXEL_FORMAT] != "JPEG": + cg.add_define("USE_ESP32_CAMERA_JPEG_CONVERSION") - add_idf_component(name="espressif/esp32-camera", ref="2.1.1") + add_idf_component(name="espressif/esp32-camera", ref="2.1.6") add_idf_sdkconfig_option("CONFIG_SCCB_HARDWARE_I2C_DRIVER_NEW", True) add_idf_sdkconfig_option("CONFIG_SCCB_HARDWARE_I2C_DRIVER_LEGACY", False) diff --git a/esphome/components/esp32_camera/esp32_camera.cpp b/esphome/components/esp32_camera/esp32_camera.cpp index 5466d2e7ef..085feb8c8a 100644 --- a/esphome/components/esp32_camera/esp32_camera.cpp +++ b/esphome/components/esp32_camera/esp32_camera.cpp @@ -16,6 +16,74 @@ static constexpr size_t FRAMEBUFFER_TASK_STACK_SIZE = 1792; static constexpr uint32_t FRAME_LOG_INTERVAL_MS = 60000; #endif +static const char *frame_size_to_str(framesize_t size) { + switch (size) { + case FRAMESIZE_QQVGA: + return "160x120 (QQVGA)"; + case FRAMESIZE_QCIF: + return "176x155 (QCIF)"; + case FRAMESIZE_HQVGA: + return "240x176 (HQVGA)"; + case FRAMESIZE_QVGA: + return "320x240 (QVGA)"; + case FRAMESIZE_CIF: + return "400x296 (CIF)"; + case FRAMESIZE_VGA: + return "640x480 (VGA)"; + case FRAMESIZE_SVGA: + return "800x600 (SVGA)"; + case FRAMESIZE_XGA: + return "1024x768 (XGA)"; + case FRAMESIZE_SXGA: + return "1280x1024 (SXGA)"; + case FRAMESIZE_UXGA: + return "1600x1200 (UXGA)"; + case FRAMESIZE_FHD: + return "1920x1080 (FHD)"; + case FRAMESIZE_P_HD: + return "720x1280 (P_HD)"; + case FRAMESIZE_P_3MP: + return "864x1536 (P_3MP)"; + case FRAMESIZE_QXGA: + return "2048x1536 (QXGA)"; + case FRAMESIZE_QHD: + return "2560x1440 (QHD)"; + case FRAMESIZE_WQXGA: + return "2560x1600 (WQXGA)"; + case FRAMESIZE_P_FHD: + return "1080x1920 (P_FHD)"; + case FRAMESIZE_QSXGA: + return "2560x1920 (QSXGA)"; + default: + return "UNKNOWN"; + } +} + +static const char *pixel_format_to_str(pixformat_t format) { + switch (format) { + case PIXFORMAT_RGB565: + return "RGB565"; + case PIXFORMAT_YUV422: + return "YUV422"; + case PIXFORMAT_YUV420: + return "YUV420"; + case PIXFORMAT_GRAYSCALE: + return "GRAYSCALE"; + case PIXFORMAT_JPEG: + return "JPEG"; + case PIXFORMAT_RGB888: + return "RGB888"; + case PIXFORMAT_RAW: + return "RAW"; + case PIXFORMAT_RGB444: + return "RGB444"; + case PIXFORMAT_RGB555: + return "RGB555"; + default: + return "UNKNOWN"; + } +} + /* ---------------- public API (derivated) ---------------- */ void ESP32Camera::setup() { #ifdef USE_I2C @@ -68,64 +136,9 @@ void ESP32Camera::dump_config() { this->name_.c_str(), YESNO(this->is_internal()), conf.pin_d0, conf.pin_d1, conf.pin_d2, conf.pin_d3, conf.pin_d4, conf.pin_d5, conf.pin_d6, conf.pin_d7, conf.pin_vsync, conf.pin_href, conf.pin_pclk, conf.pin_xclk, conf.xclk_freq_hz, conf.pin_sccb_sda, conf.pin_sccb_scl, conf.pin_reset); - switch (this->config_.frame_size) { - case FRAMESIZE_QQVGA: - ESP_LOGCONFIG(TAG, " Resolution: 160x120 (QQVGA)"); - break; - case FRAMESIZE_QCIF: - ESP_LOGCONFIG(TAG, " Resolution: 176x155 (QCIF)"); - break; - case FRAMESIZE_HQVGA: - ESP_LOGCONFIG(TAG, " Resolution: 240x176 (HQVGA)"); - break; - case FRAMESIZE_QVGA: - ESP_LOGCONFIG(TAG, " Resolution: 320x240 (QVGA)"); - break; - case FRAMESIZE_CIF: - ESP_LOGCONFIG(TAG, " Resolution: 400x296 (CIF)"); - break; - case FRAMESIZE_VGA: - ESP_LOGCONFIG(TAG, " Resolution: 640x480 (VGA)"); - break; - case FRAMESIZE_SVGA: - ESP_LOGCONFIG(TAG, " Resolution: 800x600 (SVGA)"); - break; - case FRAMESIZE_XGA: - ESP_LOGCONFIG(TAG, " Resolution: 1024x768 (XGA)"); - break; - case FRAMESIZE_SXGA: - ESP_LOGCONFIG(TAG, " Resolution: 1280x1024 (SXGA)"); - break; - case FRAMESIZE_UXGA: - ESP_LOGCONFIG(TAG, " Resolution: 1600x1200 (UXGA)"); - break; - case FRAMESIZE_FHD: - ESP_LOGCONFIG(TAG, " Resolution: 1920x1080 (FHD)"); - break; - case FRAMESIZE_P_HD: - ESP_LOGCONFIG(TAG, " Resolution: 720x1280 (P_HD)"); - break; - case FRAMESIZE_P_3MP: - ESP_LOGCONFIG(TAG, " Resolution: 864x1536 (P_3MP)"); - break; - case FRAMESIZE_QXGA: - ESP_LOGCONFIG(TAG, " Resolution: 2048x1536 (QXGA)"); - break; - case FRAMESIZE_QHD: - ESP_LOGCONFIG(TAG, " Resolution: 2560x1440 (QHD)"); - break; - case FRAMESIZE_WQXGA: - ESP_LOGCONFIG(TAG, " Resolution: 2560x1600 (WQXGA)"); - break; - case FRAMESIZE_P_FHD: - ESP_LOGCONFIG(TAG, " Resolution: 1080x1920 (P_FHD)"); - break; - case FRAMESIZE_QSXGA: - ESP_LOGCONFIG(TAG, " Resolution: 2560x1920 (QSXGA)"); - break; - default: - break; - } + + ESP_LOGCONFIG(TAG, " Resolution: %s", frame_size_to_str(this->config_.frame_size)); + ESP_LOGCONFIG(TAG, " Pixel Format: %s", pixel_format_to_str(this->config_.pixel_format)); if (this->is_failed()) { ESP_LOGE(TAG, " Setup Failed: %s", esp_err_to_name(this->init_error_)); @@ -133,6 +146,10 @@ void ESP32Camera::dump_config() { } sensor_t *s = esp_camera_sensor_get(); + if (s == nullptr) { + ESP_LOGE(TAG, " Camera sensor not available"); + return; + } auto st = s->status; ESP_LOGCONFIG(TAG, " JPEG Quality: %u\n" @@ -184,8 +201,19 @@ void ESP32Camera::loop() { // check if we can return the image if (this->can_return_image_()) { // return image - auto *fb = this->current_image_->get_raw_buffer(); - xQueueSend(this->framebuffer_return_queue_, &fb, portMAX_DELAY); +#ifdef USE_ESP32_CAMERA_JPEG_CONVERSION + if (this->config_.pixel_format != PIXFORMAT_JPEG && this->config_.jpeg_quality > 0) { + // for non-JPEG format, we need to free the data and raw buffer + auto *jpg_buf = this->current_image_->get_data_buffer(); + free(jpg_buf); // NOLINT(cppcoreguidelines-no-malloc) + auto *fb = this->current_image_->get_raw_buffer(); + this->fb_allocator_.deallocate(fb, 1); + } else +#endif + { + auto *fb = this->current_image_->get_raw_buffer(); + xQueueSend(this->framebuffer_return_queue_, &fb, portMAX_DELAY); + } this->current_image_.reset(); } @@ -212,6 +240,38 @@ void ESP32Camera::loop() { xQueueSend(this->framebuffer_return_queue_, &fb, portMAX_DELAY); return; } + +#ifdef USE_ESP32_CAMERA_JPEG_CONVERSION + if (this->config_.pixel_format != PIXFORMAT_JPEG && this->config_.jpeg_quality > 0) { + // for non-JPEG format, we need to convert the frame to JPEG + uint8_t *jpg_buf; + size_t jpg_buf_len; + size_t width = fb->width; + size_t height = fb->height; + struct timeval timestamp = fb->timestamp; + bool ok = frame2jpg(fb, 100 - this->config_.jpeg_quality, &jpg_buf, &jpg_buf_len); + // return the original frame buffer to the queue + xQueueSend(this->framebuffer_return_queue_, &fb, portMAX_DELAY); + if (!ok) { + ESP_LOGE(TAG, "Failed to convert frame to JPEG!"); + return; + } + // create a new camera_fb_t for the JPEG data + fb = this->fb_allocator_.allocate(1); + if (fb == nullptr) { + ESP_LOGE(TAG, "Failed to allocate memory for camera frame buffer!"); + free(jpg_buf); // NOLINT(cppcoreguidelines-no-malloc) + return; + } + memset(fb, 0, sizeof(camera_fb_t)); + fb->buf = jpg_buf; + fb->len = jpg_buf_len; + fb->width = width; + fb->height = height; + fb->format = PIXFORMAT_JPEG; + fb->timestamp = timestamp; + } +#endif this->current_image_ = std::make_shared(fb, this->single_requesters_ | this->stream_requesters_); #if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE @@ -235,8 +295,6 @@ void ESP32Camera::loop() { this->single_requesters_ = 0; } -float ESP32Camera::get_setup_priority() const { return setup_priority::DATA; } - /* ---------------- constructors ---------------- */ ESP32Camera::ESP32Camera() { this->config_.pin_pwdn = -1; @@ -344,6 +402,37 @@ void ESP32Camera::set_frame_size(ESP32CameraFrameSize size) { break; } } +void ESP32Camera::set_pixel_format(ESP32CameraPixelFormat format) { + switch (format) { + case ESP32_PIXEL_FORMAT_RGB565: + this->config_.pixel_format = PIXFORMAT_RGB565; + break; + case ESP32_PIXEL_FORMAT_YUV422: + this->config_.pixel_format = PIXFORMAT_YUV422; + break; + case ESP32_PIXEL_FORMAT_YUV420: + this->config_.pixel_format = PIXFORMAT_YUV420; + break; + case ESP32_PIXEL_FORMAT_GRAYSCALE: + this->config_.pixel_format = PIXFORMAT_GRAYSCALE; + break; + case ESP32_PIXEL_FORMAT_JPEG: + this->config_.pixel_format = PIXFORMAT_JPEG; + break; + case ESP32_PIXEL_FORMAT_RGB888: + this->config_.pixel_format = PIXFORMAT_RGB888; + break; + case ESP32_PIXEL_FORMAT_RAW: + this->config_.pixel_format = PIXFORMAT_RAW; + break; + case ESP32_PIXEL_FORMAT_RGB444: + this->config_.pixel_format = PIXFORMAT_RGB444; + break; + case ESP32_PIXEL_FORMAT_RGB555: + this->config_.pixel_format = PIXFORMAT_RGB555; + break; + } +} void ESP32Camera::set_jpeg_quality(uint8_t quality) { this->config_.jpeg_quality = quality; } void ESP32Camera::set_vertical_flip(bool vertical_flip) { this->vertical_flip_ = vertical_flip; } void ESP32Camera::set_horizontal_mirror(bool horizontal_mirror) { this->horizontal_mirror_ = horizontal_mirror; } @@ -398,6 +487,9 @@ void ESP32Camera::request_image(camera::CameraRequester requester) { this->singl camera::CameraImageReader *ESP32Camera::create_image_reader() { return new ESP32CameraImageReader; } void ESP32Camera::update_camera_parameters() { sensor_t *s = esp_camera_sensor_get(); + if (s == nullptr) { + return; + } /* update image */ s->set_vflip(s, this->vertical_flip_); s->set_hmirror(s, this->horizontal_mirror_); diff --git a/esphome/components/esp32_camera/esp32_camera.h b/esphome/components/esp32_camera/esp32_camera.h index e97eb27c70..9fbd3848f2 100644 --- a/esphome/components/esp32_camera/esp32_camera.h +++ b/esphome/components/esp32_camera/esp32_camera.h @@ -41,6 +41,18 @@ enum ESP32CameraFrameSize { ESP32_CAMERA_SIZE_2560X1920, // QSXGA }; +enum ESP32CameraPixelFormat { + ESP32_PIXEL_FORMAT_RGB565, + ESP32_PIXEL_FORMAT_YUV422, + ESP32_PIXEL_FORMAT_YUV420, + ESP32_PIXEL_FORMAT_GRAYSCALE, + ESP32_PIXEL_FORMAT_JPEG, + ESP32_PIXEL_FORMAT_RGB888, + ESP32_PIXEL_FORMAT_RAW, + ESP32_PIXEL_FORMAT_RGB444, + ESP32_PIXEL_FORMAT_RGB555, +}; + enum ESP32AgcGainCeiling { ESP32_GAINCEILING_2X = GAINCEILING_2X, ESP32_GAINCEILING_4X = GAINCEILING_4X, @@ -126,6 +138,7 @@ class ESP32Camera : public camera::Camera { void set_reset_pin(uint8_t pin); void set_power_down_pin(uint8_t pin); /* -- image */ + void set_pixel_format(ESP32CameraPixelFormat format); void set_frame_size(ESP32CameraFrameSize size); void set_jpeg_quality(uint8_t quality); void set_vertical_flip(bool vertical_flip); @@ -159,7 +172,6 @@ class ESP32Camera : public camera::Camera { void setup() override; void loop() override; void dump_config() override; - float get_setup_priority() const override; /* public API (specific) */ void start_stream(camera::CameraRequester requester) override; void stop_stream(camera::CameraRequester requester) override; @@ -221,6 +233,7 @@ class ESP32Camera : public camera::Camera { #ifdef USE_I2C i2c::InternalI2CBus *i2c_bus_{nullptr}; #endif // USE_I2C + RAMAllocator fb_allocator_{RAMAllocator::ALLOC_INTERNAL}; }; class ESP32CameraImageTrigger : public Trigger, public camera::CameraListener { diff --git a/esphome/components/esp32_camera_web_server/__init__.py b/esphome/components/esp32_camera_web_server/__init__.py index ed1aaa2e07..da260ad7a1 100644 --- a/esphome/components/esp32_camera_web_server/__init__.py +++ b/esphome/components/esp32_camera_web_server/__init__.py @@ -20,8 +20,10 @@ def _consume_camera_web_server_sockets(config: ConfigType) -> ConfigType: from esphome.components import socket # Each camera web server instance needs 1 listening socket + 2 client connections - sockets_needed = 3 - socket.consume_sockets(sockets_needed, "esp32_camera_web_server")(config) + socket.consume_sockets(2, "esp32_camera_web_server")(config) + socket.consume_sockets(1, "esp32_camera_web_server", socket.SocketType.TCP_LISTEN)( + config + ) return config diff --git a/esphome/components/esp32_can/canbus.py b/esphome/components/esp32_can/canbus.py index 0768b35507..7245ba7513 100644 --- a/esphome/components/esp32_can/canbus.py +++ b/esphome/components/esp32_can/canbus.py @@ -15,6 +15,7 @@ from esphome.components.esp32 import ( VARIANT_ESP32S2, VARIANT_ESP32S3, get_esp32_variant, + include_builtin_idf_component, ) import esphome.config_validation as cv from esphome.const import ( @@ -121,6 +122,10 @@ def get_default_tx_enqueue_timeout(bit_rate): async def to_code(config): + # Legacy driver component provides driver/twai.h header + include_builtin_idf_component("driver") + # Also enable esp_driver_twai for future migration to new API + include_builtin_idf_component("esp_driver_twai") var = cg.new_Pvariable(config[CONF_ID]) await canbus.register_canbus(var, config) diff --git a/esphome/components/esp32_dac/output.py b/esphome/components/esp32_dac/output.py index daace596d3..7c63d7bd11 100644 --- a/esphome/components/esp32_dac/output.py +++ b/esphome/components/esp32_dac/output.py @@ -1,7 +1,12 @@ from esphome import pins import esphome.codegen as cg from esphome.components import output -from esphome.components.esp32 import VARIANT_ESP32, VARIANT_ESP32S2, get_esp32_variant +from esphome.components.esp32 import ( + VARIANT_ESP32, + VARIANT_ESP32S2, + get_esp32_variant, + include_builtin_idf_component, +) import esphome.config_validation as cv from esphome.const import CONF_ID, CONF_NUMBER, CONF_PIN @@ -38,6 +43,7 @@ CONFIG_SCHEMA = output.FLOAT_OUTPUT_SCHEMA.extend( async def to_code(config): + include_builtin_idf_component("esp_driver_dac") var = cg.new_Pvariable(config[CONF_ID]) await cg.register_component(var, config) await output.register_output(var, config) diff --git a/esphome/components/esp32_hosted/__init__.py b/esphome/components/esp32_hosted/__init__.py index 170f436f02..3f9185745d 100644 --- a/esphome/components/esp32_hosted/__init__.py +++ b/esphome/components/esp32_hosted/__init__.py @@ -4,14 +4,7 @@ from pathlib import Path from esphome import pins from esphome.components import esp32 import esphome.config_validation as cv -from esphome.const import ( - CONF_CLK_PIN, - CONF_RESET_PIN, - CONF_VARIANT, - KEY_CORE, - KEY_FRAMEWORK_VERSION, -) -from esphome.core import CORE +from esphome.const import CONF_CLK_PIN, CONF_RESET_PIN, CONF_VARIANT from esphome.cpp_generator import add_define CODEOWNERS = ["@swoboda1337"] @@ -23,6 +16,7 @@ CONF_D1_PIN = "d1_pin" CONF_D2_PIN = "d2_pin" CONF_D3_PIN = "d3_pin" CONF_SLOT = "slot" +CONF_SDIO_FREQUENCY = "sdio_frequency" CONFIG_SCHEMA = cv.All( cv.Schema( @@ -37,6 +31,9 @@ CONFIG_SCHEMA = cv.All( cv.Required(CONF_D3_PIN): pins.internal_gpio_output_pin_number, cv.Required(CONF_RESET_PIN): pins.internal_gpio_output_pin_number, cv.Optional(CONF_SLOT, default=1): cv.int_range(min=0, max=1), + cv.Optional(CONF_SDIO_FREQUENCY, default="40MHz"): cv.All( + cv.frequency, cv.Range(min=400e3, max=50e6) + ), } ), ) @@ -91,13 +88,17 @@ async def to_code(config): config[CONF_D3_PIN], ) esp32.add_idf_sdkconfig_option("CONFIG_ESP_HOSTED_CUSTOM_SDIO_PINS", True) + esp32.add_idf_sdkconfig_option( + "CONFIG_ESP_HOSTED_SDIO_CLOCK_FREQ_KHZ", + int(config[CONF_SDIO_FREQUENCY] // 1000), + ) - framework_ver: cv.Version = CORE.data[KEY_CORE][KEY_FRAMEWORK_VERSION] - os.environ["ESP_IDF_VERSION"] = f"{framework_ver.major}.{framework_ver.minor}" - if framework_ver >= cv.Version(5, 5, 0): - esp32.add_idf_component(name="espressif/esp_wifi_remote", ref="1.2.4") + idf_ver = esp32.idf_version() + os.environ["ESP_IDF_VERSION"] = f"{idf_ver.major}.{idf_ver.minor}" + if idf_ver >= cv.Version(5, 5, 0): + esp32.add_idf_component(name="espressif/esp_wifi_remote", ref="1.4.0") esp32.add_idf_component(name="espressif/eppp_link", ref="1.1.4") - esp32.add_idf_component(name="espressif/esp_hosted", ref="2.9.3") + esp32.add_idf_component(name="espressif/esp_hosted", ref="2.12.1") else: esp32.add_idf_component(name="espressif/esp_wifi_remote", ref="0.13.0") esp32.add_idf_component(name="espressif/eppp_link", ref="0.2.0") diff --git a/esphome/components/esp32_hosted/update/esp32_hosted_update.cpp b/esphome/components/esp32_hosted/update/esp32_hosted_update.cpp index ebcdd5f36e..af35d32888 100644 --- a/esphome/components/esp32_hosted/update/esp32_hosted_update.cpp +++ b/esphome/components/esp32_hosted/update/esp32_hosted_update.cpp @@ -27,6 +27,11 @@ static const char *const TAG = "esp32_hosted.update"; // Older coprocessor firmware versions have a 1500-byte limit per RPC call constexpr size_t CHUNK_SIZE = 1500; +#ifdef USE_ESP32_HOSTED_HTTP_UPDATE +// Interval/timeout IDs (uint32_t to avoid string comparison) +constexpr uint32_t INITIAL_CHECK_INTERVAL_ID = 0; +#endif + // Compile-time version string from esp_hosted_host_fw_ver.h macros #define STRINGIFY_(x) #x #define STRINGIFY(x) STRINGIFY_(x) @@ -34,14 +39,29 @@ static const char *const ESP_HOSTED_VERSION_STR = STRINGIFY(ESP_HOSTED_VERSION_M ESP_HOSTED_VERSION_MINOR_1) "." STRINGIFY(ESP_HOSTED_VERSION_PATCH_1); #ifdef USE_ESP32_HOSTED_HTTP_UPDATE +// Parse an integer from str, advancing ptr past the number +// Returns false if no digits were parsed +static bool parse_int(const char *&ptr, int &value) { + char *end; + value = static_cast(strtol(ptr, &end, 10)); + if (end == ptr) + return false; + ptr = end; + return true; +} + // Parse version string "major.minor.patch" into components -// Returns true if parsing succeeded +// Returns true if at least major.minor was parsed static bool parse_version(const std::string &version_str, int &major, int &minor, int &patch) { major = minor = patch = 0; - if (sscanf(version_str.c_str(), "%d.%d.%d", &major, &minor, &patch) >= 2) { - return true; - } - return false; + const char *ptr = version_str.c_str(); + + if (!parse_int(ptr, major) || *ptr++ != '.' || !parse_int(ptr, minor)) + return false; + if (*ptr == '.') + parse_int(++ptr, patch); + + return true; } // Compare two versions, returns: @@ -86,11 +106,12 @@ void Esp32HostedUpdate::setup() { esp_app_desc_t *app_desc = (esp_app_desc_t *) (this->firmware_data_ + app_desc_offset); if (app_desc->magic_word == ESP_APP_DESC_MAGIC_WORD) { ESP_LOGD(TAG, - "Firmware version: %s\n" - "Project name: %s\n" - "Build date: %s\n" - "Build time: %s\n" - "IDF version: %s", + "ESP32 Hosted firmware:\n" + " Firmware version: %s\n" + " Project name: %s\n" + " Build date: %s\n" + " Build time: %s\n" + " IDF version: %s", app_desc->version, app_desc->project_name, app_desc->date, app_desc->time, app_desc->idf_ver); this->update_info_.latest_version = app_desc->version; if (this->update_info_.latest_version != this->update_info_.current_version) { @@ -112,15 +133,18 @@ void Esp32HostedUpdate::setup() { this->status_clear_error(); this->publish_state(); #else - // HTTP mode: retry initial check every 10s until network is ready (max 6 attempts) + // HTTP mode: check every 10s until network is ready (max 6 attempts) // Only if update interval is > 1 minute to avoid redundant checks if (this->get_update_interval() > 60000) { - this->set_retry("initial_check", 10000, 6, [this](uint8_t) { - if (!network::is_connected()) { - return RetryResult::RETRY; + this->initial_check_remaining_ = 6; + this->set_interval(INITIAL_CHECK_INTERVAL_ID, 10000, [this]() { + bool connected = network::is_connected(); + if (--this->initial_check_remaining_ == 0 || connected) { + this->cancel_interval(INITIAL_CHECK_INTERVAL_ID); + if (connected) { + this->check(); + } } - this->check(); - return RetryResult::DONE; }); } #endif @@ -196,11 +220,14 @@ bool Esp32HostedUpdate::fetch_manifest_() { int read_or_error = container->read(buf, sizeof(buf)); App.feed_wdt(); yield(); - auto result = http_request::http_read_loop_result(read_or_error, last_data_time, read_timeout); + auto result = + http_request::http_read_loop_result(read_or_error, last_data_time, read_timeout, container->is_read_complete()); if (result == http_request::HttpReadLoopResult::RETRY) continue; + // Note: COMPLETE is currently unreachable since the loop condition checks bytes_read < content_length, + // but this is defensive code in case chunked transfer encoding support is added in the future. if (result != http_request::HttpReadLoopResult::DATA) - break; // ERROR or TIMEOUT + break; // COMPLETE, ERROR, or TIMEOUT json_str.append(reinterpret_cast(buf), read_or_error); } container->end(); @@ -321,9 +348,14 @@ bool Esp32HostedUpdate::stream_firmware_to_coprocessor_() { App.feed_wdt(); yield(); - auto result = http_request::http_read_loop_result(read_or_error, last_data_time, read_timeout); + auto result = + http_request::http_read_loop_result(read_or_error, last_data_time, read_timeout, container->is_read_complete()); if (result == http_request::HttpReadLoopResult::RETRY) continue; + // Note: COMPLETE is currently unreachable since the loop condition checks bytes_read < content_length, + // but this is defensive code in case chunked transfer encoding support is added in the future. + if (result == http_request::HttpReadLoopResult::COMPLETE) + break; if (result != http_request::HttpReadLoopResult::DATA) { if (result == http_request::HttpReadLoopResult::TIMEOUT) { ESP_LOGE(TAG, "Timeout reading firmware data"); @@ -416,6 +448,13 @@ void Esp32HostedUpdate::perform(bool force) { return; } +#ifdef USE_ESP32_HOSTED_HTTP_UPDATE + if (this->firmware_url_.empty()) { + ESP_LOGW(TAG, "No firmware URL available, run check first"); + return; + } +#endif + update::UpdateState prev_state = this->state_; this->state_ = update::UPDATE_STATE_INSTALLING; this->update_info_.has_progress = false; diff --git a/esphome/components/esp32_hosted/update/esp32_hosted_update.h b/esphome/components/esp32_hosted/update/esp32_hosted_update.h index 7c9645c12a..005e6a6f21 100644 --- a/esphome/components/esp32_hosted/update/esp32_hosted_update.h +++ b/esphome/components/esp32_hosted/update/esp32_hosted_update.h @@ -44,6 +44,7 @@ class Esp32HostedUpdate : public update::UpdateEntity, public PollingComponent { // HTTP mode helpers bool fetch_manifest_(); bool stream_firmware_to_coprocessor_(); + uint8_t initial_check_remaining_{0}; #else // Embedded mode members const uint8_t *firmware_data_{nullptr}; diff --git a/esphome/components/esp32_improv/automation.h b/esphome/components/esp32_improv/automation.h index 52c5da125b..cd2bd84c30 100644 --- a/esphome/components/esp32_improv/automation.h +++ b/esphome/components/esp32_improv/automation.h @@ -12,58 +12,73 @@ namespace esp32_improv { class ESP32ImprovProvisionedTrigger : public Trigger<> { public: - explicit ESP32ImprovProvisionedTrigger(ESP32ImprovComponent *parent) { - parent->add_on_state_callback([this, parent](improv::State state, improv::Error error) { - if (state == improv::STATE_PROVISIONED && !parent->is_failed()) { - trigger(); + explicit ESP32ImprovProvisionedTrigger(ESP32ImprovComponent *parent) : parent_(parent) { + parent->add_on_state_callback([this](improv::State state, improv::Error error) { + if (state == improv::STATE_PROVISIONED && !this->parent_->is_failed()) { + this->trigger(); } }); } + + protected: + ESP32ImprovComponent *parent_; }; class ESP32ImprovProvisioningTrigger : public Trigger<> { public: - explicit ESP32ImprovProvisioningTrigger(ESP32ImprovComponent *parent) { - parent->add_on_state_callback([this, parent](improv::State state, improv::Error error) { - if (state == improv::STATE_PROVISIONING && !parent->is_failed()) { - trigger(); + explicit ESP32ImprovProvisioningTrigger(ESP32ImprovComponent *parent) : parent_(parent) { + parent->add_on_state_callback([this](improv::State state, improv::Error error) { + if (state == improv::STATE_PROVISIONING && !this->parent_->is_failed()) { + this->trigger(); } }); } + + protected: + ESP32ImprovComponent *parent_; }; class ESP32ImprovStartTrigger : public Trigger<> { public: - explicit ESP32ImprovStartTrigger(ESP32ImprovComponent *parent) { - parent->add_on_state_callback([this, parent](improv::State state, improv::Error error) { + explicit ESP32ImprovStartTrigger(ESP32ImprovComponent *parent) : parent_(parent) { + parent->add_on_state_callback([this](improv::State state, improv::Error error) { if ((state == improv::STATE_AUTHORIZED || state == improv::STATE_AWAITING_AUTHORIZATION) && - !parent->is_failed()) { - trigger(); + !this->parent_->is_failed()) { + this->trigger(); } }); } + + protected: + ESP32ImprovComponent *parent_; }; class ESP32ImprovStateTrigger : public Trigger { public: - explicit ESP32ImprovStateTrigger(ESP32ImprovComponent *parent) { - parent->add_on_state_callback([this, parent](improv::State state, improv::Error error) { - if (!parent->is_failed()) { - trigger(state, error); + explicit ESP32ImprovStateTrigger(ESP32ImprovComponent *parent) : parent_(parent) { + parent->add_on_state_callback([this](improv::State state, improv::Error error) { + if (!this->parent_->is_failed()) { + this->trigger(state, error); } }); } + + protected: + ESP32ImprovComponent *parent_; }; class ESP32ImprovStoppedTrigger : public Trigger<> { public: - explicit ESP32ImprovStoppedTrigger(ESP32ImprovComponent *parent) { - parent->add_on_state_callback([this, parent](improv::State state, improv::Error error) { - if (state == improv::STATE_STOPPED && !parent->is_failed()) { - trigger(); + explicit ESP32ImprovStoppedTrigger(ESP32ImprovComponent *parent) : parent_(parent) { + parent->add_on_state_callback([this](improv::State state, improv::Error error) { + if (state == improv::STATE_STOPPED && !this->parent_->is_failed()) { + this->trigger(); } }); } + + protected: + ESP32ImprovComponent *parent_; }; } // namespace esp32_improv diff --git a/esphome/components/esp32_improv/esp32_improv_component.cpp b/esphome/components/esp32_improv/esp32_improv_component.cpp index 1a19472c87..c24b08b06f 100644 --- a/esphome/components/esp32_improv/esp32_improv_component.cpp +++ b/esphome/components/esp32_improv/esp32_improv_component.cpp @@ -314,6 +314,8 @@ void ESP32ImprovComponent::dump_config() { } void ESP32ImprovComponent::process_incoming_data_() { + if (this->incoming_data_.size() < 3) + return; uint8_t length = this->incoming_data_[1]; #if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE @@ -338,8 +340,8 @@ void ESP32ImprovComponent::process_incoming_data_() { return; } wifi::WiFiAP sta{}; - sta.set_ssid(command.ssid); - sta.set_password(command.password); + sta.set_ssid(command.ssid.c_str()); + sta.set_password(command.password.c_str()); this->connecting_sta_ = sta; wifi::global_wifi_component->set_sta(sta); @@ -348,8 +350,7 @@ void ESP32ImprovComponent::process_incoming_data_() { ESP_LOGD(TAG, "Received Improv Wi-Fi settings ssid=%s, password=" LOG_SECRET("%s"), command.ssid.c_str(), command.password.c_str()); - auto f = std::bind(&ESP32ImprovComponent::on_wifi_connect_timeout_, this); - this->set_timeout("wifi-connect-timeout", 30000, f); + this->set_timeout("wifi-connect-timeout", 30000, [this]() { this->on_wifi_connect_timeout_(); }); this->incoming_data_.clear(); break; } diff --git a/esphome/components/esp32_improv/esp32_improv_component.h b/esphome/components/esp32_improv/esp32_improv_component.h index 8f4cfd7958..41799f2325 100644 --- a/esphome/components/esp32_improv/esp32_improv_component.h +++ b/esphome/components/esp32_improv/esp32_improv_component.h @@ -48,8 +48,8 @@ class ESP32ImprovComponent : public Component, public improv_base::ImprovBase { bool should_start() const { return this->should_start_; } #ifdef USE_ESP32_IMPROV_STATE_CALLBACK - void add_on_state_callback(std::function &&callback) { - this->state_callback_.add(std::move(callback)); + template void add_on_state_callback(F &&callback) { + this->state_callback_.add(std::forward(callback)); } #endif #ifdef USE_BINARY_SENSOR diff --git a/esphome/components/esp32_rmt/__init__.py b/esphome/components/esp32_rmt/__init__.py index 272c7c81ba..1076bcabdc 100644 --- a/esphome/components/esp32_rmt/__init__.py +++ b/esphome/components/esp32_rmt/__init__.py @@ -1,8 +1,30 @@ from esphome.components import esp32 import esphome.config_validation as cv +from esphome.core import CORE CODEOWNERS = ["@jesserockz"] +VARIANTS_NO_RMT = {esp32.VARIANT_ESP32C2, esp32.VARIANT_ESP32C61} + + +def validate_rmt_not_supported(rmt_only_keys): + """Validate that RMT-only config keys are not used on variants without RMT hardware.""" + rmt_only_keys = set(rmt_only_keys) + + def _validator(config): + if CORE.is_esp32: + variant = esp32.get_esp32_variant() + if variant in VARIANTS_NO_RMT: + unsupported = rmt_only_keys.intersection(config) + if unsupported: + keys = ", ".join(sorted(f"'{k}'" for k in unsupported)) + raise cv.Invalid( + f"{keys} not available on {variant} (no RMT hardware)" + ) + return config + + return _validator + def validate_clock_resolution(): def _validator(value): diff --git a/esphome/components/esp32_rmt_led_strip/led_strip.cpp b/esphome/components/esp32_rmt_led_strip/led_strip.cpp index 4ca0b998b1..ca97a181fd 100644 --- a/esphome/components/esp32_rmt_led_strip/led_strip.cpp +++ b/esphome/components/esp32_rmt_led_strip/led_strip.cpp @@ -7,22 +7,25 @@ #include "esphome/core/log.h" #include +#include namespace esphome { namespace esp32_rmt_led_strip { static const char *const TAG = "esp32_rmt_led_strip"; -#ifdef USE_ESP32_VARIANT_ESP32H2 -static const uint32_t RMT_CLK_FREQ = 32000000; -static const uint8_t RMT_CLK_DIV = 1; -#else -static const uint32_t RMT_CLK_FREQ = 80000000; -static const uint8_t RMT_CLK_DIV = 2; -#endif - static const size_t RMT_SYMBOLS_PER_BYTE = 8; +// Query the RMT default clock source frequency. This varies by variant: +// APB (80MHz) on ESP32/S2/S3/C3, PLL_F80M (80MHz) on C6/P4, XTAL (32MHz) on H2. +// Worst-case reset time is WS2811 at 300µs = 24000 ticks at 80MHz, well within +// the 15-bit rmt_symbol_word_t duration field max of 32767. +static uint32_t rmt_resolution_hz() { + uint32_t freq; + esp_clk_tree_src_get_freq_hz((soc_module_clk_t) RMT_CLK_SRC_DEFAULT, ESP_CLK_TREE_SRC_FREQ_PRECISION_CACHED, &freq); + return freq; +} + #if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 3, 0) static size_t IRAM_ATTR HOT encoder_callback(const void *data, size_t size, size_t symbols_written, size_t symbols_free, rmt_symbol_word_t *symbols, bool *done, void *arg) { @@ -92,12 +95,10 @@ void ESP32RMTLEDStripLightOutput::setup() { rmt_tx_channel_config_t channel; memset(&channel, 0, sizeof(channel)); channel.clk_src = RMT_CLK_SRC_DEFAULT; - channel.resolution_hz = RMT_CLK_FREQ / RMT_CLK_DIV; + channel.resolution_hz = rmt_resolution_hz(); channel.gpio_num = gpio_num_t(this->pin_); channel.mem_block_symbols = this->rmt_symbols_; channel.trans_queue_depth = 1; - channel.flags.io_loop_back = 0; - channel.flags.io_od_mode = 0; channel.flags.invert_out = this->invert_out_; channel.flags.with_dma = this->use_dma_; channel.intr_priority = 0; @@ -137,7 +138,7 @@ void ESP32RMTLEDStripLightOutput::setup() { void ESP32RMTLEDStripLightOutput::set_led_params(uint32_t bit0_high, uint32_t bit0_low, uint32_t bit1_high, uint32_t bit1_low, uint32_t reset_time_high, uint32_t reset_time_low) { - float ratio = (float) RMT_CLK_FREQ / RMT_CLK_DIV / 1e09f; + float ratio = (float) rmt_resolution_hz() / 1e09f; // 0-bit this->params_.bit0.duration0 = (uint32_t) (ratio * bit0_high); @@ -159,7 +160,8 @@ void ESP32RMTLEDStripLightOutput::set_led_params(uint32_t bit0_high, uint32_t bi void ESP32RMTLEDStripLightOutput::write_state(light::LightState *state) { // protect from refreshing too often uint32_t now = micros(); - if (*this->max_refresh_rate_ != 0 && (now - this->last_refresh_) < *this->max_refresh_rate_) { + auto rate = this->max_refresh_rate_.value_or(0); + if (rate != 0 && (now - this->last_refresh_) < rate) { // try again next loop iteration, so that this change won't get lost this->schedule_show(); return; @@ -298,7 +300,7 @@ void ESP32RMTLEDStripLightOutput::dump_config() { " RGB Order: %s\n" " Max refresh rate: %" PRIu32 "\n" " Number of LEDs: %u", - rgb_order, *this->max_refresh_rate_, this->num_leds_); + rgb_order, this->max_refresh_rate_.value_or(0), this->num_leds_); } float ESP32RMTLEDStripLightOutput::get_setup_priority() const { return setup_priority::HARDWARE; } diff --git a/esphome/components/esp32_rmt_led_strip/light.py b/esphome/components/esp32_rmt_led_strip/light.py index 3be3c758f1..1c6943b003 100644 --- a/esphome/components/esp32_rmt_led_strip/light.py +++ b/esphome/components/esp32_rmt_led_strip/light.py @@ -3,8 +3,9 @@ import logging from esphome import pins import esphome.codegen as cg -from esphome.components import esp32, light +from esphome.components import esp32, esp32_rmt, light from esphome.components.const import CONF_USE_PSRAM +from esphome.components.esp32 import include_builtin_idf_component import esphome.config_validation as cv from esphome.const import ( CONF_CHIPSET, @@ -70,6 +71,10 @@ CONF_RESET_LOW = "reset_low" CONFIG_SCHEMA = cv.All( + esp32.only_on_variant( + unsupported=list(esp32_rmt.VARIANTS_NO_RMT), + msg_prefix="ESP32 RMT LED strip", + ), light.ADDRESSABLE_LIGHT_SCHEMA.extend( { cv.GenerateID(CONF_OUTPUT_ID): cv.declare_id(ESP32RMTLEDStripLightOutput), @@ -129,6 +134,9 @@ CONFIG_SCHEMA = cv.All( 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]) await light.register_light(var, config) await cg.register_component(var, config) diff --git a/esphome/components/esp32_touch/__init__.py b/esphome/components/esp32_touch/__init__.py index c54ed8b9ea..10ad339b12 100644 --- a/esphome/components/esp32_touch/__init__.py +++ b/esphome/components/esp32_touch/__init__.py @@ -1,11 +1,15 @@ +import logging + import esphome.codegen as cg from esphome.components import esp32 from esphome.components.esp32 import ( VARIANT_ESP32, + VARIANT_ESP32P4, VARIANT_ESP32S2, VARIANT_ESP32S3, get_esp32_variant, gpio, + include_builtin_idf_component, ) import esphome.config_validation as cv from esphome.const import ( @@ -20,6 +24,8 @@ from esphome.const import ( ) from esphome.core import TimePeriod +_LOGGER = logging.getLogger(__name__) + AUTO_LOAD = ["binary_sensor"] DEPENDENCIES = ["esp32"] @@ -36,135 +42,161 @@ CONF_WATERPROOF_SHIELD_DRIVER = "waterproof_shield_driver" esp32_touch_ns = cg.esphome_ns.namespace("esp32_touch") ESP32TouchComponent = esp32_touch_ns.class_("ESP32TouchComponent", cg.Component) +# Channel ID mappings: GPIO pin number -> integer channel ID +# These are plain integers - the new unified API uses int chan_id directly. TOUCH_PADS = { VARIANT_ESP32: { - 4: cg.global_ns.TOUCH_PAD_NUM0, - 0: cg.global_ns.TOUCH_PAD_NUM1, - 2: cg.global_ns.TOUCH_PAD_NUM2, - 15: cg.global_ns.TOUCH_PAD_NUM3, - 13: cg.global_ns.TOUCH_PAD_NUM4, - 12: cg.global_ns.TOUCH_PAD_NUM5, - 14: cg.global_ns.TOUCH_PAD_NUM6, - 27: cg.global_ns.TOUCH_PAD_NUM7, - 33: cg.global_ns.TOUCH_PAD_NUM8, - 32: cg.global_ns.TOUCH_PAD_NUM9, + 4: 0, + 0: 1, + 2: 2, + 15: 3, + 13: 4, + 12: 5, + 14: 6, + 27: 7, + 33: 8, + 32: 9, }, VARIANT_ESP32S2: { - 1: cg.global_ns.TOUCH_PAD_NUM1, - 2: cg.global_ns.TOUCH_PAD_NUM2, - 3: cg.global_ns.TOUCH_PAD_NUM3, - 4: cg.global_ns.TOUCH_PAD_NUM4, - 5: cg.global_ns.TOUCH_PAD_NUM5, - 6: cg.global_ns.TOUCH_PAD_NUM6, - 7: cg.global_ns.TOUCH_PAD_NUM7, - 8: cg.global_ns.TOUCH_PAD_NUM8, - 9: cg.global_ns.TOUCH_PAD_NUM9, - 10: cg.global_ns.TOUCH_PAD_NUM10, - 11: cg.global_ns.TOUCH_PAD_NUM11, - 12: cg.global_ns.TOUCH_PAD_NUM12, - 13: cg.global_ns.TOUCH_PAD_NUM13, - 14: cg.global_ns.TOUCH_PAD_NUM14, + 1: 1, + 2: 2, + 3: 3, + 4: 4, + 5: 5, + 6: 6, + 7: 7, + 8: 8, + 9: 9, + 10: 10, + 11: 11, + 12: 12, + 13: 13, + 14: 14, }, VARIANT_ESP32S3: { - 1: cg.global_ns.TOUCH_PAD_NUM1, - 2: cg.global_ns.TOUCH_PAD_NUM2, - 3: cg.global_ns.TOUCH_PAD_NUM3, - 4: cg.global_ns.TOUCH_PAD_NUM4, - 5: cg.global_ns.TOUCH_PAD_NUM5, - 6: cg.global_ns.TOUCH_PAD_NUM6, - 7: cg.global_ns.TOUCH_PAD_NUM7, - 8: cg.global_ns.TOUCH_PAD_NUM8, - 9: cg.global_ns.TOUCH_PAD_NUM9, - 10: cg.global_ns.TOUCH_PAD_NUM10, - 11: cg.global_ns.TOUCH_PAD_NUM11, - 12: cg.global_ns.TOUCH_PAD_NUM12, - 13: cg.global_ns.TOUCH_PAD_NUM13, - 14: cg.global_ns.TOUCH_PAD_NUM14, + 1: 1, + 2: 2, + 3: 3, + 4: 4, + 5: 5, + 6: 6, + 7: 7, + 8: 8, + 9: 9, + 10: 10, + 11: 11, + 12: 12, + 13: 13, + 14: 14, + }, + VARIANT_ESP32P4: { + 2: 1, + 3: 2, + 4: 3, + 5: 4, + 6: 5, + 7: 6, + 8: 7, + 9: 8, + 10: 9, + 11: 10, + 12: 11, + 13: 12, + 14: 13, + 15: 14, }, } TOUCH_PAD_DENOISE_GRADE = { - "BIT12": cg.global_ns.TOUCH_PAD_DENOISE_BIT12, - "BIT10": cg.global_ns.TOUCH_PAD_DENOISE_BIT10, - "BIT8": cg.global_ns.TOUCH_PAD_DENOISE_BIT8, - "BIT4": cg.global_ns.TOUCH_PAD_DENOISE_BIT4, + "BIT12": cg.global_ns.TOUCH_DENOISE_CHAN_RESOLUTION_BIT12, + "BIT10": cg.global_ns.TOUCH_DENOISE_CHAN_RESOLUTION_BIT10, + "BIT8": cg.global_ns.TOUCH_DENOISE_CHAN_RESOLUTION_BIT8, + "BIT4": cg.global_ns.TOUCH_DENOISE_CHAN_RESOLUTION_BIT4, } TOUCH_PAD_DENOISE_CAP_LEVEL = { - "L0": cg.global_ns.TOUCH_PAD_DENOISE_CAP_L0, - "L1": cg.global_ns.TOUCH_PAD_DENOISE_CAP_L1, - "L2": cg.global_ns.TOUCH_PAD_DENOISE_CAP_L2, - "L3": cg.global_ns.TOUCH_PAD_DENOISE_CAP_L3, - "L4": cg.global_ns.TOUCH_PAD_DENOISE_CAP_L4, - "L5": cg.global_ns.TOUCH_PAD_DENOISE_CAP_L5, - "L6": cg.global_ns.TOUCH_PAD_DENOISE_CAP_L6, - "L7": cg.global_ns.TOUCH_PAD_DENOISE_CAP_L7, + "L0": cg.global_ns.TOUCH_DENOISE_CHAN_CAP_5PF, + "L1": cg.global_ns.TOUCH_DENOISE_CHAN_CAP_6PF, + "L2": cg.global_ns.TOUCH_DENOISE_CHAN_CAP_7PF, + "L3": cg.global_ns.TOUCH_DENOISE_CHAN_CAP_9PF, + "L4": cg.global_ns.TOUCH_DENOISE_CHAN_CAP_10PF, + "L5": cg.global_ns.TOUCH_DENOISE_CHAN_CAP_12PF, + "L6": cg.global_ns.TOUCH_DENOISE_CHAN_CAP_13PF, + "L7": cg.global_ns.TOUCH_DENOISE_CHAN_CAP_14PF, } TOUCH_PAD_FILTER_MODE = { - "IIR_4": cg.global_ns.TOUCH_PAD_FILTER_IIR_4, - "IIR_8": cg.global_ns.TOUCH_PAD_FILTER_IIR_8, - "IIR_16": cg.global_ns.TOUCH_PAD_FILTER_IIR_16, - "IIR_32": cg.global_ns.TOUCH_PAD_FILTER_IIR_32, - "IIR_64": cg.global_ns.TOUCH_PAD_FILTER_IIR_64, - "IIR_128": cg.global_ns.TOUCH_PAD_FILTER_IIR_128, - "IIR_256": cg.global_ns.TOUCH_PAD_FILTER_IIR_256, - "JITTER": cg.global_ns.TOUCH_PAD_FILTER_JITTER, + "IIR_4": cg.global_ns.TOUCH_BM_IIR_FILTER_4, + "IIR_8": cg.global_ns.TOUCH_BM_IIR_FILTER_8, + "IIR_16": cg.global_ns.TOUCH_BM_IIR_FILTER_16, + "IIR_32": cg.global_ns.TOUCH_BM_IIR_FILTER_32, + "IIR_64": cg.global_ns.TOUCH_BM_IIR_FILTER_64, + "IIR_128": cg.global_ns.TOUCH_BM_IIR_FILTER_128, + "IIR_256": cg.global_ns.TOUCH_BM_IIR_FILTER_256, + "JITTER": cg.global_ns.TOUCH_BM_JITTER_FILTER, } TOUCH_PAD_SMOOTH_MODE = { - "OFF": cg.global_ns.TOUCH_PAD_SMOOTH_OFF, - "IIR_2": cg.global_ns.TOUCH_PAD_SMOOTH_IIR_2, - "IIR_4": cg.global_ns.TOUCH_PAD_SMOOTH_IIR_4, - "IIR_8": cg.global_ns.TOUCH_PAD_SMOOTH_IIR_8, + "OFF": cg.global_ns.TOUCH_SMOOTH_NO_FILTER, + "IIR_2": cg.global_ns.TOUCH_SMOOTH_IIR_FILTER_2, + "IIR_4": cg.global_ns.TOUCH_SMOOTH_IIR_FILTER_4, + "IIR_8": cg.global_ns.TOUCH_SMOOTH_IIR_FILTER_8, } LOW_VOLTAGE_REFERENCE = { - "0.5V": cg.global_ns.TOUCH_LVOLT_0V5, - "0.6V": cg.global_ns.TOUCH_LVOLT_0V6, - "0.7V": cg.global_ns.TOUCH_LVOLT_0V7, - "0.8V": cg.global_ns.TOUCH_LVOLT_0V8, + "0.5V": cg.global_ns.TOUCH_VOLT_LIM_L_0V5, + "0.6V": cg.global_ns.TOUCH_VOLT_LIM_L_0V6, + "0.7V": cg.global_ns.TOUCH_VOLT_LIM_L_0V7, + "0.8V": cg.global_ns.TOUCH_VOLT_LIM_L_0V8, } HIGH_VOLTAGE_REFERENCE = { - "2.4V": cg.global_ns.TOUCH_HVOLT_2V4, - "2.5V": cg.global_ns.TOUCH_HVOLT_2V5, - "2.6V": cg.global_ns.TOUCH_HVOLT_2V6, - "2.7V": cg.global_ns.TOUCH_HVOLT_2V7, + "2.4V": cg.global_ns.TOUCH_VOLT_LIM_H_2V4, + "2.5V": cg.global_ns.TOUCH_VOLT_LIM_H_2V5, + "2.6V": cg.global_ns.TOUCH_VOLT_LIM_H_2V6, + "2.7V": cg.global_ns.TOUCH_VOLT_LIM_H_2V7, } -VOLTAGE_ATTENUATION = { - "1.5V": cg.global_ns.TOUCH_HVOLT_ATTEN_1V5, - "1V": cg.global_ns.TOUCH_HVOLT_ATTEN_1V, - "0.5V": cg.global_ns.TOUCH_HVOLT_ATTEN_0V5, - "0V": cg.global_ns.TOUCH_HVOLT_ATTEN_0V, -} -TOUCH_PAD_WATERPROOF_SHIELD_DRIVER = { - "L0": cg.global_ns.TOUCH_PAD_SHIELD_DRV_L0, - "L1": cg.global_ns.TOUCH_PAD_SHIELD_DRV_L1, - "L2": cg.global_ns.TOUCH_PAD_SHIELD_DRV_L2, - "L3": cg.global_ns.TOUCH_PAD_SHIELD_DRV_L3, - "L4": cg.global_ns.TOUCH_PAD_SHIELD_DRV_L4, - "L5": cg.global_ns.TOUCH_PAD_SHIELD_DRV_L5, - "L6": cg.global_ns.TOUCH_PAD_SHIELD_DRV_L6, - "L7": cg.global_ns.TOUCH_PAD_SHIELD_DRV_L7, +VOLTAGE_ATTENUATION = {"1.5V", "1V", "0.5V", "0V"} + +# ESP32 V1: The new API's touch_volt_lim_h_t combines the old high_voltage_reference +# and voltage_attenuation into a single enum representing the effective upper voltage. +# Effective voltage = high_voltage_reference - voltage_attenuation +EFFECTIVE_HIGH_VOLTAGE = { + ("2.4V", "1.5V"): cg.global_ns.TOUCH_VOLT_LIM_H_0V9, + ("2.5V", "1.5V"): cg.global_ns.TOUCH_VOLT_LIM_H_1V0, + ("2.6V", "1.5V"): cg.global_ns.TOUCH_VOLT_LIM_H_1V1, + ("2.7V", "1.5V"): cg.global_ns.TOUCH_VOLT_LIM_H_1V2, + ("2.4V", "1V"): cg.global_ns.TOUCH_VOLT_LIM_H_1V4, + ("2.5V", "1V"): cg.global_ns.TOUCH_VOLT_LIM_H_1V5, + ("2.6V", "1V"): cg.global_ns.TOUCH_VOLT_LIM_H_1V6, + ("2.7V", "1V"): cg.global_ns.TOUCH_VOLT_LIM_H_1V7, + ("2.4V", "0.5V"): cg.global_ns.TOUCH_VOLT_LIM_H_1V9, + ("2.5V", "0.5V"): cg.global_ns.TOUCH_VOLT_LIM_H_2V0, + ("2.6V", "0.5V"): cg.global_ns.TOUCH_VOLT_LIM_H_2V1, + ("2.7V", "0.5V"): cg.global_ns.TOUCH_VOLT_LIM_H_2V2, + ("2.4V", "0V"): cg.global_ns.TOUCH_VOLT_LIM_H_2V4, + ("2.5V", "0V"): cg.global_ns.TOUCH_VOLT_LIM_H_2V5, + ("2.6V", "0V"): cg.global_ns.TOUCH_VOLT_LIM_H_2V6, + ("2.7V", "0V"): cg.global_ns.TOUCH_VOLT_LIM_H_2V7, } def validate_touch_pad(value): value = gpio.gpio_pin_number_validator(value) variant = get_esp32_variant() - if variant not in TOUCH_PADS: + pads = TOUCH_PADS.get(variant) + if pads is None: raise cv.Invalid(f"ESP32 variant {variant} does not support touch pads.") - - pads = TOUCH_PADS[variant] if value not in pads: raise cv.Invalid(f"Pin {value} does not support touch pads.") - return cv.enum(pads)(value) + return pads[value] # Return integer channel ID def validate_variant_vars(config): - if get_esp32_variant() == VARIANT_ESP32: - variant_vars = { + variant = get_esp32_variant() + invalid_vars = set() + if variant == VARIANT_ESP32: + invalid_vars = { CONF_DEBOUNCE_COUNT, CONF_DENOISE_GRADE, CONF_DENOISE_CAP_LEVEL, @@ -175,15 +207,14 @@ def validate_variant_vars(config): CONF_WATERPROOF_GUARD_RING, CONF_WATERPROOF_SHIELD_DRIVER, } - for vvar in variant_vars: - if vvar in config: - raise cv.Invalid(f"{vvar} is not valid on {VARIANT_ESP32}") - elif ( - get_esp32_variant() == VARIANT_ESP32S2 or get_esp32_variant() == VARIANT_ESP32S3 - ) and CONF_IIR_FILTER in config: - raise cv.Invalid( - f"{CONF_IIR_FILTER} is not valid on {VARIANT_ESP32S2} or {VARIANT_ESP32S3}" - ) + elif variant in (VARIANT_ESP32S2, VARIANT_ESP32S3, VARIANT_ESP32P4): + invalid_vars = {CONF_IIR_FILTER} + if variant == VARIANT_ESP32P4: + invalid_vars |= {CONF_DENOISE_GRADE, CONF_DENOISE_CAP_LEVEL} + unsupported = invalid_vars.intersection(config) + if unsupported: + keys = ", ".join(sorted(f"'{k}'" for k in unsupported)) + raise cv.Invalid(f"{keys} not valid on {variant}") return config @@ -218,12 +249,17 @@ CONFIG_SCHEMA = cv.All( cv.Optional(CONF_HIGH_VOLTAGE_REFERENCE, default="2.7V"): validate_voltage( HIGH_VOLTAGE_REFERENCE ), - cv.Optional(CONF_VOLTAGE_ATTENUATION, default="0V"): validate_voltage( - VOLTAGE_ATTENUATION - ), + # ESP32 V1 only: attenuates the high voltage reference + cv.SplitDefault( + CONF_VOLTAGE_ATTENUATION, + esp32="0V", + esp32_s2=cv.UNDEFINED, + esp32_s3=cv.UNDEFINED, + esp32_p4=cv.UNDEFINED, + ): validate_voltage(VOLTAGE_ATTENUATION), # ESP32 only cv.Optional(CONF_IIR_FILTER): cv.positive_time_period_milliseconds, - # ESP32-S2/S3 only + # ESP32-S2/S3/P4 only cv.Optional(CONF_DEBOUNCE_COUNT): cv.int_range(min=0, max=7), cv.Optional(CONF_FILTER_MODE): cv.enum( TOUCH_PAD_FILTER_MODE, upper=True, space="_" @@ -240,9 +276,7 @@ CONFIG_SCHEMA = cv.All( TOUCH_PAD_DENOISE_CAP_LEVEL, upper=True, space="_" ), cv.Optional(CONF_WATERPROOF_GUARD_RING): validate_touch_pad, - cv.Optional(CONF_WATERPROOF_SHIELD_DRIVER): cv.enum( - TOUCH_PAD_WATERPROOF_SHIELD_DRIVER, upper=True, space="_" - ), + cv.Optional(CONF_WATERPROOF_SHIELD_DRIVER): cv.int_range(min=0, max=7), } ).extend(cv.COMPONENT_SCHEMA), cv.has_none_or_all_keys(CONF_DENOISE_GRADE, CONF_DENOISE_CAP_LEVEL), @@ -259,6 +293,7 @@ CONFIG_SCHEMA = cv.All( esp32.VARIANT_ESP32, esp32.VARIANT_ESP32S2, esp32.VARIANT_ESP32S3, + esp32.VARIANT_ESP32P4, ] ), validate_variant_vars, @@ -266,39 +301,67 @@ CONFIG_SCHEMA = cv.All( async def to_code(config): + # New unified touch sensor driver + include_builtin_idf_component("esp_driver_touch_sens") + touch = cg.new_Pvariable(config[CONF_ID]) await cg.register_component(touch, config) cg.add(touch.set_setup_mode(config[CONF_SETUP_MODE])) - sleep_duration = int(round(config[CONF_SLEEP_DURATION].total_microseconds * 0.15)) - cg.add(touch.set_sleep_duration(sleep_duration)) + # sleep_duration -> meas_interval_us (pass microseconds directly) + cg.add(touch.set_meas_interval_us(config[CONF_SLEEP_DURATION].total_microseconds)) - measurement_duration = int( - round(config[CONF_MEASUREMENT_DURATION].total_microseconds * 7.99987793) - ) - cg.add(touch.set_measurement_duration(measurement_duration)) + variant = get_esp32_variant() - cg.add( - touch.set_low_voltage_reference( - LOW_VOLTAGE_REFERENCE[config[CONF_LOW_VOLTAGE_REFERENCE]] + # measurement_duration handling differs per variant + if variant == VARIANT_ESP32: + # V1: charge_duration_ms (convert from microseconds to milliseconds) + charge_duration_ms = ( + config[CONF_MEASUREMENT_DURATION].total_microseconds / 1000.0 ) - ) - cg.add( - touch.set_high_voltage_reference( - HIGH_VOLTAGE_REFERENCE[config[CONF_HIGH_VOLTAGE_REFERENCE]] + cg.add(touch.set_charge_duration_ms(charge_duration_ms)) + else: + # V2/V3: charge_times (approximate conversion from duration) + # The old API used clock cycles; the new API uses charge_times count. + # Default is 500 for V2/V3. Use measurement_duration as a rough scaling factor. + # 65535 / 8192 ≈ 7.9999 maps the microsecond duration to charge_times. + charge_times = int( + round(config[CONF_MEASUREMENT_DURATION].total_microseconds * (65535 / 8192)) ) - ) - cg.add( - touch.set_voltage_attenuation( - VOLTAGE_ATTENUATION[config[CONF_VOLTAGE_ATTENUATION]] - ) - ) + charge_times = max(charge_times, 1) + cg.add(touch.set_charge_times(charge_times)) - if get_esp32_variant() == VARIANT_ESP32 and CONF_IIR_FILTER in config: + # Voltage references (not applicable to P4) + if variant != VARIANT_ESP32P4: + if CONF_LOW_VOLTAGE_REFERENCE in config: + cg.add( + touch.set_low_voltage_reference( + LOW_VOLTAGE_REFERENCE[config[CONF_LOW_VOLTAGE_REFERENCE]] + ) + ) + if CONF_HIGH_VOLTAGE_REFERENCE in config: + if variant == VARIANT_ESP32: + # V1: combine high_voltage_reference with voltage_attenuation + high_ref = config[CONF_HIGH_VOLTAGE_REFERENCE] + atten = config[CONF_VOLTAGE_ATTENUATION] + cg.add( + touch.set_high_voltage_reference( + EFFECTIVE_HIGH_VOLTAGE[(high_ref, atten)] + ) + ) + else: + # V2/V3: no attenuation concept, use directly + cg.add( + touch.set_high_voltage_reference( + HIGH_VOLTAGE_REFERENCE[config[CONF_HIGH_VOLTAGE_REFERENCE]] + ) + ) + + if variant == VARIANT_ESP32 and CONF_IIR_FILTER in config: cg.add(touch.set_iir_filter(config[CONF_IIR_FILTER])) - if get_esp32_variant() == VARIANT_ESP32S2 or get_esp32_variant() == VARIANT_ESP32S3: + if variant in (VARIANT_ESP32S2, VARIANT_ESP32S3, VARIANT_ESP32P4): if CONF_FILTER_MODE in config: cg.add(touch.set_filter_mode(config[CONF_FILTER_MODE])) if CONF_DEBOUNCE_COUNT in config: diff --git a/esphome/components/esp32_touch/esp32_touch.cpp b/esphome/components/esp32_touch/esp32_touch.cpp new file mode 100644 index 0000000000..e44bc807e9 --- /dev/null +++ b/esphome/components/esp32_touch/esp32_touch.cpp @@ -0,0 +1,505 @@ +#ifdef USE_ESP32 + +#include "esp32_touch.h" +#include "esphome/core/application.h" +#include "esphome/core/log.h" +#include "esphome/core/hal.h" + +#include + +namespace esphome::esp32_touch { + +template static const char *lookup_str(const char *const (&table)[N], size_t index) { + return (index < N) ? table[index] : "UNKNOWN"; +} + +static const char *const TAG = "esp32_touch"; + +static constexpr uint32_t SETUP_MODE_LOG_INTERVAL_MS = 250; +static constexpr uint32_t INITIAL_STATE_DELAY_MS = 1500; +static constexpr uint32_t ONESHOT_SCAN_COUNT = 3; +static constexpr uint32_t ONESHOT_SCAN_TIMEOUT_MS = 2000; + +// V1: called from esp_timer context (software filter) +// V2/V3: called from ISR context +// xQueueSendFromISR is safe from both contexts. + +bool IRAM_ATTR ESP32TouchComponent::on_active_cb(touch_sensor_handle_t handle, const touch_active_event_data_t *event, + void *ctx) { + auto *comp = static_cast(ctx); + TouchEvent te{event->chan_id, true}; + BaseType_t higher = pdFALSE; + xQueueSendFromISR(comp->touch_queue_, &te, &higher); + comp->enable_loop_soon_any_context(); + return higher == pdTRUE; +} + +bool IRAM_ATTR ESP32TouchComponent::on_inactive_cb(touch_sensor_handle_t handle, + const touch_inactive_event_data_t *event, void *ctx) { + auto *comp = static_cast(ctx); + TouchEvent te{event->chan_id, false}; + BaseType_t higher = pdFALSE; + xQueueSendFromISR(comp->touch_queue_, &te, &higher); + comp->enable_loop_soon_any_context(); + return higher == pdTRUE; +} + +void ESP32TouchComponent::setup() { + if (!this->create_touch_queue_()) { + return; + } + + // Create sample config - differs per hardware version +#ifdef USE_ESP32_VARIANT_ESP32 + touch_sensor_sample_config_t sample_cfg = TOUCH_SENSOR_V1_DEFAULT_SAMPLE_CONFIG( + this->charge_duration_ms_, this->low_voltage_reference_, this->high_voltage_reference_); +#elif defined(USE_ESP32_VARIANT_ESP32P4) + // div_num=8 (data scaling divisor), coarse_freq_tune=2, fine_freq_tune=2 + touch_sensor_sample_config_t sample_cfg = TOUCH_SENSOR_V3_DEFAULT_SAMPLE_CONFIG(8, 2, 2); + sample_cfg.charge_times = this->charge_times_; +#else + // ESP32-S2/S3 (V2) + touch_sensor_sample_config_t sample_cfg = TOUCH_SENSOR_V2_DEFAULT_SAMPLE_CONFIG( + this->charge_times_, this->low_voltage_reference_, this->high_voltage_reference_); +#endif + + // Create controller + touch_sensor_config_t sens_cfg = TOUCH_SENSOR_DEFAULT_BASIC_CONFIG(1, &sample_cfg); + sens_cfg.meas_interval_us = this->meas_interval_us_; +#ifndef USE_ESP32_VARIANT_ESP32 + sens_cfg.max_meas_time_us = 0; // Disable measurement timeout (V2/V3 only) +#endif + + esp_err_t err = touch_sensor_new_controller(&sens_cfg, &this->sens_handle_); + if (err != ESP_OK) { + ESP_LOGE(TAG, "Failed to create touch controller: %s", esp_err_to_name(err)); + this->cleanup_touch_queue_(); + this->mark_failed(); + return; + } + + // Create channels for all children + for (auto *child : this->children_) { + touch_channel_config_t chan_cfg = {}; +#ifdef USE_ESP32_VARIANT_ESP32 + chan_cfg.abs_active_thresh[0] = child->get_threshold(); + chan_cfg.charge_speed = TOUCH_CHARGE_SPEED_7; + chan_cfg.init_charge_volt = TOUCH_INIT_CHARGE_VOLT_DEFAULT; + chan_cfg.group = TOUCH_CHAN_TRIG_GROUP_BOTH; +#elif defined(USE_ESP32_VARIANT_ESP32P4) + chan_cfg.active_thresh[0] = child->get_threshold(); +#else + // ESP32-S2/S3 (V2) + chan_cfg.active_thresh[0] = child->get_threshold(); + chan_cfg.charge_speed = TOUCH_CHARGE_SPEED_7; + chan_cfg.init_charge_volt = TOUCH_INIT_CHARGE_VOLT_DEFAULT; +#endif + + err = touch_sensor_new_channel(this->sens_handle_, child->get_channel_id(), &chan_cfg, &child->chan_handle_); + if (err != ESP_OK) { + ESP_LOGE(TAG, "Failed to create touch channel %d: %s", child->get_channel_id(), esp_err_to_name(err)); + this->cleanup_touch_queue_(); + this->mark_failed(); + return; + } + } + + // Configure filter +#ifdef USE_ESP32_VARIANT_ESP32 + // Software filter is REQUIRED for V1 on_active/on_inactive callbacks + { + touch_sensor_filter_config_t filter_cfg = TOUCH_SENSOR_DEFAULT_FILTER_CONFIG(); + if (this->iir_filter_enabled_()) { + filter_cfg.interval_ms = this->iir_filter_; + } + err = touch_sensor_config_filter(this->sens_handle_, &filter_cfg); + if (err != ESP_OK) { + ESP_LOGE(TAG, "Failed to configure filter: %s", esp_err_to_name(err)); + this->cleanup_touch_queue_(); + this->mark_failed(); + return; + } + } +#else + // V2/V3: Hardware benchmark filter + { + touch_sensor_filter_config_t filter_cfg = TOUCH_SENSOR_DEFAULT_FILTER_CONFIG(); + if (this->filter_configured_) { + filter_cfg.benchmark.filter_mode = this->filter_mode_; + filter_cfg.benchmark.jitter_step = this->jitter_step_; + filter_cfg.benchmark.denoise_lvl = this->noise_threshold_; + filter_cfg.data.smooth_filter = this->smooth_level_; + filter_cfg.data.debounce_cnt = this->debounce_count_; + } + err = touch_sensor_config_filter(this->sens_handle_, &filter_cfg); + if (err != ESP_OK) { + ESP_LOGW(TAG, "Failed to configure filter: %s", esp_err_to_name(err)); + } + } +#endif + +#if SOC_TOUCH_SUPPORT_DENOISE_CHAN + if (this->denoise_configured_) { + touch_denoise_chan_config_t denoise_cfg = {}; + denoise_cfg.charge_speed = TOUCH_CHARGE_SPEED_7; + denoise_cfg.init_charge_volt = TOUCH_INIT_CHARGE_VOLT_DEFAULT; + denoise_cfg.ref_cap = this->denoise_cap_level_; + denoise_cfg.resolution = this->denoise_grade_; + err = touch_sensor_config_denoise_channel(this->sens_handle_, &denoise_cfg); + if (err != ESP_OK) { + ESP_LOGW(TAG, "Failed to configure denoise: %s", esp_err_to_name(err)); + } + } +#endif + +#if SOC_TOUCH_SUPPORT_WATERPROOF + if (this->waterproof_configured_) { + touch_channel_handle_t guard_chan = nullptr; + for (auto *child : this->children_) { + if (child->get_channel_id() == this->waterproof_guard_ring_pad_) { + guard_chan = child->chan_handle_; + break; + } + } + + touch_channel_handle_t shield_chan = nullptr; + touch_channel_config_t shield_cfg = {}; +#ifdef USE_ESP32_VARIANT_ESP32P4 + shield_cfg.active_thresh[0] = 0; + err = touch_sensor_new_channel(this->sens_handle_, SOC_TOUCH_MAX_CHAN_ID, &shield_cfg, &shield_chan); +#else + shield_cfg.active_thresh[0] = 0; + shield_cfg.charge_speed = TOUCH_CHARGE_SPEED_7; + shield_cfg.init_charge_volt = TOUCH_INIT_CHARGE_VOLT_DEFAULT; + err = touch_sensor_new_channel(this->sens_handle_, TOUCH_SHIELD_CHAN_ID, &shield_cfg, &shield_chan); +#endif + if (err == ESP_OK) { + touch_waterproof_config_t wp_cfg = {}; + wp_cfg.guard_chan = guard_chan; + wp_cfg.shield_chan = shield_chan; + wp_cfg.shield_drv = this->waterproof_shield_driver_; + wp_cfg.flags.immersion_proof = 1; + err = touch_sensor_config_waterproof(this->sens_handle_, &wp_cfg); + if (err != ESP_OK) { + ESP_LOGW(TAG, "Failed to configure waterproof: %s", esp_err_to_name(err)); + } + } else { + ESP_LOGW(TAG, "Failed to create shield channel: %s", esp_err_to_name(err)); + } + } +#endif + + // Configure wakeup pads before enabling (must be done in INIT state) + this->configure_wakeup_pads_(); + + // Register callbacks + touch_event_callbacks_t cbs = {}; + cbs.on_active = on_active_cb; + cbs.on_inactive = on_inactive_cb; + err = touch_sensor_register_callbacks(this->sens_handle_, &cbs, this); + if (err != ESP_OK) { + ESP_LOGE(TAG, "Failed to register callbacks: %s", esp_err_to_name(err)); + this->cleanup_touch_queue_(); + this->mark_failed(); + return; + } + + // Enable and start scanning + err = touch_sensor_enable(this->sens_handle_); + if (err != ESP_OK) { + ESP_LOGE(TAG, "Failed to enable touch sensor: %s", esp_err_to_name(err)); + this->cleanup_touch_queue_(); + this->mark_failed(); + return; + } + + // Do initial oneshot scans to populate baseline values + for (uint32_t i = 0; i < ONESHOT_SCAN_COUNT; i++) { + err = touch_sensor_trigger_oneshot_scanning(this->sens_handle_, ONESHOT_SCAN_TIMEOUT_MS); + if (err != ESP_OK) { + ESP_LOGW(TAG, "Oneshot scan %" PRIu32 " failed: %s", i, esp_err_to_name(err)); + } + } + + err = touch_sensor_start_continuous_scanning(this->sens_handle_); + if (err != ESP_OK) { + ESP_LOGE(TAG, "Failed to start continuous scanning: %s", esp_err_to_name(err)); + this->mark_failed(); + return; + } +} + +void ESP32TouchComponent::dump_config() { +#if !defined(USE_ESP32_VARIANT_ESP32P4) + static constexpr const char *LV_STRS[] = {"0.5V", "0.6V", "0.7V", "0.8V"}; + static constexpr const char *HV_STRS[] = {"0.9V", "1.0V", "1.1V", "1.2V", "1.4V", "1.5V", "1.6V", "1.7V", + "1.9V", "2.0V", "2.1V", "2.2V", "2.4V", "2.5V", "2.6V", "2.7V"}; + const char *lv_s = lookup_str(LV_STRS, this->low_voltage_reference_); + const char *hv_s = lookup_str(HV_STRS, this->high_voltage_reference_); + + ESP_LOGCONFIG(TAG, + "Config for ESP32 Touch Hub:\n" + " Measurement interval: %.1fus\n" + " Low Voltage Reference: %s\n" + " High Voltage Reference: %s", + this->meas_interval_us_, lv_s, hv_s); +#else + ESP_LOGCONFIG(TAG, + "Config for ESP32 Touch Hub:\n" + " Measurement interval: %.1fus", + this->meas_interval_us_); +#endif + +#ifdef USE_ESP32_VARIANT_ESP32 + if (this->iir_filter_enabled_()) { + ESP_LOGCONFIG(TAG, " IIR Filter: %" PRIu32 "ms", this->iir_filter_); + } else { + ESP_LOGCONFIG(TAG, " IIR Filter: 10ms (default)"); + } +#else + if (this->filter_configured_) { + // TOUCH_BM_IIR_FILTER_256 only exists on V2, shifting JITTER's position + static constexpr const char *FILTER_STRS[] = { + "IIR_4", + "IIR_8", + "IIR_16", + "IIR_32", + "IIR_64", + "IIR_128", +#if SOC_TOUCH_SENSOR_VERSION == 2 + "IIR_256", +#endif + "JITTER", + }; + static constexpr const char *SMOOTH_STRS[] = {"OFF", "IIR_2", "IIR_4", "IIR_8"}; + const char *filter_s = lookup_str(FILTER_STRS, this->filter_mode_); + const char *smooth_s = lookup_str(SMOOTH_STRS, this->smooth_level_); + ESP_LOGCONFIG(TAG, + " Filter mode: %s\n" + " Debounce count: %" PRIu32 "\n" + " Noise threshold coefficient: %" PRIu32 "\n" + " Jitter filter step size: %" PRIu32 "\n" + " Smooth level: %s", + filter_s, this->debounce_count_, this->noise_threshold_, this->jitter_step_, smooth_s); + } + +#if SOC_TOUCH_SUPPORT_DENOISE_CHAN + if (this->denoise_configured_) { + static constexpr const char *GRADE_STRS[] = {"BIT12", "BIT10", "BIT8", "BIT4"}; + static constexpr const char *CAP_STRS[] = {"5pF", "6.4pF", "7.8pF", "9.2pF", "10.6pF", "12pF", "13.4pF", "14.8pF"}; + const char *grade_s = lookup_str(GRADE_STRS, this->denoise_grade_); + const char *cap_s = lookup_str(CAP_STRS, this->denoise_cap_level_); + ESP_LOGCONFIG(TAG, + " Denoise grade: %s\n" + " Denoise capacitance level: %s", + grade_s, cap_s); + } +#endif +#endif // !USE_ESP32_VARIANT_ESP32 + + if (this->setup_mode_) { + ESP_LOGCONFIG(TAG, " Setup Mode ENABLED"); + } + + for (auto *child : this->children_) { + LOG_BINARY_SENSOR(" ", "Touch Pad", child); + ESP_LOGCONFIG(TAG, + " Channel: %d\n" + " Threshold: %" PRIu32 "\n" + " Benchmark: %" PRIu32, + child->channel_id_, child->threshold_, child->benchmark_); + } +} + +void ESP32TouchComponent::loop() { + const uint32_t now = App.get_loop_component_start_time(); + + // In setup mode, periodically log all pad values + this->process_setup_mode_logging_(now); + + // Process queued touch events from callbacks + TouchEvent event; + while (xQueueReceive(this->touch_queue_, &event, 0) == pdTRUE) { + for (auto *child : this->children_) { + if (child->get_channel_id() != event.chan_id) { + continue; + } + + // Read current smooth value + uint32_t value = 0; + touch_channel_read_data(child->chan_handle_, TOUCH_CHAN_DATA_TYPE_SMOOTH, &value); + child->value_ = value; + +#ifndef USE_ESP32_VARIANT_ESP32 + // V2/V3: also read benchmark + uint32_t benchmark = 0; + touch_channel_read_data(child->chan_handle_, TOUCH_CHAN_DATA_TYPE_BENCHMARK, &benchmark); + child->benchmark_ = benchmark; +#endif + + bool new_state = event.is_active; + + if (new_state != child->last_state_) { + child->initial_state_published_ = true; + child->last_state_ = new_state; + child->publish_state(new_state); +#ifdef USE_ESP32_VARIANT_ESP32 + ESP_LOGV(TAG, "Touch Pad '%s' state: %s (value: %" PRIu32 ", threshold: %" PRIu32 ")", + child->get_name().c_str(), ONOFF(new_state), value, child->get_threshold()); +#else + if (new_state) { + ESP_LOGV(TAG, "Touch Pad '%s' state: ON (value: %" PRIu32 ", benchmark: %" PRIu32 ", threshold: %" PRIu32 ")", + child->get_name().c_str(), value, benchmark, child->get_threshold()); + } else { + ESP_LOGV(TAG, "Touch Pad '%s' state: OFF", child->get_name().c_str()); + } +#endif + } + break; + } + } + + // Publish initial OFF state for sensors that haven't received events yet + bool all_initial_published = true; + for (auto *child : this->children_) { + this->publish_initial_state_if_needed_(child, now); + if (!child->initial_state_published_) { + all_initial_published = false; + } + } + + // Only disable loop once all initial states are published + if (!this->setup_mode_ && all_initial_published) { + this->disable_loop(); + } +} + +void ESP32TouchComponent::on_shutdown() { + if (this->sens_handle_ == nullptr) + return; + + touch_sensor_stop_continuous_scanning(this->sens_handle_); + touch_sensor_disable(this->sens_handle_); + + for (auto *child : this->children_) { + if (child->chan_handle_ != nullptr) { + touch_sensor_del_channel(child->chan_handle_); + child->chan_handle_ = nullptr; + } + } + + touch_sensor_del_controller(this->sens_handle_); + this->sens_handle_ = nullptr; + + this->cleanup_touch_queue_(); +} + +bool ESP32TouchComponent::create_touch_queue_() { + size_t queue_size = this->children_.size() * 4; + if (queue_size < 8) + queue_size = 8; + + this->touch_queue_ = xQueueCreate(queue_size, sizeof(TouchEvent)); + + if (this->touch_queue_ == nullptr) { + ESP_LOGE(TAG, "Failed to create touch event queue of size %" PRIu32, (uint32_t) queue_size); + this->mark_failed(); + return false; + } + return true; +} + +void ESP32TouchComponent::cleanup_touch_queue_() { + if (this->touch_queue_) { + vQueueDelete(this->touch_queue_); + this->touch_queue_ = nullptr; + } +} + +void ESP32TouchComponent::configure_wakeup_pads_() { +#if SOC_TOUCH_SUPPORT_SLEEP_WAKEUP + bool has_wakeup = false; + for (auto *child : this->children_) { + if (child->get_wakeup_threshold() != 0) { + has_wakeup = true; + break; + } + } + + if (!has_wakeup) + return; + +#ifdef USE_ESP32_VARIANT_ESP32 + // V1: Simple sleep config - threshold is set via channel config's abs_active_thresh + touch_sleep_config_t sleep_cfg = TOUCH_SENSOR_DEFAULT_DSLP_CONFIG(); + sleep_cfg.deep_slp_sens_cfg = nullptr; + esp_err_t err = touch_sensor_config_sleep_wakeup(this->sens_handle_, &sleep_cfg); + if (err != ESP_OK) { + ESP_LOGW(TAG, "Failed to configure touch sleep wakeup: %s", esp_err_to_name(err)); + } +#else + // V2/V3: Need to specify a deep sleep channel and threshold + touch_channel_handle_t wakeup_chan = nullptr; + uint32_t wakeup_thresh = 0; + for (auto *child : this->children_) { + if (child->get_wakeup_threshold() != 0) { + wakeup_chan = child->chan_handle_; + wakeup_thresh = child->get_wakeup_threshold(); + break; // Only one deep sleep wakeup channel is supported + } + } + + if (wakeup_chan != nullptr) { + touch_sleep_config_t sleep_cfg = TOUCH_SENSOR_DEFAULT_DSLP_CONFIG(); + sleep_cfg.deep_slp_chan = wakeup_chan; + sleep_cfg.deep_slp_thresh[0] = wakeup_thresh; + sleep_cfg.deep_slp_sens_cfg = nullptr; + esp_err_t err = touch_sensor_config_sleep_wakeup(this->sens_handle_, &sleep_cfg); + if (err != ESP_OK) { + ESP_LOGW(TAG, "Failed to configure touch sleep wakeup: %s", esp_err_to_name(err)); + } + } +#endif +#endif // SOC_TOUCH_SUPPORT_SLEEP_WAKEUP +} + +void ESP32TouchComponent::process_setup_mode_logging_(uint32_t now) { + if (this->setup_mode_ && now - this->setup_mode_last_log_print_ > SETUP_MODE_LOG_INTERVAL_MS) { + for (auto *child : this->children_) { + if (child->chan_handle_ == nullptr) + continue; + + uint32_t smooth_value = 0; + touch_channel_read_data(child->chan_handle_, TOUCH_CHAN_DATA_TYPE_SMOOTH, &smooth_value); + child->value_ = smooth_value; + +#ifdef USE_ESP32_VARIANT_ESP32 + ESP_LOGD(TAG, "Touch Pad '%s' (Ch%d): %" PRIu32, child->get_name().c_str(), child->channel_id_, smooth_value); +#else + uint32_t benchmark = 0; + touch_channel_read_data(child->chan_handle_, TOUCH_CHAN_DATA_TYPE_BENCHMARK, &benchmark); + child->benchmark_ = benchmark; + int32_t difference = static_cast(smooth_value) - static_cast(benchmark); + ESP_LOGD(TAG, + "Touch Pad '%s' (Ch%d): value=%" PRIu32 ", benchmark=%" PRIu32 ", difference=%" PRId32 + " (set threshold < %" PRId32 " to detect touch)", + child->get_name().c_str(), child->channel_id_, smooth_value, benchmark, difference, difference); +#endif + } + this->setup_mode_last_log_print_ = now; + } +} + +void ESP32TouchComponent::publish_initial_state_if_needed_(ESP32TouchBinarySensor *child, uint32_t now) { + if (!child->initial_state_published_) { + if (now > INITIAL_STATE_DELAY_MS) { + child->publish_initial_state(false); + child->initial_state_published_ = true; + ESP_LOGV(TAG, "Touch Pad '%s' state: OFF (initial)", child->get_name().c_str()); + } + } +} + +} // namespace esphome::esp32_touch + +#endif // USE_ESP32 diff --git a/esphome/components/esp32_touch/esp32_touch.h b/esphome/components/esp32_touch/esp32_touch.h index 812c746301..d51b2d4922 100644 --- a/esphome/components/esp32_touch/esp32_touch.h +++ b/esphome/components/esp32_touch/esp32_touch.h @@ -4,234 +4,180 @@ #include "esphome/core/component.h" #include "esphome/components/binary_sensor/binary_sensor.h" -#include #include -#include +#include #include #include -namespace esphome { -namespace esp32_touch { +namespace esphome::esp32_touch { // IMPORTANT: Touch detection logic differs between ESP32 variants: -// - ESP32 v1 (original): Touch detected when value < threshold (capacitance increase causes value decrease) -// - ESP32-S2/S3 v2: Touch detected when value > threshold (capacitance increase causes value increase) -// This inversion is due to different hardware implementations between chip generations. +// - ESP32 v1 (original): Touch detected when value < threshold (absolute threshold, capacitance increase causes +// value decrease) +// - ESP32-S2/S3 v2, ESP32-P4 v3: Touch detected when (smooth - benchmark) > threshold (relative threshold) // -// INTERRUPT BEHAVIOR: -// - ESP32 v1: Interrupts fire when ANY pad is touched and continue while touched. -// Releases are detected by timeout since hardware doesn't generate release interrupts. -// - ESP32-S2/S3 v2: Hardware supports both touch and release interrupts, but release -// interrupts are unreliable and sometimes don't fire. We now only use touch interrupts -// and detect releases via timeout, similar to v1. - -static const uint32_t SETUP_MODE_LOG_INTERVAL_MS = 250; +// CALLBACK BEHAVIOR: +// - ESP32 v1: on_active/on_inactive fire from a software filter timer (esp_timer context). +// The software filter MUST be configured for these callbacks to fire. +// - ESP32-S2/S3 v2, ESP32-P4 v3: on_active/on_inactive fire from hardware ISR context. +// Release detection via on_inactive is used, with timeout as safety fallback. class ESP32TouchBinarySensor; -class ESP32TouchComponent : public Component { +class ESP32TouchComponent final : public Component { public: void register_touch_pad(ESP32TouchBinarySensor *pad) { this->children_.push_back(pad); } void set_setup_mode(bool setup_mode) { this->setup_mode_ = setup_mode; } - void set_sleep_duration(uint16_t sleep_duration) { this->sleep_cycle_ = sleep_duration; } - void set_measurement_duration(uint16_t meas_cycle) { this->meas_cycle_ = meas_cycle; } - void set_low_voltage_reference(touch_low_volt_t low_voltage_reference) { + void set_meas_interval_us(float meas_interval_us) { this->meas_interval_us_ = meas_interval_us; } + +#ifdef USE_ESP32_VARIANT_ESP32 + void set_charge_duration_ms(float charge_duration_ms) { this->charge_duration_ms_ = charge_duration_ms; } +#else + void set_charge_times(uint32_t charge_times) { this->charge_times_ = charge_times; } +#endif + +#if !defined(USE_ESP32_VARIANT_ESP32P4) + void set_low_voltage_reference(touch_volt_lim_l_t low_voltage_reference) { this->low_voltage_reference_ = low_voltage_reference; } - void set_high_voltage_reference(touch_high_volt_t high_voltage_reference) { + void set_high_voltage_reference(touch_volt_lim_h_t high_voltage_reference) { this->high_voltage_reference_ = high_voltage_reference; } - void set_voltage_attenuation(touch_volt_atten_t voltage_attenuation) { - this->voltage_attenuation_ = voltage_attenuation; - } +#endif void setup() override; void dump_config() override; void loop() override; - float get_setup_priority() const override { return setup_priority::DATA; } void on_shutdown() override; -#if defined(USE_ESP32_VARIANT_ESP32S2) || defined(USE_ESP32_VARIANT_ESP32S3) - void set_filter_mode(touch_filter_mode_t filter_mode) { this->filter_mode_ = filter_mode; } - void set_debounce_count(uint32_t debounce_count) { this->debounce_count_ = debounce_count; } - void set_noise_threshold(uint32_t noise_threshold) { this->noise_threshold_ = noise_threshold; } - void set_jitter_step(uint32_t jitter_step) { this->jitter_step_ = jitter_step; } - void set_smooth_level(touch_smooth_mode_t smooth_level) { this->smooth_level_ = smooth_level; } - void set_denoise_grade(touch_pad_denoise_grade_t denoise_grade) { this->grade_ = denoise_grade; } - void set_denoise_cap(touch_pad_denoise_cap_t cap_level) { this->cap_level_ = cap_level; } - void set_waterproof_guard_ring_pad(touch_pad_t pad) { this->waterproof_guard_ring_pad_ = pad; } - void set_waterproof_shield_driver(touch_pad_shield_driver_t drive_capability) { +#if defined(USE_ESP32_VARIANT_ESP32S2) || defined(USE_ESP32_VARIANT_ESP32S3) || defined(USE_ESP32_VARIANT_ESP32P4) + void set_filter_mode(touch_benchmark_filter_mode_t filter_mode) { + this->filter_mode_ = filter_mode; + this->filter_configured_ = true; + } + void set_debounce_count(uint32_t debounce_count) { + this->debounce_count_ = debounce_count; + this->filter_configured_ = true; + } + void set_noise_threshold(uint32_t noise_threshold) { + this->noise_threshold_ = noise_threshold; + this->filter_configured_ = true; + } + void set_jitter_step(uint32_t jitter_step) { + this->jitter_step_ = jitter_step; + this->filter_configured_ = true; + } + void set_smooth_level(touch_smooth_filter_mode_t smooth_level) { + this->smooth_level_ = smooth_level; + this->filter_configured_ = true; + } +#if SOC_TOUCH_SUPPORT_DENOISE_CHAN + void set_denoise_grade(touch_denoise_chan_resolution_t denoise_grade) { + this->denoise_grade_ = denoise_grade; + this->denoise_configured_ = true; + } + void set_denoise_cap(touch_denoise_chan_cap_t cap_level) { + this->denoise_cap_level_ = cap_level; + this->denoise_configured_ = true; + } +#endif + void set_waterproof_guard_ring_pad(int channel_id) { + this->waterproof_guard_ring_pad_ = channel_id; + this->waterproof_configured_ = true; + } + void set_waterproof_shield_driver(uint32_t drive_capability) { this->waterproof_shield_driver_ = drive_capability; + this->waterproof_configured_ = true; } #else void set_iir_filter(uint32_t iir_filter) { this->iir_filter_ = iir_filter; } #endif protected: + // Unified touch event for queue communication + struct TouchEvent { + int chan_id; + bool is_active; + }; + // Common helper methods - void dump_config_base_(); - void dump_config_sensors_(); bool create_touch_queue_(); void cleanup_touch_queue_(); void configure_wakeup_pads_(); // Helper methods for loop() logic void process_setup_mode_logging_(uint32_t now); - bool should_check_for_releases_(uint32_t now); void publish_initial_state_if_needed_(ESP32TouchBinarySensor *child, uint32_t now); - void check_and_disable_loop_if_all_released_(size_t pads_off); - void calculate_release_timeout_(); + + // Unified callbacks for new API + static bool on_active_cb(touch_sensor_handle_t handle, const touch_active_event_data_t *event, void *ctx); + static bool on_inactive_cb(touch_sensor_handle_t handle, const touch_inactive_event_data_t *event, void *ctx); // Common members std::vector children_; bool setup_mode_{false}; uint32_t setup_mode_last_log_print_{0}; - uint32_t last_release_check_{0}; - uint32_t release_timeout_ms_{1500}; - uint32_t release_check_interval_ms_{50}; + + // Controller handle (new API) + touch_sensor_handle_t sens_handle_{nullptr}; + QueueHandle_t touch_queue_{nullptr}; // Common configuration parameters - uint16_t sleep_cycle_{4095}; - uint16_t meas_cycle_{65535}; - touch_low_volt_t low_voltage_reference_{TOUCH_LVOLT_0V5}; - touch_high_volt_t high_voltage_reference_{TOUCH_HVOLT_2V7}; - touch_volt_atten_t voltage_attenuation_{TOUCH_HVOLT_ATTEN_0V}; + float meas_interval_us_{320.0f}; - // Common constants - static constexpr uint32_t MINIMUM_RELEASE_TIME_MS = 100; +#ifdef USE_ESP32_VARIANT_ESP32 + float charge_duration_ms_{1.0f}; +#else + uint32_t charge_times_{500}; +#endif - // ==================== PLATFORM SPECIFIC ==================== +#if !defined(USE_ESP32_VARIANT_ESP32P4) + touch_volt_lim_l_t low_voltage_reference_{TOUCH_VOLT_LIM_L_0V5}; + touch_volt_lim_h_t high_voltage_reference_{TOUCH_VOLT_LIM_H_2V7}; +#endif #ifdef USE_ESP32_VARIANT_ESP32 // ESP32 v1 specific - - static void touch_isr_handler(void *arg); - QueueHandle_t touch_queue_{nullptr}; - - private: - // Touch event structure for ESP32 v1 - // Contains touch pad info, value, and touch state for queue communication - struct TouchPadEventV1 { - touch_pad_t pad; - uint32_t value; - bool is_touched; - }; - - protected: uint32_t iir_filter_{0}; bool iir_filter_enabled_() const { return this->iir_filter_ > 0; } -#elif defined(USE_ESP32_VARIANT_ESP32S2) || defined(USE_ESP32_VARIANT_ESP32S3) - // ESP32-S2/S3 v2 specific - static void touch_isr_handler(void *arg); - QueueHandle_t touch_queue_{nullptr}; +#elif defined(USE_ESP32_VARIANT_ESP32S2) || defined(USE_ESP32_VARIANT_ESP32S3) || defined(USE_ESP32_VARIANT_ESP32P4) + // ESP32-S2/S3/P4 v2/v3 specific - private: - // Touch event structure for ESP32 v2 (S2/S3) - // Contains touch pad and interrupt mask for queue communication - struct TouchPadEventV2 { - touch_pad_t pad; - uint32_t intr_mask; - }; - - protected: - // Filter configuration - touch_filter_mode_t filter_mode_{TOUCH_PAD_FILTER_MAX}; + // Filter configuration - use sentinel values to detect "not configured" + touch_benchmark_filter_mode_t filter_mode_{TOUCH_BM_JITTER_FILTER}; uint32_t debounce_count_{0}; uint32_t noise_threshold_{0}; uint32_t jitter_step_{0}; - touch_smooth_mode_t smooth_level_{TOUCH_PAD_SMOOTH_MAX}; + touch_smooth_filter_mode_t smooth_level_{TOUCH_SMOOTH_NO_FILTER}; + bool filter_configured_{false}; +#if SOC_TOUCH_SUPPORT_DENOISE_CHAN // Denoise configuration - touch_pad_denoise_grade_t grade_{TOUCH_PAD_DENOISE_MAX}; - touch_pad_denoise_cap_t cap_level_{TOUCH_PAD_DENOISE_CAP_MAX}; - - // Waterproof configuration - touch_pad_t waterproof_guard_ring_pad_{TOUCH_PAD_MAX}; - touch_pad_shield_driver_t waterproof_shield_driver_{TOUCH_PAD_SHIELD_DRV_MAX}; - - bool filter_configured_() const { - return (this->filter_mode_ != TOUCH_PAD_FILTER_MAX) && (this->smooth_level_ != TOUCH_PAD_SMOOTH_MAX); - } - bool denoise_configured_() const { - return (this->grade_ != TOUCH_PAD_DENOISE_MAX) && (this->cap_level_ != TOUCH_PAD_DENOISE_CAP_MAX); - } - bool waterproof_configured_() const { - return (this->waterproof_guard_ring_pad_ != TOUCH_PAD_MAX) && - (this->waterproof_shield_driver_ != TOUCH_PAD_SHIELD_DRV_MAX); - } - - // Helper method to read touch values - non-blocking operation - // Returns the current touch pad value using either filtered or raw reading - // based on the filter configuration - uint32_t read_touch_value(touch_pad_t pad) const; - - // Helper to update touch state with a known state and value - void update_touch_state_(ESP32TouchBinarySensor *child, bool is_touched, uint32_t value); - - // Helper to read touch value and update state for a given child - bool check_and_update_touch_state_(ESP32TouchBinarySensor *child); + touch_denoise_chan_resolution_t denoise_grade_{TOUCH_DENOISE_CHAN_RESOLUTION_BIT12}; + touch_denoise_chan_cap_t denoise_cap_level_{TOUCH_DENOISE_CHAN_CAP_5PF}; + bool denoise_configured_{false}; #endif - // Helper functions for dump_config - common to both implementations - static const char *get_low_voltage_reference_str(touch_low_volt_t ref) { - switch (ref) { - case TOUCH_LVOLT_0V5: - return "0.5V"; - case TOUCH_LVOLT_0V6: - return "0.6V"; - case TOUCH_LVOLT_0V7: - return "0.7V"; - case TOUCH_LVOLT_0V8: - return "0.8V"; - default: - return "UNKNOWN"; - } - } - - static const char *get_high_voltage_reference_str(touch_high_volt_t ref) { - switch (ref) { - case TOUCH_HVOLT_2V4: - return "2.4V"; - case TOUCH_HVOLT_2V5: - return "2.5V"; - case TOUCH_HVOLT_2V6: - return "2.6V"; - case TOUCH_HVOLT_2V7: - return "2.7V"; - default: - return "UNKNOWN"; - } - } - - static const char *get_voltage_attenuation_str(touch_volt_atten_t atten) { - switch (atten) { - case TOUCH_HVOLT_ATTEN_1V5: - return "1.5V"; - case TOUCH_HVOLT_ATTEN_1V: - return "1V"; - case TOUCH_HVOLT_ATTEN_0V5: - return "0.5V"; - case TOUCH_HVOLT_ATTEN_0V: - return "0V"; - default: - return "UNKNOWN"; - } - } + // Waterproof configuration + int waterproof_guard_ring_pad_{-1}; + uint32_t waterproof_shield_driver_{0}; + bool waterproof_configured_{false}; +#endif }; /// Simple helper class to expose a touch pad value as a binary sensor. class ESP32TouchBinarySensor : public binary_sensor::BinarySensor { public: - ESP32TouchBinarySensor(touch_pad_t touch_pad, uint32_t threshold, uint32_t wakeup_threshold) - : touch_pad_(touch_pad), threshold_(threshold), wakeup_threshold_(wakeup_threshold) {} + ESP32TouchBinarySensor(int channel_id, uint32_t threshold, uint32_t wakeup_threshold) + : channel_id_(channel_id), threshold_(threshold), wakeup_threshold_(wakeup_threshold) {} - touch_pad_t get_touch_pad() const { return this->touch_pad_; } + int get_channel_id() const { return this->channel_id_; } uint32_t get_threshold() const { return this->threshold_; } void set_threshold(uint32_t threshold) { this->threshold_ = threshold; } @@ -243,39 +189,22 @@ class ESP32TouchBinarySensor : public binary_sensor::BinarySensor { uint32_t get_wakeup_threshold() const { return this->wakeup_threshold_; } -#if defined(USE_ESP32_VARIANT_ESP32S2) || defined(USE_ESP32_VARIANT_ESP32S3) - /// Ensure benchmark value is read (v2 touch hardware only). - /// Called from multiple places - kept as helper to document shared usage. - void ensure_benchmark_read() { - if (this->benchmark_ == 0) { - touch_pad_read_benchmark(this->touch_pad_, &this->benchmark_); - } - } -#endif - protected: friend ESP32TouchComponent; - touch_pad_t touch_pad_{TOUCH_PAD_MAX}; + int channel_id_; + touch_channel_handle_t chan_handle_{nullptr}; uint32_t threshold_{0}; - uint32_t benchmark_{}; + uint32_t benchmark_{0}; /// Stores the last raw touch measurement value. uint32_t value_{0}; bool last_state_{false}; const uint32_t wakeup_threshold_{0}; // Track last touch time for timeout-based release detection - // Design note: last_touch_time_ does not require synchronization primitives because: - // 1. ESP32 guarantees atomic 32-bit aligned reads/writes - // 2. ISR only writes timestamps, main loop only reads - // 3. Timing tolerance allows for occasional stale reads (50ms check interval) - // 4. Queue operations provide implicit memory barriers - // Using atomic/critical sections would add overhead without meaningful benefit - uint32_t last_touch_time_{}; - bool initial_state_published_{}; + bool initial_state_published_{false}; }; -} // namespace esp32_touch -} // namespace esphome +} // namespace esphome::esp32_touch #endif diff --git a/esphome/components/esp32_touch/esp32_touch_common.cpp b/esphome/components/esp32_touch/esp32_touch_common.cpp deleted file mode 100644 index 429b5173be..0000000000 --- a/esphome/components/esp32_touch/esp32_touch_common.cpp +++ /dev/null @@ -1,173 +0,0 @@ -#ifdef USE_ESP32 - -#include "esp32_touch.h" -#include "esphome/core/log.h" -#include - -#include "soc/rtc.h" - -namespace esphome { -namespace esp32_touch { - -static const char *const TAG = "esp32_touch"; - -void ESP32TouchComponent::dump_config_base_() { - const char *lv_s = get_low_voltage_reference_str(this->low_voltage_reference_); - const char *hv_s = get_high_voltage_reference_str(this->high_voltage_reference_); - const char *atten_s = get_voltage_attenuation_str(this->voltage_attenuation_); - - ESP_LOGCONFIG(TAG, - "Config for ESP32 Touch Hub:\n" - " Meas cycle: %.2fms\n" - " Sleep cycle: %.2fms\n" - " Low Voltage Reference: %s\n" - " High Voltage Reference: %s\n" - " Voltage Attenuation: %s\n" - " Release Timeout: %" PRIu32 "ms\n", - this->meas_cycle_ / (8000000.0f / 1000.0f), this->sleep_cycle_ / (150000.0f / 1000.0f), lv_s, hv_s, - atten_s, this->release_timeout_ms_); -} - -void ESP32TouchComponent::dump_config_sensors_() { - for (auto *child : this->children_) { - LOG_BINARY_SENSOR(" ", "Touch Pad", child); - ESP_LOGCONFIG(TAG, - " Pad: T%u\n" - " Threshold: %" PRIu32 "\n" - " Benchmark: %" PRIu32, - (unsigned) child->touch_pad_, child->threshold_, child->benchmark_); - } -} - -bool ESP32TouchComponent::create_touch_queue_() { - // Queue size calculation: children * 4 allows for burst scenarios where ISR - // fires multiple times before main loop processes. - size_t queue_size = this->children_.size() * 4; - if (queue_size < 8) - queue_size = 8; - -#ifdef USE_ESP32_VARIANT_ESP32 - this->touch_queue_ = xQueueCreate(queue_size, sizeof(TouchPadEventV1)); -#else - this->touch_queue_ = xQueueCreate(queue_size, sizeof(TouchPadEventV2)); -#endif - - if (this->touch_queue_ == nullptr) { - ESP_LOGE(TAG, "Failed to create touch event queue of size %" PRIu32, (uint32_t) queue_size); - this->mark_failed(); - return false; - } - return true; -} - -void ESP32TouchComponent::cleanup_touch_queue_() { - if (this->touch_queue_) { - vQueueDelete(this->touch_queue_); - this->touch_queue_ = nullptr; - } -} - -void ESP32TouchComponent::configure_wakeup_pads_() { - bool is_wakeup_source = false; - - // Check if any pad is configured for wakeup - for (auto *child : this->children_) { - if (child->get_wakeup_threshold() != 0) { - is_wakeup_source = true; - -#ifdef USE_ESP32_VARIANT_ESP32 - // ESP32 v1: No filter available when using as wake-up source. - touch_pad_config(child->get_touch_pad(), child->get_wakeup_threshold()); -#else - // ESP32-S2/S3 v2: Set threshold for wakeup - touch_pad_set_thresh(child->get_touch_pad(), child->get_wakeup_threshold()); -#endif - } - } - - if (!is_wakeup_source) { - // If no pad is configured for wakeup, deinitialize touch pad - touch_pad_deinit(); - } -} - -void ESP32TouchComponent::process_setup_mode_logging_(uint32_t now) { - if (this->setup_mode_ && now - this->setup_mode_last_log_print_ > SETUP_MODE_LOG_INTERVAL_MS) { - for (auto *child : this->children_) { -#ifdef USE_ESP32_VARIANT_ESP32 - ESP_LOGD(TAG, "Touch Pad '%s' (T%" PRIu32 "): %" PRIu32, child->get_name().c_str(), - (uint32_t) child->get_touch_pad(), child->value_); -#else - // Read the value being used for touch detection - uint32_t value = this->read_touch_value(child->get_touch_pad()); - // Store the value for get_value() access in lambdas - child->value_ = value; - // Read benchmark if not already read - child->ensure_benchmark_read(); - // Calculate difference to help user set threshold - // For ESP32-S2/S3 v2: touch detected when value > benchmark + threshold - // So threshold should be < (value - benchmark) when touched - int32_t difference = static_cast(value) - static_cast(child->benchmark_); - ESP_LOGD(TAG, - "Touch Pad '%s' (T%d): value=%d, benchmark=%" PRIu32 ", difference=%" PRId32 " (set threshold < %" PRId32 - " to detect touch)", - child->get_name().c_str(), child->get_touch_pad(), value, child->benchmark_, difference, difference); -#endif - } - this->setup_mode_last_log_print_ = now; - } -} - -bool ESP32TouchComponent::should_check_for_releases_(uint32_t now) { - if (now - this->last_release_check_ < this->release_check_interval_ms_) { - return false; - } - this->last_release_check_ = now; - return true; -} - -void ESP32TouchComponent::publish_initial_state_if_needed_(ESP32TouchBinarySensor *child, uint32_t now) { - if (!child->initial_state_published_) { - // Check if enough time has passed since startup - if (now > this->release_timeout_ms_) { - child->publish_initial_state(false); - child->initial_state_published_ = true; - ESP_LOGV(TAG, "Touch Pad '%s' state: OFF (initial)", child->get_name().c_str()); - } - } -} - -void ESP32TouchComponent::check_and_disable_loop_if_all_released_(size_t pads_off) { - // Disable the loop to save CPU cycles when all pads are off and not in setup mode. - if (pads_off == this->children_.size() && !this->setup_mode_) { - this->disable_loop(); - } -} - -void ESP32TouchComponent::calculate_release_timeout_() { - // Calculate release timeout based on sleep cycle - // Design note: Hardware limitation - interrupts only fire reliably on touch (not release) - // We must use timeout-based detection for release events - // Formula: 3 sleep cycles converted to ms, with MINIMUM_RELEASE_TIME_MS minimum - // Per ESP-IDF docs: t_sleep = sleep_cycle / SOC_CLK_RC_SLOW_FREQ_APPROX - - uint32_t rtc_freq = rtc_clk_slow_freq_get_hz(); - - // Calculate timeout as 3 sleep cycles - this->release_timeout_ms_ = (this->sleep_cycle_ * 1000 * 3) / rtc_freq; - - if (this->release_timeout_ms_ < MINIMUM_RELEASE_TIME_MS) { - this->release_timeout_ms_ = MINIMUM_RELEASE_TIME_MS; - } - - // Check for releases at 1/4 the timeout interval - // Since hardware doesn't generate reliable release interrupts, we must poll - // for releases in the main loop. Checking at 1/4 the timeout interval provides - // a good balance between responsiveness and efficiency. - this->release_check_interval_ms_ = this->release_timeout_ms_ / 4; -} - -} // namespace esp32_touch -} // namespace esphome - -#endif // USE_ESP32 diff --git a/esphome/components/esp32_touch/esp32_touch_v1.cpp b/esphome/components/esp32_touch/esp32_touch_v1.cpp deleted file mode 100644 index ffb805e008..0000000000 --- a/esphome/components/esp32_touch/esp32_touch_v1.cpp +++ /dev/null @@ -1,244 +0,0 @@ -#ifdef USE_ESP32_VARIANT_ESP32 - -#include "esp32_touch.h" -#include "esphome/core/application.h" -#include "esphome/core/log.h" -#include "esphome/core/hal.h" - -#include -#include - -// Include HAL for ISR-safe touch reading -#include "hal/touch_sensor_ll.h" - -namespace esphome { -namespace esp32_touch { - -static const char *const TAG = "esp32_touch"; - -static const uint32_t SETUP_MODE_THRESHOLD = 0xFFFF; - -void ESP32TouchComponent::setup() { - // Create queue for touch events - // Queue size calculation: children * 4 allows for burst scenarios where ISR - // fires multiple times before main loop processes. This is important because - // ESP32 v1 scans all pads on each interrupt, potentially sending multiple events. - if (!this->create_touch_queue_()) { - return; - } - - touch_pad_init(); - touch_pad_set_fsm_mode(TOUCH_FSM_MODE_TIMER); - - // Set up IIR filter if enabled - if (this->iir_filter_enabled_()) { - touch_pad_filter_start(this->iir_filter_); - } - - // Configure measurement parameters -#if ESP_IDF_VERSION_MAJOR >= 5 - touch_pad_set_measurement_clock_cycles(this->meas_cycle_); - touch_pad_set_measurement_interval(this->sleep_cycle_); -#else - touch_pad_set_meas_time(this->sleep_cycle_, this->meas_cycle_); -#endif - touch_pad_set_voltage(this->high_voltage_reference_, this->low_voltage_reference_, this->voltage_attenuation_); - - // Configure each touch pad - for (auto *child : this->children_) { - if (this->setup_mode_) { - touch_pad_config(child->get_touch_pad(), SETUP_MODE_THRESHOLD); - } else { - touch_pad_config(child->get_touch_pad(), child->get_threshold()); - } - } - - // Register ISR handler - esp_err_t err = touch_pad_isr_register(touch_isr_handler, this); - if (err != ESP_OK) { - ESP_LOGE(TAG, "Failed to register touch ISR: %s", esp_err_to_name(err)); - this->cleanup_touch_queue_(); - this->mark_failed(); - return; - } - - // Calculate release timeout based on sleep cycle - this->calculate_release_timeout_(); - - // Enable touch pad interrupt - touch_pad_intr_enable(); -} - -void ESP32TouchComponent::dump_config() { - this->dump_config_base_(); - - if (this->iir_filter_enabled_()) { - ESP_LOGCONFIG(TAG, " IIR Filter: %" PRIu32 "ms", this->iir_filter_); - } else { - ESP_LOGCONFIG(TAG, " IIR Filter DISABLED"); - } - - if (this->setup_mode_) { - ESP_LOGCONFIG(TAG, " Setup Mode ENABLED"); - } - - this->dump_config_sensors_(); -} - -void ESP32TouchComponent::loop() { - const uint32_t now = App.get_loop_component_start_time(); - - // Print debug info for all pads in setup mode - this->process_setup_mode_logging_(now); - - // Process any queued touch events from interrupts - // Note: Events are only sent by ISR for pads that were measured in that cycle (value != 0) - // This is more efficient than sending all pad states every interrupt - TouchPadEventV1 event; - while (xQueueReceive(this->touch_queue_, &event, 0) == pdTRUE) { - // Find the corresponding sensor - O(n) search is acceptable since events are infrequent - for (auto *child : this->children_) { - if (child->get_touch_pad() != event.pad) { - continue; - } - - // Found matching pad - process it - child->value_ = event.value; - - // The interrupt gives us the touch state directly - bool new_state = event.is_touched; - - // Track when we last saw this pad as touched - if (new_state) { - child->last_touch_time_ = now; - } - - // Only publish if state changed - this filters out repeated events - if (new_state != child->last_state_) { - child->initial_state_published_ = true; - child->last_state_ = new_state; - child->publish_state(new_state); - // Original ESP32: ISR only fires when touched, release is detected by timeout - // Note: ESP32 v1 uses inverted logic - touched when value < threshold - ESP_LOGV(TAG, "Touch Pad '%s' state: %s (value: %" PRIu32 " < threshold: %" PRIu32 ")", - child->get_name().c_str(), ONOFF(new_state), event.value, child->get_threshold()); - } - break; // Exit inner loop after processing matching pad - } - } - - // Check for released pads periodically - if (!this->should_check_for_releases_(now)) { - return; - } - - size_t pads_off = 0; - for (auto *child : this->children_) { - // Handle initial state publication after startup - this->publish_initial_state_if_needed_(child, now); - - if (child->last_state_) { - // Pad is currently in touched state - check for release timeout - // Using subtraction handles 32-bit rollover correctly - uint32_t time_diff = now - child->last_touch_time_; - - // Check if we haven't seen this pad recently - if (time_diff > this->release_timeout_ms_) { - // Haven't seen this pad recently, assume it's released - child->last_state_ = false; - child->publish_state(false); - ESP_LOGV(TAG, "Touch Pad '%s' state: OFF (timeout)", child->get_name().c_str()); - pads_off++; - } - } else { - // Pad is already off - pads_off++; - } - } - - // Disable the loop to save CPU cycles when all pads are off and not in setup mode. - // The loop will be re-enabled by the ISR when any touch pad is touched. - // v1 hardware limitations require us to check all pads are off because: - // - v1 only generates interrupts on touch events (not releases) - // - We must poll for release timeouts in the main loop - // - We can only safely disable when no pads need timeout monitoring - this->check_and_disable_loop_if_all_released_(pads_off); -} - -void ESP32TouchComponent::on_shutdown() { - touch_pad_intr_disable(); - touch_pad_isr_deregister(touch_isr_handler, this); - this->cleanup_touch_queue_(); - - if (this->iir_filter_enabled_()) { - touch_pad_filter_stop(); - touch_pad_filter_delete(); - } - - // Configure wakeup pads if any are set - this->configure_wakeup_pads_(); -} - -void IRAM_ATTR ESP32TouchComponent::touch_isr_handler(void *arg) { - ESP32TouchComponent *component = static_cast(arg); - - uint32_t mask = 0; - touch_ll_read_trigger_status_mask(&mask); - touch_ll_clear_trigger_status_mask(); - touch_pad_clear_status(); - - // INTERRUPT BEHAVIOR: On ESP32 v1 hardware, the interrupt fires when ANY configured - // touch pad detects a touch (value goes below threshold). The hardware does NOT - // generate interrupts on release - only on touch events. - // The interrupt will continue to fire periodically (based on sleep_cycle) as long - // as any pad remains touched. This allows us to detect both new touches and - // continued touches, but releases must be detected by timeout in the main loop. - - // Process all configured pads to check their current state - // Note: ESP32 v1 doesn't tell us which specific pad triggered the interrupt, - // so we must scan all configured pads to find which ones were touched - for (auto *child : component->children_) { - touch_pad_t pad = child->get_touch_pad(); - - // Read current value using ISR-safe API - // IMPORTANT: ESP-IDF v5.4 regression - touch_pad_read_filtered() is no longer ISR-safe - // In ESP-IDF v5.3 and earlier it was ISR-safe, but ESP-IDF v5.4 added mutex protection that causes: - // "assert failed: xQueueSemaphoreTake queue.c:1718" - // We must use raw values even when filter is enabled as a workaround. - // Users should adjust thresholds to compensate for the lack of IIR filtering. - // See: https://github.com/espressif/esp-idf/issues/17045 - uint32_t value = touch_ll_read_raw_data(pad); - - // Skip pads that aren’t in the trigger mask - if (((mask >> pad) & 1) == 0) { - continue; - } - - // IMPORTANT: ESP32 v1 touch detection logic - INVERTED compared to v2! - // ESP32 v1: Touch is detected when capacitance INCREASES, causing the measured value to DECREASE - // Therefore: touched = (value < threshold) - // This is opposite to ESP32-S2/S3 v2 where touched = (value > threshold) - bool is_touched = value < child->get_threshold(); - - // Always send the current state - the main loop will filter for changes - // We send both touched and untouched states because the ISR doesn't - // track previous state (to keep ISR fast and simple) - TouchPadEventV1 event; - event.pad = pad; - event.value = value; - event.is_touched = is_touched; - - // Send to queue from ISR - non-blocking, drops if queue full - BaseType_t x_higher_priority_task_woken = pdFALSE; - xQueueSendFromISR(component->touch_queue_, &event, &x_higher_priority_task_woken); - component->enable_loop_soon_any_context(); - if (x_higher_priority_task_woken) { - portYIELD_FROM_ISR(); - } - } -} - -} // namespace esp32_touch -} // namespace esphome - -#endif // USE_ESP32_VARIANT_ESP32 diff --git a/esphome/components/esp32_touch/esp32_touch_v2.cpp b/esphome/components/esp32_touch/esp32_touch_v2.cpp deleted file mode 100644 index b34ca1abd3..0000000000 --- a/esphome/components/esp32_touch/esp32_touch_v2.cpp +++ /dev/null @@ -1,402 +0,0 @@ -#if defined(USE_ESP32_VARIANT_ESP32S2) || defined(USE_ESP32_VARIANT_ESP32S3) - -#include "esp32_touch.h" -#include "esphome/core/application.h" -#include "esphome/core/log.h" -#include "esphome/core/hal.h" - -namespace esphome { -namespace esp32_touch { - -static const char *const TAG = "esp32_touch"; - -// Helper to update touch state with a known state and value -void ESP32TouchComponent::update_touch_state_(ESP32TouchBinarySensor *child, bool is_touched, uint32_t value) { - // Store the value for get_value() access in lambdas - child->value_ = value; - - // Always update timer when touched - if (is_touched) { - child->last_touch_time_ = App.get_loop_component_start_time(); - } - - if (child->last_state_ != is_touched) { - child->last_state_ = is_touched; - child->publish_state(is_touched); - if (is_touched) { - ESP_LOGV(TAG, "Touch Pad '%s' state: ON (value: %" PRIu32 " > threshold: %" PRIu32 ")", child->get_name().c_str(), - value, child->threshold_ + child->benchmark_); - } else { - ESP_LOGV(TAG, "Touch Pad '%s' state: OFF", child->get_name().c_str()); - } - } -} - -// Helper to read touch value and update state for a given child (used for timeout events) -bool ESP32TouchComponent::check_and_update_touch_state_(ESP32TouchBinarySensor *child) { - // Read current touch value - uint32_t value = this->read_touch_value(child->touch_pad_); - - // ESP32-S2/S3 v2: Touch is detected when value > threshold + benchmark - ESP_LOGV(TAG, - "Checking touch state for '%s' (T%d): value = %" PRIu32 ", threshold = %" PRIu32 ", benchmark = %" PRIu32, - child->get_name().c_str(), child->touch_pad_, value, child->threshold_, child->benchmark_); - bool is_touched = value > child->benchmark_ + child->threshold_; - - this->update_touch_state_(child, is_touched, value); - return is_touched; -} - -void ESP32TouchComponent::setup() { - // Create queue for touch events first - if (!this->create_touch_queue_()) { - return; - } - - // Initialize touch pad peripheral - esp_err_t init_err = touch_pad_init(); - if (init_err != ESP_OK) { - ESP_LOGE(TAG, "Failed to initialize touch pad: %s", esp_err_to_name(init_err)); - this->mark_failed(); - return; - } - - // Configure each touch pad first - for (auto *child : this->children_) { - esp_err_t config_err = touch_pad_config(child->touch_pad_); - if (config_err != ESP_OK) { - ESP_LOGE(TAG, "Failed to configure touch pad %d: %s", child->touch_pad_, esp_err_to_name(config_err)); - } - } - - // Set up filtering if configured - if (this->filter_configured_()) { - touch_filter_config_t filter_info = { - .mode = this->filter_mode_, - .debounce_cnt = this->debounce_count_, - .noise_thr = this->noise_threshold_, - .jitter_step = this->jitter_step_, - .smh_lvl = this->smooth_level_, - }; - touch_pad_filter_set_config(&filter_info); - touch_pad_filter_enable(); - } - - if (this->denoise_configured_()) { - touch_pad_denoise_t denoise = { - .grade = this->grade_, - .cap_level = this->cap_level_, - }; - touch_pad_denoise_set_config(&denoise); - touch_pad_denoise_enable(); - } - - if (this->waterproof_configured_()) { - touch_pad_waterproof_t waterproof = { - .guard_ring_pad = this->waterproof_guard_ring_pad_, - .shield_driver = this->waterproof_shield_driver_, - }; - touch_pad_waterproof_set_config(&waterproof); - touch_pad_waterproof_enable(); - } - - // Configure measurement parameters - touch_pad_set_voltage(this->high_voltage_reference_, this->low_voltage_reference_, this->voltage_attenuation_); - touch_pad_set_charge_discharge_times(this->meas_cycle_); - touch_pad_set_measurement_interval(this->sleep_cycle_); - - // Disable hardware timeout - it causes continuous interrupts with high-capacitance - // setups (e.g., pressure sensors under cushions). The periodic release check in - // loop() handles state detection reliably without needing hardware timeout. - touch_pad_timeout_set(false, TOUCH_PAD_THRESHOLD_MAX); - - // Register ISR handler with interrupt mask - esp_err_t err = - touch_pad_isr_register(touch_isr_handler, this, static_cast(TOUCH_PAD_INTR_MASK_ALL)); - if (err != ESP_OK) { - ESP_LOGE(TAG, "Failed to register touch ISR: %s", esp_err_to_name(err)); - this->cleanup_touch_queue_(); - this->mark_failed(); - return; - } - - // Set thresholds for each pad BEFORE starting FSM - for (auto *child : this->children_) { - if (child->threshold_ != 0) { - touch_pad_set_thresh(child->touch_pad_, child->threshold_); - } - } - - // Enable interrupts - only ACTIVE and TIMEOUT - // NOTE: We intentionally don't enable INACTIVE interrupts because they are unreliable - // on ESP32-S2/S3 hardware and sometimes don't fire. Instead, we use timeout-based - // release detection with the ability to verify the actual state. - touch_pad_intr_enable(static_cast(TOUCH_PAD_INTR_MASK_ACTIVE | TOUCH_PAD_INTR_MASK_TIMEOUT)); - - // Set FSM mode before starting - touch_pad_set_fsm_mode(TOUCH_FSM_MODE_TIMER); - - // Start FSM - touch_pad_fsm_start(); - - // Calculate release timeout based on sleep cycle - this->calculate_release_timeout_(); -} - -void ESP32TouchComponent::dump_config() { - this->dump_config_base_(); - - if (this->filter_configured_()) { - const char *filter_mode_s; - switch (this->filter_mode_) { - case TOUCH_PAD_FILTER_IIR_4: - filter_mode_s = "IIR_4"; - break; - case TOUCH_PAD_FILTER_IIR_8: - filter_mode_s = "IIR_8"; - break; - case TOUCH_PAD_FILTER_IIR_16: - filter_mode_s = "IIR_16"; - break; - case TOUCH_PAD_FILTER_IIR_32: - filter_mode_s = "IIR_32"; - break; - case TOUCH_PAD_FILTER_IIR_64: - filter_mode_s = "IIR_64"; - break; - case TOUCH_PAD_FILTER_IIR_128: - filter_mode_s = "IIR_128"; - break; - case TOUCH_PAD_FILTER_IIR_256: - filter_mode_s = "IIR_256"; - break; - case TOUCH_PAD_FILTER_JITTER: - filter_mode_s = "JITTER"; - break; - default: - filter_mode_s = "UNKNOWN"; - break; - } - ESP_LOGCONFIG(TAG, - " Filter mode: %s\n" - " Debounce count: %" PRIu32 "\n" - " Noise threshold coefficient: %" PRIu32 "\n" - " Jitter filter step size: %" PRIu32, - filter_mode_s, this->debounce_count_, this->noise_threshold_, this->jitter_step_); - const char *smooth_level_s; - switch (this->smooth_level_) { - case TOUCH_PAD_SMOOTH_OFF: - smooth_level_s = "OFF"; - break; - case TOUCH_PAD_SMOOTH_IIR_2: - smooth_level_s = "IIR_2"; - break; - case TOUCH_PAD_SMOOTH_IIR_4: - smooth_level_s = "IIR_4"; - break; - case TOUCH_PAD_SMOOTH_IIR_8: - smooth_level_s = "IIR_8"; - break; - default: - smooth_level_s = "UNKNOWN"; - break; - } - ESP_LOGCONFIG(TAG, " Smooth level: %s", smooth_level_s); - } - - if (this->denoise_configured_()) { - const char *grade_s; - switch (this->grade_) { - case TOUCH_PAD_DENOISE_BIT12: - grade_s = "BIT12"; - break; - case TOUCH_PAD_DENOISE_BIT10: - grade_s = "BIT10"; - break; - case TOUCH_PAD_DENOISE_BIT8: - grade_s = "BIT8"; - break; - case TOUCH_PAD_DENOISE_BIT4: - grade_s = "BIT4"; - break; - default: - grade_s = "UNKNOWN"; - break; - } - ESP_LOGCONFIG(TAG, " Denoise grade: %s", grade_s); - - const char *cap_level_s; - switch (this->cap_level_) { - case TOUCH_PAD_DENOISE_CAP_L0: - cap_level_s = "L0"; - break; - case TOUCH_PAD_DENOISE_CAP_L1: - cap_level_s = "L1"; - break; - case TOUCH_PAD_DENOISE_CAP_L2: - cap_level_s = "L2"; - break; - case TOUCH_PAD_DENOISE_CAP_L3: - cap_level_s = "L3"; - break; - case TOUCH_PAD_DENOISE_CAP_L4: - cap_level_s = "L4"; - break; - case TOUCH_PAD_DENOISE_CAP_L5: - cap_level_s = "L5"; - break; - case TOUCH_PAD_DENOISE_CAP_L6: - cap_level_s = "L6"; - break; - case TOUCH_PAD_DENOISE_CAP_L7: - cap_level_s = "L7"; - break; - default: - cap_level_s = "UNKNOWN"; - break; - } - ESP_LOGCONFIG(TAG, " Denoise capacitance level: %s", cap_level_s); - } - - if (this->setup_mode_) { - ESP_LOGCONFIG(TAG, " Setup Mode ENABLED"); - } - - this->dump_config_sensors_(); -} - -void ESP32TouchComponent::loop() { - const uint32_t now = App.get_loop_component_start_time(); - - // V2 TOUCH HANDLING: - // Due to unreliable INACTIVE interrupts on ESP32-S2/S3, we use a hybrid approach: - // 1. Process ACTIVE interrupts when pads are touched - // 2. Use timeout-based release detection (like v1) - // 3. But smarter than v1: verify actual state before releasing on timeout - // This prevents false releases if we missed interrupts - - // In setup mode, periodically log all pad values - this->process_setup_mode_logging_(now); - - // Process any queued touch events from interrupts - TouchPadEventV2 event; - while (xQueueReceive(this->touch_queue_, &event, 0) == pdTRUE) { - ESP_LOGD(TAG, "Event received, mask = 0x%" PRIx32 ", pad = %d", event.intr_mask, event.pad); - // Handle timeout events - if (event.intr_mask & TOUCH_PAD_INTR_MASK_TIMEOUT) { - // Resume measurement after timeout - touch_pad_timeout_resume(); - // For timeout events, always check the current state - } else if (!(event.intr_mask & TOUCH_PAD_INTR_MASK_ACTIVE)) { - // Skip if not an active/timeout event - continue; - } - - // Find the child for the pad that triggered the interrupt - for (auto *child : this->children_) { - if (child->touch_pad_ == event.pad) { - if (event.intr_mask & TOUCH_PAD_INTR_MASK_TIMEOUT) { - // For timeout events, we need to read the value to determine state - this->check_and_update_touch_state_(child); - } else if (event.intr_mask & TOUCH_PAD_INTR_MASK_ACTIVE) { - // We only get ACTIVE interrupts now, releases are detected by timeout - // Read the current value - uint32_t value = this->read_touch_value(child->touch_pad_); - this->update_touch_state_(child, true, value); // Always touched for ACTIVE interrupts - } - break; - } - } - } - - // Check for released pads periodically (like v1) - if (!this->should_check_for_releases_(now)) { - return; - } - - size_t pads_off = 0; - for (auto *child : this->children_) { - child->ensure_benchmark_read(); - // Handle initial state publication after startup - this->publish_initial_state_if_needed_(child, now); - - if (child->last_state_) { - // Pad is currently in touched state - check for release timeout - // Using subtraction handles 32-bit rollover correctly - uint32_t time_diff = now - child->last_touch_time_; - - // Check if we haven't seen this pad recently - if (time_diff > this->release_timeout_ms_) { - // Haven't seen this pad recently - verify actual state - // Unlike v1, v2 hardware allows us to read the current state anytime - // This makes v2 smarter: we can verify if it's actually released before - // declaring a timeout, preventing false releases if interrupts were missed - bool still_touched = this->check_and_update_touch_state_(child); - - if (still_touched) { - // Still touched! Timer was reset in update_touch_state_ - ESP_LOGVV(TAG, "Touch Pad '%s' still touched after %" PRIu32 "ms timeout, resetting timer", - child->get_name().c_str(), this->release_timeout_ms_); - } else { - // Actually released - already handled by check_and_update_touch_state_ - pads_off++; - } - } - } else { - // Pad is already off - pads_off++; - } - } - - // Disable the loop when all pads are off and not in setup mode (like v1) - // We need to keep checking for timeouts, so only disable when all pads are confirmed off - this->check_and_disable_loop_if_all_released_(pads_off); -} - -void ESP32TouchComponent::on_shutdown() { - // Disable interrupts - touch_pad_intr_disable(TOUCH_PAD_INTR_MASK_ACTIVE); - touch_pad_isr_deregister(touch_isr_handler, this); - this->cleanup_touch_queue_(); - - // Configure wakeup pads if any are set - this->configure_wakeup_pads_(); -} - -void IRAM_ATTR ESP32TouchComponent::touch_isr_handler(void *arg) { - ESP32TouchComponent *component = static_cast(arg); - BaseType_t x_higher_priority_task_woken = pdFALSE; - - // Read interrupt status - TouchPadEventV2 event; - event.intr_mask = touch_pad_read_intr_status_mask(); - event.pad = touch_pad_get_current_meas_channel(); - - // Send event to queue for processing in main loop - xQueueSendFromISR(component->touch_queue_, &event, &x_higher_priority_task_woken); - component->enable_loop_soon_any_context(); - - if (x_higher_priority_task_woken) { - portYIELD_FROM_ISR(); - } -} - -uint32_t ESP32TouchComponent::read_touch_value(touch_pad_t pad) const { - // Unlike ESP32 v1, touch reads on ESP32-S2/S3 v2 are non-blocking operations. - // The hardware continuously samples in the background and we can read the - // latest value at any time without waiting. - uint32_t value = 0; - if (this->filter_configured_()) { - // Read filtered/smoothed value when filter is enabled - touch_pad_filter_read_smooth(pad, &value); - } else { - // Read raw value when filter is not configured - touch_pad_read_raw_data(pad, &value); - } - return value; -} - -} // namespace esp32_touch -} // namespace esphome - -#endif // USE_ESP32_VARIANT_ESP32S2 || USE_ESP32_VARIANT_ESP32S3 diff --git a/esphome/components/esp8266/__init__.py b/esphome/components/esp8266/__init__.py index c7b5d5c130..2081145096 100644 --- a/esphome/components/esp8266/__init__.py +++ b/esphome/components/esp8266/__init__.py @@ -1,11 +1,13 @@ import logging from pathlib import Path +import re import esphome.codegen as cg import esphome.config_validation as cv from esphome.const import ( CONF_BOARD, CONF_BOARD_FLASH_MODE, + CONF_ENABLE_FULL_PRINTF, CONF_FRAMEWORK, CONF_PLATFORM_VERSION, CONF_SOURCE, @@ -17,8 +19,9 @@ from esphome.const import ( PLATFORM_ESP8266, ThreadModel, ) -from esphome.core import CORE, CoroPriority, coroutine_with_priority +from esphome.core import CORE, CoroPriority, Lambda, coroutine_with_priority from esphome.helpers import copy_file_if_changed +from esphome.types import ConfigType from .boards import BOARDS, ESP8266_LD_SCRIPTS from .const import ( @@ -39,12 +42,42 @@ from .const import ( ) from .gpio import PinInitialState, add_pin_initial_states_array +CONF_ENABLE_SCANF_FLOAT = "enable_scanf_float" +# Heuristically matches scanf/sscanf calls with float format specifiers. +# Standard scanf float conversions: %f %F %e %E %g %G %a %A +# With optional modifiers: %*f (suppression), %8f (width), %lf %Lf (length) +# Also matches non-standard patterns like %.2f as a heuristic — these are +# invalid in scanf but users may write them by analogy with printf. +# Uses [^;]*? to stay within a single statement, preventing false positives +# from e.g. sscanf(buf, "%d", &x); printf("%f", val); +_SCANF_FLOAT_RE = re.compile(r"scanf\s*\([^;]*?%[*\d.]*[hlL]*[feEgGaAF]") + CODEOWNERS = ["@esphome/core"] _LOGGER = logging.getLogger(__name__) AUTO_LOAD = ["preferences"] IS_TARGET_PLATFORM = True +def lambdas_use_scanf_float(config: ConfigType) -> bool: + """Check if any lambda in the config uses scanf with a float format specifier. + + Comments are stripped before matching to avoid false positives from + commented-out code. The cost of a false positive is only ~8KB flash. + """ + stack: list = [config] + while stack: + obj = stack.pop() + if isinstance(obj, Lambda): + src = obj.comment_remover(obj.value) + if _SCANF_FLOAT_RE.search(src): + return True + elif isinstance(obj, dict): + stack.extend(obj.values()) + elif isinstance(obj, list): + stack.extend(obj) + return False + + def set_core_data(config): CORE.data[KEY_ESP8266] = {} CORE.data[KEY_CORE][KEY_TARGET_PLATFORM] = PLATFORM_ESP8266 @@ -179,6 +212,8 @@ CONFIG_SCHEMA = cv.All( ), cv.Optional(CONF_ENABLE_SERIAL): cv.boolean, cv.Optional(CONF_ENABLE_SERIAL1): cv.boolean, + cv.Optional(CONF_ENABLE_FULL_PRINTF, default=False): cv.boolean, + cv.Optional(CONF_ENABLE_SCANF_FLOAT): cv.boolean, } ), set_core_data, @@ -199,15 +234,23 @@ async def to_code(config): cg.add_define("ESPHOME_VARIANT", "ESP8266") cg.add_define(ThreadModel.SINGLE) - cg.add_platformio_option( - "extra_scripts", - [ - "pre:testing_mode.py", - "pre:exclude_updater.py", - "pre:exclude_waveform.py", - "post:post_build.py", - ], - ) + enable_scanf_float = config.get(CONF_ENABLE_SCANF_FLOAT) + if enable_scanf_float is None and lambdas_use_scanf_float(CORE.config): + enable_scanf_float = True + _LOGGER.warning( + "Lambda uses scanf with a float format specifier; " + "enabling scanf float support (~8KB flash)" + ) + + extra_scripts = [ + "pre:testing_mode.py", + "pre:exclude_updater.py", + "pre:exclude_waveform.py", + ] + if not enable_scanf_float: + extra_scripts.append("pre:remove_float_scanf.py") + extra_scripts.append("post:post_build.py") + cg.add_platformio_option("extra_scripts", extra_scripts) conf = config[CONF_FRAMEWORK] cg.add_platformio_option("framework", "arduino") @@ -259,6 +302,14 @@ async def to_code(config): if CORE.testing_mode: cg.add_build_flag("-DESPHOME_TESTING_MODE") + # Wrap FILE*-based printf functions to eliminate newlib's _vfiprintf_r + # (~1.6 KB). See printf_stubs.cpp for implementation. + if config.get(CONF_ENABLE_FULL_PRINTF): + cg.add_define("USE_FULL_PRINTF") + else: + for symbol in ("vprintf", "printf", "fprintf"): + cg.add_build_flag(f"-Wl,--wrap={symbol}") + cg.add_platformio_option("board_build.flash_mode", config[CONF_BOARD_FLASH_MODE]) ver: cv.Version = CORE.data[KEY_CORE][KEY_FRAMEWORK_VERSION] @@ -342,3 +393,8 @@ def copy_files() -> None: exclude_waveform_file, CORE.relative_build_path("exclude_waveform.py"), ) + remove_float_scanf_file = dir / "remove_float_scanf.py.script" + copy_file_if_changed( + remove_float_scanf_file, + CORE.relative_build_path("remove_float_scanf.py"), + ) diff --git a/esphome/components/esp8266/core.cpp b/esphome/components/esp8266/core.cpp index 784b87916b..159ec20e77 100644 --- a/esphome/components/esp8266/core.cpp +++ b/esphome/components/esp8266/core.cpp @@ -3,6 +3,7 @@ #include "core.h" #include "esphome/core/defines.h" #include "esphome/core/hal.h" +#include "esphome/core/time_64.h" #include "esphome/core/helpers.h" #include "preferences.h" #include @@ -14,9 +15,10 @@ extern "C" { namespace esphome { -void IRAM_ATTR HOT yield() { ::yield(); } +void HOT yield() { ::yield(); } uint32_t IRAM_ATTR HOT millis() { return ::millis(); } -void IRAM_ATTR HOT delay(uint32_t ms) { ::delay(ms); } +uint64_t millis_64() { return Millis64Impl::compute(::millis()); } +void HOT delay(uint32_t ms) { ::delay(ms); } uint32_t IRAM_ATTR HOT micros() { return ::micros(); } void IRAM_ATTR HOT delayMicroseconds(uint32_t us) { delay_microseconds_safe(us); } void arch_restart() { @@ -27,11 +29,17 @@ void arch_restart() { } } void arch_init() {} -void IRAM_ATTR HOT arch_feed_wdt() { system_soft_wdt_feed(); } +void HOT arch_feed_wdt() { system_soft_wdt_feed(); } uint8_t progmem_read_byte(const uint8_t *addr) { return pgm_read_byte(addr); // NOLINT } +const char *progmem_read_ptr(const char *const *addr) { + return reinterpret_cast(pgm_read_ptr(addr)); // NOLINT +} +uint16_t progmem_read_uint16(const uint16_t *addr) { + return pgm_read_word(addr); // NOLINT +} uint32_t IRAM_ATTR HOT arch_get_cpu_cycle_count() { return esp_get_cycle_count(); } uint32_t arch_get_cpu_freq_hz() { return F_CPU; } diff --git a/esphome/components/esp8266/gpio.py b/esphome/components/esp8266/gpio.py index 2e8d6496bc..43508afaf9 100644 --- a/esphome/components/esp8266/gpio.py +++ b/esphome/components/esp8266/gpio.py @@ -202,11 +202,11 @@ async def add_pin_initial_states_array(): cg.add_global( cg.RawExpression( - f"const uint8_t ESPHOME_ESP8266_GPIO_INITIAL_MODE[16] PROGMEM = {{{initial_modes_s}}}" + f"constexpr uint8_t ESPHOME_ESP8266_GPIO_INITIAL_MODE[16] PROGMEM = {{{initial_modes_s}}}" ) ) cg.add_global( cg.RawExpression( - f"const uint8_t ESPHOME_ESP8266_GPIO_INITIAL_LEVEL[16] PROGMEM = {{{initial_levels_s}}}" + f"constexpr uint8_t ESPHOME_ESP8266_GPIO_INITIAL_LEVEL[16] PROGMEM = {{{initial_levels_s}}}" ) ) diff --git a/esphome/components/esp8266/helpers.cpp b/esphome/components/esp8266/helpers.cpp index 036594fa17..aadfc31197 100644 --- a/esphome/components/esp8266/helpers.cpp +++ b/esphome/components/esp8266/helpers.cpp @@ -12,19 +12,13 @@ namespace esphome { uint32_t random_uint32() { return os_random(); } bool random_bytes(uint8_t *data, size_t len) { return os_get_random(data, len) == 0; } -// ESP8266 doesn't have mutexes, but that shouldn't be an issue as it's single-core and non-preemptive OS. -Mutex::Mutex() {} -Mutex::~Mutex() {} -void Mutex::lock() {} -bool Mutex::try_lock() { return true; } -void Mutex::unlock() {} +// ESP8266 Mutex is defined inline as a no-op in helpers.h when USE_ESP8266 (or USE_RP2040) is set, +// independent of the ESPHOME_THREAD_SINGLE thread model define. IRAM_ATTR InterruptLock::InterruptLock() { state_ = xt_rsil(15); } IRAM_ATTR InterruptLock::~InterruptLock() { xt_wsr_ps(state_); } -// ESP8266 doesn't support lwIP core locking, so this is a no-op -LwIPLock::LwIPLock() {} -LwIPLock::~LwIPLock() {} +// ESP8266 LwIPLock is defined inline as a no-op in helpers.h void get_mac_address_raw(uint8_t *mac) { // NOLINT(readability-non-const-parameter) wifi_get_macaddr(STATION_IF, mac); diff --git a/esphome/components/esp8266/preference_backend.h b/esphome/components/esp8266/preference_backend.h new file mode 100644 index 0000000000..f9da8ff165 --- /dev/null +++ b/esphome/components/esp8266/preference_backend.h @@ -0,0 +1,29 @@ +#pragma once +#ifdef USE_ESP8266 + +#include +#include + +namespace esphome::esp8266 { + +class ESP8266PreferenceBackend final { + public: + bool save(const uint8_t *data, size_t len); + bool load(uint8_t *data, size_t len); + + uint32_t type = 0; + uint16_t offset = 0; + uint8_t length_words = 0; // Max 255 words (1020 bytes of data) + bool in_flash = false; +}; + +class ESP8266Preferences; +ESP8266Preferences *get_preferences(); + +} // namespace esphome::esp8266 + +namespace esphome { +using PreferenceBackend = esp8266::ESP8266PreferenceBackend; +} // namespace esphome + +#endif // USE_ESP8266 diff --git a/esphome/components/esp8266/preferences.cpp b/esphome/components/esp8266/preferences.cpp index 35d1cd07f7..906fed2b29 100644 --- a/esphome/components/esp8266/preferences.cpp +++ b/esphome/components/esp8266/preferences.cpp @@ -5,21 +5,15 @@ extern "C" { #include "spi_flash.h" } -#include "esphome/core/defines.h" +#include "preferences.h" #include "esphome/core/helpers.h" #include "esphome/core/log.h" -#include "esphome/core/preferences.h" -#include "preferences.h" #include namespace esphome::esp8266 { -static const char *const TAG = "esp8266.preferences"; - -static uint32_t *s_flash_storage = nullptr; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) -static bool s_prevent_write = false; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) -static bool s_flash_dirty = false; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) +static const char *const TAG = "preferences"; static constexpr uint32_t ESP_RTC_USER_MEM_START = 0x60001200; static constexpr uint32_t ESP_RTC_USER_MEM_SIZE_WORDS = 128; @@ -37,12 +31,21 @@ static constexpr uint32_t MAX_PREFERENCE_WORDS = 255; #define ESP_RTC_USER_MEM ((uint32_t *) ESP_RTC_USER_MEM_START) +// Flash storage size depends on esp8266 -> restore_from_flash YAML option (default: false). +// When enabled (USE_ESP8266_PREFERENCES_FLASH), all preferences default to flash and need +// 128 words (512 bytes). When disabled, only explicit flash prefs use this storage so +// 64 words (256 bytes) suffices since most preferences go to RTC memory instead. #ifdef USE_ESP8266_PREFERENCES_FLASH static constexpr uint32_t ESP8266_FLASH_STORAGE_SIZE = 128; #else static constexpr uint32_t ESP8266_FLASH_STORAGE_SIZE = 64; #endif +static uint32_t + s_flash_storage[ESP8266_FLASH_STORAGE_SIZE]; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) +static bool s_prevent_write = false; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) +static bool s_flash_dirty = false; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) + static inline bool esp_rtc_user_mem_read(uint32_t index, uint32_t *dest) { if (index >= ESP_RTC_USER_MEM_SIZE_WORDS) { return false; @@ -126,167 +129,144 @@ static bool load_from_rtc(size_t offset, uint32_t *data, size_t len) { return true; } -// Stack buffer size - 16 words total: up to 15 words of preference data + 1 word CRC (60 bytes of preference data) -// This handles virtually all real-world preferences without heap allocation -static constexpr size_t PREF_BUFFER_WORDS = 16; +// Maximum buffer for any single preference - bounded by storage sizes. +// Flash prefs: bounded by ESP8266_FLASH_STORAGE_SIZE (128 or 64 words). +// RTC prefs: bounded by RTC_NORMAL_REGION_WORDS (96) - a single pref can't span both RTC regions. +static constexpr size_t PREF_MAX_BUFFER_WORDS = + ESP8266_FLASH_STORAGE_SIZE > RTC_NORMAL_REGION_WORDS ? ESP8266_FLASH_STORAGE_SIZE : RTC_NORMAL_REGION_WORDS; -class ESP8266PreferenceBackend : public ESPPreferenceBackend { - public: - uint32_t type = 0; - uint16_t offset = 0; - uint8_t length_words = 0; // Max 255 words (1020 bytes of data) - bool in_flash = false; +bool ESP8266PreferenceBackend::save(const uint8_t *data, size_t len) { + if (bytes_to_words(len) != this->length_words) + return false; + const size_t buffer_size = static_cast(this->length_words) + 1; + if (buffer_size > PREF_MAX_BUFFER_WORDS) + return false; + uint32_t buffer[PREF_MAX_BUFFER_WORDS]; + memset(buffer, 0, buffer_size * sizeof(uint32_t)); + memcpy(buffer, data, len); + buffer[this->length_words] = calculate_crc(buffer, buffer + this->length_words, this->type); + return this->in_flash ? save_to_flash(this->offset, buffer, buffer_size) + : save_to_rtc(this->offset, buffer, buffer_size); +} - bool save(const uint8_t *data, size_t len) override { - if (bytes_to_words(len) != this->length_words) - return false; +bool ESP8266PreferenceBackend::load(uint8_t *data, size_t len) { + if (bytes_to_words(len) != this->length_words) + return false; + const size_t buffer_size = static_cast(this->length_words) + 1; + if (buffer_size > PREF_MAX_BUFFER_WORDS) + return false; + uint32_t buffer[PREF_MAX_BUFFER_WORDS]; + bool ret = this->in_flash ? load_from_flash(this->offset, buffer, buffer_size) + : load_from_rtc(this->offset, buffer, buffer_size); + if (!ret) + return false; + if (buffer[this->length_words] != calculate_crc(buffer, buffer + this->length_words, this->type)) + return false; + memcpy(data, buffer, len); + return true; +} - const size_t buffer_size = static_cast(this->length_words) + 1; - SmallBufferWithHeapFallback buffer_alloc(buffer_size); - uint32_t *buffer = buffer_alloc.get(); - memset(buffer, 0, buffer_size * sizeof(uint32_t)); +void ESP8266Preferences::setup() { + ESP_LOGVV(TAG, "Loading preferences from flash"); - memcpy(buffer, data, len); - buffer[this->length_words] = calculate_crc(buffer, buffer + this->length_words, this->type); + { + InterruptLock lock; + spi_flash_read(get_esp8266_flash_address(), s_flash_storage, ESP8266_FLASH_STORAGE_SIZE * 4); + } +} - return this->in_flash ? save_to_flash(this->offset, buffer, buffer_size) - : save_to_rtc(this->offset, buffer, buffer_size); +ESPPreferenceObject ESP8266Preferences::make_preference(size_t length, uint32_t type, bool in_flash) { + const uint32_t length_words = bytes_to_words(length); + if (length_words > MAX_PREFERENCE_WORDS) { + ESP_LOGE(TAG, "Preference too large: %u words", static_cast(length_words)); + return {}; } - bool load(uint8_t *data, size_t len) override { - if (bytes_to_words(len) != this->length_words) - return false; + const uint32_t total_words = length_words + 1; // +1 for CRC + uint16_t offset; - const size_t buffer_size = static_cast(this->length_words) + 1; - SmallBufferWithHeapFallback buffer_alloc(buffer_size); - uint32_t *buffer = buffer_alloc.get(); - - bool ret = this->in_flash ? load_from_flash(this->offset, buffer, buffer_size) - : load_from_rtc(this->offset, buffer, buffer_size); - if (!ret) - return false; - - if (buffer[this->length_words] != calculate_crc(buffer, buffer + this->length_words, this->type)) - return false; - - memcpy(data, buffer, len); - return true; - } -}; - -class ESP8266Preferences : public ESPPreferences { - public: - uint32_t current_offset = 0; - uint32_t current_flash_offset = 0; // in words - - void setup() { - s_flash_storage = new uint32_t[ESP8266_FLASH_STORAGE_SIZE]; // NOLINT - ESP_LOGVV(TAG, "Loading preferences from flash"); - - { - InterruptLock lock; - spi_flash_read(get_esp8266_flash_address(), s_flash_storage, ESP8266_FLASH_STORAGE_SIZE * 4); - } - } - - ESPPreferenceObject make_preference(size_t length, uint32_t type, bool in_flash) override { - const uint32_t length_words = bytes_to_words(length); - if (length_words > MAX_PREFERENCE_WORDS) { - ESP_LOGE(TAG, "Preference too large: %u words", static_cast(length_words)); + if (in_flash) { + if (this->current_flash_offset + total_words > ESP8266_FLASH_STORAGE_SIZE) return {}; + offset = static_cast(this->current_flash_offset); + this->current_flash_offset += total_words; + } else { + uint32_t start = this->current_offset; + bool in_normal = start < RTC_NORMAL_REGION_WORDS; + // Normal: offset 0-95 maps to RTC offset 32-127 + // Eboot: offset 96-127 maps to RTC offset 0-31 + if (in_normal && start + total_words > RTC_NORMAL_REGION_WORDS) { + // start is in normal but end is not -> switch to Eboot + this->current_offset = start = RTC_NORMAL_REGION_WORDS; + in_normal = false; } - - const uint32_t total_words = length_words + 1; // +1 for CRC - uint16_t offset; - - if (in_flash) { - if (this->current_flash_offset + total_words > ESP8266_FLASH_STORAGE_SIZE) - return {}; - offset = static_cast(this->current_flash_offset); - this->current_flash_offset += total_words; - } else { - uint32_t start = this->current_offset; - bool in_normal = start < RTC_NORMAL_REGION_WORDS; - // Normal: offset 0-95 maps to RTC offset 32-127 - // Eboot: offset 96-127 maps to RTC offset 0-31 - if (in_normal && start + total_words > RTC_NORMAL_REGION_WORDS) { - // start is in normal but end is not -> switch to Eboot - this->current_offset = start = RTC_NORMAL_REGION_WORDS; - in_normal = false; - } - if (start + total_words > PREF_TOTAL_WORDS) - return {}; // Doesn't fit in RTC memory - // Convert preference offset to RTC memory offset - offset = static_cast(in_normal ? start + RTC_EBOOT_REGION_WORDS : start - RTC_NORMAL_REGION_WORDS); - this->current_offset = start + total_words; - } - - auto *pref = new ESP8266PreferenceBackend(); // NOLINT(cppcoreguidelines-owning-memory) - pref->offset = offset; - pref->type = type; - pref->length_words = static_cast(length_words); - pref->in_flash = in_flash; - return pref; + if (start + total_words > PREF_TOTAL_WORDS) + return {}; // Doesn't fit in RTC memory + // Convert preference offset to RTC memory offset + offset = static_cast(in_normal ? start + RTC_EBOOT_REGION_WORDS : start - RTC_NORMAL_REGION_WORDS); + this->current_offset = start + total_words; } - ESPPreferenceObject make_preference(size_t length, uint32_t type) override { -#ifdef USE_ESP8266_PREFERENCES_FLASH - return make_preference(length, type, true); -#else - return make_preference(length, type, false); -#endif - } + auto *pref = new ESP8266PreferenceBackend(); // NOLINT(cppcoreguidelines-owning-memory) + pref->offset = offset; + pref->type = type; + pref->length_words = static_cast(length_words); + pref->in_flash = in_flash; + return ESPPreferenceObject(pref); +} - bool sync() override { - if (!s_flash_dirty) - return true; - if (s_prevent_write) - return false; - - ESP_LOGD(TAG, "Saving"); - SpiFlashOpResult erase_res, write_res = SPI_FLASH_RESULT_OK; - { - InterruptLock lock; - erase_res = spi_flash_erase_sector(get_esp8266_flash_sector()); - if (erase_res == SPI_FLASH_RESULT_OK) { - write_res = spi_flash_write(get_esp8266_flash_address(), s_flash_storage, ESP8266_FLASH_STORAGE_SIZE * 4); - } - } - if (erase_res != SPI_FLASH_RESULT_OK) { - ESP_LOGE(TAG, "Erasing failed"); - return false; - } - if (write_res != SPI_FLASH_RESULT_OK) { - ESP_LOGE(TAG, "Writing failed"); - return false; - } - - s_flash_dirty = false; +bool ESP8266Preferences::sync() { + if (!s_flash_dirty) return true; + if (s_prevent_write) + return false; + + ESP_LOGD(TAG, "Saving"); + SpiFlashOpResult erase_res, write_res = SPI_FLASH_RESULT_OK; + { + InterruptLock lock; + erase_res = spi_flash_erase_sector(get_esp8266_flash_sector()); + if (erase_res == SPI_FLASH_RESULT_OK) { + write_res = spi_flash_write(get_esp8266_flash_address(), s_flash_storage, ESP8266_FLASH_STORAGE_SIZE * 4); + } + } + if (erase_res != SPI_FLASH_RESULT_OK) { + ESP_LOGE(TAG, "Erasing failed"); + return false; + } + if (write_res != SPI_FLASH_RESULT_OK) { + ESP_LOGE(TAG, "Writing failed"); + return false; } - bool reset() override { - ESP_LOGD(TAG, "Erasing storage"); - SpiFlashOpResult erase_res; - { - InterruptLock lock; - erase_res = spi_flash_erase_sector(get_esp8266_flash_sector()); - } - if (erase_res != SPI_FLASH_RESULT_OK) { - ESP_LOGE(TAG, "Erasing failed"); - return false; - } + s_flash_dirty = false; + return true; +} - // Protect flash from writing till restart - s_prevent_write = true; - return true; +bool ESP8266Preferences::reset() { + ESP_LOGD(TAG, "Erasing storage"); + SpiFlashOpResult erase_res; + { + InterruptLock lock; + erase_res = spi_flash_erase_sector(get_esp8266_flash_sector()); } -}; + if (erase_res != SPI_FLASH_RESULT_OK) { + ESP_LOGE(TAG, "Erasing failed"); + return false; + } + + // Protect flash from writing till restart + s_prevent_write = true; + return true; +} + +static ESP8266Preferences s_preferences; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) + +ESP8266Preferences *get_preferences() { return &s_preferences; } void setup_preferences() { - auto *pref = new ESP8266Preferences(); // NOLINT(cppcoreguidelines-owning-memory) - pref->setup(); - global_preferences = pref; + s_preferences.setup(); + global_preferences = &s_preferences; } void preferences_prevent_write(bool prevent) { s_prevent_write = prevent; } diff --git a/esphome/components/esp8266/preferences.h b/esphome/components/esp8266/preferences.h index 16cf80a129..43557d5ec5 100644 --- a/esphome/components/esp8266/preferences.h +++ b/esphome/components/esp8266/preferences.h @@ -1,12 +1,34 @@ #pragma once - #ifdef USE_ESP8266 +#include "esphome/core/preference_backend.h" + namespace esphome::esp8266 { +class ESP8266Preferences final : public PreferencesMixin { + public: + using PreferencesMixin::make_preference; + void setup(); + ESPPreferenceObject make_preference(size_t length, uint32_t type, bool in_flash); + ESPPreferenceObject make_preference(size_t length, uint32_t type) { +#ifdef USE_ESP8266_PREFERENCES_FLASH + return this->make_preference(length, type, true); +#else + return this->make_preference(length, type, false); +#endif + } + bool sync(); + bool reset(); + + uint32_t current_offset = 0; + uint32_t current_flash_offset = 0; // in words +}; + void setup_preferences(); void preferences_prevent_write(bool prevent); } // namespace esphome::esp8266 +DECLARE_PREFERENCE_ALIASES(esphome::esp8266::ESP8266Preferences) + #endif // USE_ESP8266 diff --git a/esphome/components/esp8266/printf_stubs.cpp b/esphome/components/esp8266/printf_stubs.cpp new file mode 100644 index 0000000000..e6d4a74866 --- /dev/null +++ b/esphome/components/esp8266/printf_stubs.cpp @@ -0,0 +1,71 @@ +/* + * Linker wrap stubs for FILE*-based printf functions. + * + * The ESP8266 Arduino framework and libraries may reference printf(), + * vprintf(), and fprintf() which pull in newlib's _vfprintf_r (~900 bytes). + * ESPHome never uses these — all logging writes directly to the UART via + * Arduino's Serial, so the libc FILE*-based printf path is dead code. + * + * These stubs redirect through vsnprintf() (which is already in the binary + * for ESPHome's logging) and fwrite(), allowing the linker to dead-code + * eliminate _vfprintf_r. + * + * Saves ~1.6 KB of flash. + */ + +#if defined(USE_ESP8266) && !defined(USE_FULL_PRINTF) +#include +#include +#include + +namespace esphome::esp8266 {} + +static constexpr size_t PRINTF_BUFFER_SIZE = 512; + +// These stubs are essentially dead code at runtime — ESPHome writes directly +// to the UART via Arduino's Serial, and Serial.printf() has its own implementation. +// The buffer overflow check is purely defensive and should never trigger. +static int write_printf_buffer(FILE *stream, char *buf, int len) { + if (len < 0) { + return len; + } + size_t write_len = len; + if (write_len >= PRINTF_BUFFER_SIZE) { + fwrite(buf, 1, PRINTF_BUFFER_SIZE - 1, stream); + abort(); + } + if (fwrite(buf, 1, write_len, stream) < write_len || ferror(stream)) { + return -1; + } + return len; +} + +// NOLINTBEGIN(bugprone-reserved-identifier,cert-dcl37-c,cert-dcl51-cpp,readability-identifier-naming) +extern "C" { + +int __wrap_vprintf(const char *fmt, va_list ap) { + char buf[PRINTF_BUFFER_SIZE]; + return write_printf_buffer(stdout, buf, vsnprintf(buf, sizeof(buf), fmt, ap)); +} + +int __wrap_printf(const char *fmt, ...) { + va_list ap; + va_start(ap, fmt); + int len = __wrap_vprintf(fmt, ap); + va_end(ap); + return len; +} + +int __wrap_fprintf(FILE *stream, const char *fmt, ...) { + va_list ap; + va_start(ap, fmt); + char buf[PRINTF_BUFFER_SIZE]; + int len = write_printf_buffer(stream, buf, vsnprintf(buf, sizeof(buf), fmt, ap)); + va_end(ap); + return len; +} + +} // extern "C" +// NOLINTEND(bugprone-reserved-identifier,cert-dcl37-c,cert-dcl51-cpp,readability-identifier-naming) + +#endif // USE_ESP8266 && !USE_FULL_PRINTF diff --git a/esphome/components/esp8266/remove_float_scanf.py.script b/esphome/components/esp8266/remove_float_scanf.py.script new file mode 100644 index 0000000000..b1d03a4d46 --- /dev/null +++ b/esphome/components/esp8266/remove_float_scanf.py.script @@ -0,0 +1,46 @@ +# pylint: disable=E0602 +Import("env") # noqa + +# Remove forced scanf linkage to allow garbage collection of unused code +# +# The ESP8266 Arduino framework unconditionally adds: +# -u _printf_float -u _scanf_float +# +# The -u flag forces symbols to be linked even if unreferenced, which pulls +# in the entire scanf family (~7-8KB). ESPHome doesn't use scanf at all +# (verified by CI check in PR #13657), so this is pure dead weight. +# +# By removing -u _scanf_float, --gc-sections can eliminate: +# - scanf family functions (~7KB) +# - _strtod_l (~3.7KB) +# - Related parsing infrastructure +# +# We keep -u _printf_float because components still use %f in logging. + + +def remove_scanf_float_flag(source, target, env): + """Remove -u _scanf_float from linker flags. + + This is called as a pre-action before the link step, after the + Arduino framework has added its default flags. + """ + linkflags = env.get("LINKFLAGS", []) + new_linkflags = [] + i = 0 + + while i < len(linkflags): + flag = linkflags[i] + if flag == "-u" and i + 1 < len(linkflags): + next_flag = linkflags[i + 1] + if next_flag == "_scanf_float": + print("ESPHome: Removing _scanf_float (saves ~8KB flash)") + i += 2 # Skip both -u and the symbol + continue + new_linkflags.append(flag) + i += 1 + + env.Replace(LINKFLAGS=new_linkflags) + + +# Register the callback to run before the link step +env.AddPreAction("$BUILD_DIR/${PROGNAME}.elf", remove_scanf_float_flag) diff --git a/esphome/components/esp8266_pwm/output.py b/esphome/components/esp8266_pwm/output.py index a78831c516..b9b6dcc95a 100644 --- a/esphome/components/esp8266_pwm/output.py +++ b/esphome/components/esp8266_pwm/output.py @@ -57,6 +57,7 @@ async def to_code(config) -> None: cv.Required(CONF_FREQUENCY): cv.templatable(validate_frequency), } ), + synchronous=True, ) async def esp8266_set_frequency_to_code(config, action_id, template_arg, args): paren = await cg.get_variable(config[CONF_ID]) diff --git a/esphome/components/esp_ldo/__init__.py b/esphome/components/esp_ldo/__init__.py index f136dd149b..a489651b59 100644 --- a/esphome/components/esp_ldo/__init__.py +++ b/esphome/components/esp_ldo/__init__.py @@ -13,22 +13,63 @@ esp_ldo_ns = cg.esphome_ns.namespace("esp_ldo") EspLdo = esp_ldo_ns.class_("EspLdo", cg.Component) AdjustAction = esp_ldo_ns.class_("AdjustAction", Action) -CHANNELS = (3, 4) +CHANNELS = (1, 2, 3, 4) +CHANNELS_INTERNAL = (1, 2) CONF_ADJUSTABLE = "adjustable" +CONF_ALLOW_INTERNAL_CHANNEL = "allow_internal_channel" +CONF_PASSTHROUGH = "passthrough" adjusted_ids = set() + +def validate_ldo_voltage(value): + if isinstance(value, str) and value.lower() == CONF_PASSTHROUGH: + return CONF_PASSTHROUGH + value = cv.voltage(value) + if 0.5 <= value <= 2.7: + return value + raise cv.Invalid( + f"LDO voltage must be in range 0.5V-2.7V or 'passthrough' (bypass mode), got {value}V" + ) + + +def validate_ldo_config(config): + channel = config[CONF_CHANNEL] + allow_internal = config[CONF_ALLOW_INTERNAL_CHANNEL] + if allow_internal and channel not in CHANNELS_INTERNAL: + raise cv.Invalid( + f"'{CONF_ALLOW_INTERNAL_CHANNEL}' is only valid for internal channels (1, 2). " + f"Channel {channel} is a user-configurable channel — its usage depends on your board schematic.", + path=[CONF_ALLOW_INTERNAL_CHANNEL], + ) + if channel in CHANNELS_INTERNAL and not allow_internal: + raise cv.Invalid( + f"LDO channel {channel} is normally used internally by the chip (flash/PSRAM). " + f"Set '{CONF_ALLOW_INTERNAL_CHANNEL}: true' to confirm you know what you are doing.", + path=[CONF_CHANNEL], + ) + if config[CONF_VOLTAGE] == CONF_PASSTHROUGH and config[CONF_ADJUSTABLE]: + raise cv.Invalid( + "Passthrough mode passes the supply voltage directly to the output and does not support " + "runtime voltage adjustment.", + path=[CONF_ADJUSTABLE], + ) + return config + + CONFIG_SCHEMA = cv.All( cv.ensure_list( - cv.COMPONENT_SCHEMA.extend( - { - cv.GenerateID(): cv.declare_id(EspLdo), - cv.Required(CONF_VOLTAGE): cv.All( - cv.voltage, cv.float_range(min=0.5, max=2.7) - ), - cv.Required(CONF_CHANNEL): cv.one_of(*CHANNELS, int=True), - cv.Optional(CONF_ADJUSTABLE, default=False): cv.boolean, - } + cv.All( + cv.COMPONENT_SCHEMA.extend( + { + cv.GenerateID(): cv.declare_id(EspLdo), + cv.Required(CONF_VOLTAGE): validate_ldo_voltage, + cv.Required(CONF_CHANNEL): cv.one_of(*CHANNELS, int=True), + cv.Optional(CONF_ADJUSTABLE, default=False): cv.boolean, + cv.Optional(CONF_ALLOW_INTERNAL_CHANNEL, default=False): cv.boolean, + } + ), + validate_ldo_config, ) ), cv.only_on_esp32, @@ -40,7 +81,11 @@ async def to_code(configs): for config in configs: var = cg.new_Pvariable(config[CONF_ID], config[CONF_CHANNEL]) await cg.register_component(var, config) - cg.add(var.set_voltage(config[CONF_VOLTAGE])) + voltage = config[CONF_VOLTAGE] + if voltage == CONF_PASSTHROUGH: + cg.add(var.set_voltage(3300)) + else: + cg.add(var.set_voltage(int(round(voltage * 1000)))) cg.add(var.set_adjustable(config[CONF_ADJUSTABLE])) @@ -84,6 +129,7 @@ def adjusted_ldo_id(value): ), } ), + synchronous=True, ) async def ldo_voltage_adjust_to_code(config, action_id, template_arg, args): parent = await cg.get_variable(config[CONF_ID]) diff --git a/esphome/components/esp_ldo/esp_ldo.cpp b/esphome/components/esp_ldo/esp_ldo.cpp index 2eee855b46..f8ebec1903 100644 --- a/esphome/components/esp_ldo/esp_ldo.cpp +++ b/esphome/components/esp_ldo/esp_ldo.cpp @@ -10,32 +10,34 @@ static const char *const TAG = "esp_ldo"; void EspLdo::setup() { esp_ldo_channel_config_t config{}; config.chan_id = this->channel_; - config.voltage_mv = (int) (this->voltage_ * 1000.0f); + config.voltage_mv = this->voltage_mv_; config.flags.adjustable = this->adjustable_; auto err = esp_ldo_acquire_channel(&config, &this->handle_); if (err != ESP_OK) { - ESP_LOGE(TAG, "Failed to acquire LDO channel %d with voltage %fV", this->channel_, this->voltage_); + ESP_LOGE(TAG, "Failed to acquire LDO channel %d with voltage %dmV", this->channel_, this->voltage_mv_); this->mark_failed(LOG_STR("Failed to acquire LDO channel")); } else { - ESP_LOGD(TAG, "Acquired LDO channel %d with voltage %fV", this->channel_, this->voltage_); + ESP_LOGD(TAG, "Acquired LDO channel %d with voltage %dmV", this->channel_, this->voltage_mv_); } } void EspLdo::dump_config() { ESP_LOGCONFIG(TAG, "ESP LDO Channel %d:\n" - " Voltage: %fV\n" + " Voltage: %dmV\n" " Adjustable: %s", - this->channel_, this->voltage_, YESNO(this->adjustable_)); + this->channel_, this->voltage_mv_, YESNO(this->adjustable_)); } void EspLdo::adjust_voltage(float voltage) { if (!std::isfinite(voltage) || voltage < 0.5f || voltage > 2.7f) { - ESP_LOGE(TAG, "Invalid voltage %fV for LDO channel %d", voltage, this->channel_); + ESP_LOGE(TAG, "Invalid voltage %fV for LDO channel %d (must be 0.5V-2.7V)", voltage, this->channel_); return; } - auto erro = esp_ldo_channel_adjust_voltage(this->handle_, (int) (voltage * 1000.0f)); - if (erro != ESP_OK) { - ESP_LOGE(TAG, "Failed to adjust LDO channel %d to voltage %fV: %s", this->channel_, voltage, esp_err_to_name(erro)); + int voltage_mv = (int) roundf(voltage * 1000.0f); + auto err = esp_ldo_channel_adjust_voltage(this->handle_, voltage_mv); + if (err != ESP_OK) { + ESP_LOGE(TAG, "Failed to adjust LDO channel %d to voltage %dmV: %s", this->channel_, voltage_mv, + esp_err_to_name(err)); } } diff --git a/esphome/components/esp_ldo/esp_ldo.h b/esphome/components/esp_ldo/esp_ldo.h index 9edd303e16..1a20f1d08a 100644 --- a/esphome/components/esp_ldo/esp_ldo.h +++ b/esphome/components/esp_ldo/esp_ldo.h @@ -15,7 +15,7 @@ class EspLdo : public Component { void dump_config() override; void set_adjustable(bool adjustable) { this->adjustable_ = adjustable; } - void set_voltage(float voltage) { this->voltage_ = voltage; } + void set_voltage(int voltage_mv) { this->voltage_mv_ = voltage_mv; } void adjust_voltage(float voltage); float get_setup_priority() const override { return setup_priority::BUS; // LDO setup should be done early @@ -23,7 +23,7 @@ class EspLdo : public Component { protected: int channel_; - float voltage_{2.7}; + int voltage_mv_{2700}; bool adjustable_{false}; esp_ldo_channel_handle_t handle_{}; }; diff --git a/esphome/components/esphome/ota/__init__.py b/esphome/components/esphome/ota/__init__.py index 2f637d714d..337064dd27 100644 --- a/esphome/components/esphome/ota/__init__.py +++ b/esphome/components/esphome/ota/__init__.py @@ -97,8 +97,9 @@ def _consume_ota_sockets(config: ConfigType) -> ConfigType: """Register socket needs for OTA component.""" from esphome.components import socket - # OTA needs 1 listening socket (client connections are temporary during updates) - socket.consume_sockets(1, "ota")(config) + # OTA needs 1 listening socket. The active transfer connection during an update + # uses a TCP PCB from the general pool, covered by MIN_TCP_SOCKETS headroom. + socket.consume_sockets(1, "ota", socket.SocketType.TCP_LISTEN)(config) return config diff --git a/esphome/components/esphome/ota/ota_esphome.cpp b/esphome/components/esphome/ota/ota_esphome.cpp index df2ea98f2c..972d2b2b8d 100644 --- a/esphome/components/esphome/ota/ota_esphome.cpp +++ b/esphome/components/esphome/ota/ota_esphome.cpp @@ -18,6 +18,7 @@ #include #include +#include namespace esphome { @@ -28,10 +29,9 @@ static constexpr uint32_t OTA_SOCKET_TIMEOUT_HANDSHAKE = 20000; // milliseconds static constexpr uint32_t OTA_SOCKET_TIMEOUT_DATA = 90000; // milliseconds for data transfer void ESPHomeOTAComponent::setup() { - this->server_ = socket::socket_ip_loop_monitored(SOCK_STREAM, 0); // monitored for incoming connections + this->server_ = socket::socket_ip_loop_monitored(SOCK_STREAM, 0).release(); // monitored for incoming connections if (this->server_ == nullptr) { - this->log_socket_error_(LOG_STR("creation")); - this->mark_failed(); + this->server_failed_(LOG_STR("creation")); return; } int enable = 1; @@ -42,8 +42,7 @@ void ESPHomeOTAComponent::setup() { } err = this->server_->setblocking(false); if (err != 0) { - this->log_socket_error_(LOG_STR("non-blocking")); - this->mark_failed(); + this->server_failed_(LOG_STR("nonblocking")); return; } @@ -51,22 +50,19 @@ void ESPHomeOTAComponent::setup() { socklen_t sl = socket::set_sockaddr_any((struct sockaddr *) &server, sizeof(server), this->port_); if (sl == 0) { - this->log_socket_error_(LOG_STR("set sockaddr")); - this->mark_failed(); + this->server_failed_(LOG_STR("set sockaddr")); return; } err = this->server_->bind((struct sockaddr *) &server, sizeof(server)); if (err != 0) { - this->log_socket_error_(LOG_STR("bind")); - this->mark_failed(); + this->server_failed_(LOG_STR("bind")); return; } err = this->server_->listen(1); // Only one client at a time if (err != 0) { - this->log_socket_error_(LOG_STR("listen")); - this->mark_failed(); + this->server_failed_(LOG_STR("listen")); return; } } @@ -243,6 +239,31 @@ void ESPHomeOTAComponent::handle_data_() { /// and reboots on success. /// /// Authentication has already been handled in the non-blocking states AUTH_SEND/AUTH_READ. + /// + /// Socket I/O strategy: + /// + /// Before this function, the handshake states use non-blocking I/O: + /// read()/write() return immediately with EWOULDBLOCK if no data + /// loop() retries on next iteration (~16ms), no delay needed + /// + /// This function switches to blocking mode with SO_RCVTIMEO/SO_SNDTIMEO: + /// + /// Path | Wait mechanism | WDT strategy + /// --------------|------------------------|--------------------------- + /// Main read | SO_RCVTIMEO (2s block) | feed_wdt() only, no delay + /// readall_() | SO_RCVTIMEO (2s block) | feed_wdt() + delay(0) + /// writeall_() | SO_SNDTIMEO (2s block) | feed_wdt() + delay(1) + /// + /// readall_() uses delay(0) because SO_RCVTIMEO already waited — just yield. + /// writeall_() uses delay(1) because on raw TCP (ESP8266, RP2040) writes + /// never block (tcp_write returns immediately), so delay(1) prevents spinning. + /// + /// Platform details: + /// BSD sockets (ESP32): setblocking(true) makes read/write block + /// lwip sockets (LT): setblocking(true) makes read/write block + /// Raw TCP (8266, RP2040): setblocking is no-op; SO_RCVTIMEO uses + /// socket_delay()/socket_wake() in read(); + /// write() always returns immediately ota::OTAResponseTypes error_code = ota::OTA_RESPONSE_ERROR_UNKNOWN; bool update_started = false; size_t total = 0; @@ -254,6 +275,14 @@ void ESPHomeOTAComponent::handle_data_() { size_t size_acknowledged = 0; #endif + // Set socket timeouts and blocking mode (see strategy table above) + struct timeval tv; + tv.tv_sec = 2; + tv.tv_usec = 0; + this->client_->setsockopt(SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof(tv)); + this->client_->setsockopt(SOL_SOCKET, SO_SNDTIMEO, &tv, sizeof(tv)); + this->client_->setblocking(true); + // Acknowledge auth OK - 1 byte this->write_byte_(ota::OTA_RESPONSE_AUTH_OK); @@ -303,11 +332,13 @@ void ESPHomeOTAComponent::handle_data_() { size_t requested = remaining < OTA_BUFFER_SIZE ? remaining : OTA_BUFFER_SIZE; ssize_t read = this->client_->read(buf, requested); if (read == -1) { - if (this->would_block_(errno)) { - this->yield_and_feed_watchdog_(); + const int err = errno; + if (this->would_block_(err)) { + // read() already waited up to SO_RCVTIMEO for data, just feed WDT + App.feed_wdt(); continue; } - ESP_LOGW(TAG, "Read err %d", errno); + ESP_LOGW(TAG, "Read err %d", err); goto error; // NOLINT(cppcoreguidelines-avoid-goto) } else if (read == 0) { ESP_LOGW(TAG, "Remote closed"); @@ -396,8 +427,9 @@ bool ESPHomeOTAComponent::readall_(uint8_t *buf, size_t len) { ssize_t read = this->client_->read(buf + at, len - at); if (read == -1) { - if (!this->would_block_(errno)) { - ESP_LOGW(TAG, "Read err %zu bytes, errno %d", len, errno); + const int err = errno; + if (!this->would_block_(err)) { + ESP_LOGW(TAG, "Read err %zu bytes, errno %d", len, err); return false; } } else if (read == 0) { @@ -406,7 +438,9 @@ bool ESPHomeOTAComponent::readall_(uint8_t *buf, size_t len) { } else { at += read; } - this->yield_and_feed_watchdog_(); + // read() already waited via SO_RCVTIMEO, just yield without 1ms stall + App.feed_wdt(); + delay(0); } return true; @@ -423,14 +457,18 @@ bool ESPHomeOTAComponent::writeall_(const uint8_t *buf, size_t len) { ssize_t written = this->client_->write(buf + at, len - at); if (written == -1) { - if (!this->would_block_(errno)) { - ESP_LOGW(TAG, "Write err %zu bytes, errno %d", len, errno); + const int err = errno; + if (!this->would_block_(err)) { + ESP_LOGW(TAG, "Write err %zu bytes, errno %d", len, err); return false; } + // EWOULDBLOCK: on raw TCP writes never block, delay(1) prevents spinning + this->yield_and_feed_watchdog_(); } else { at += written; + // write() may block up to SO_SNDTIMEO on BSD/lwip sockets, feed WDT + App.feed_wdt(); } - this->yield_and_feed_watchdog_(); } return true; } @@ -455,6 +493,15 @@ void ESPHomeOTAComponent::log_remote_closed_(const LogString *during) { ESP_LOGW(TAG, "Remote closed at %s", LOG_STR_ARG(during)); } +void ESPHomeOTAComponent::server_failed_(const LogString *msg) { + this->log_socket_error_(msg); + // No explicit close() needed — listen sockets have no active connections on + // failure/shutdown. Destructor handles fd cleanup (close or abort per platform). + delete this->server_; + this->server_ = nullptr; + this->mark_failed(); +} + bool ESPHomeOTAComponent::handle_read_error_(ssize_t read, const LogString *desc) { if (read == -1 && this->would_block_(errno)) { return false; // No data yet, try again next loop diff --git a/esphome/components/esphome/ota/ota_esphome.h b/esphome/components/esphome/ota/ota_esphome.h index e199b7e406..f3a5952398 100644 --- a/esphome/components/esphome/ota/ota_esphome.h +++ b/esphome/components/esphome/ota/ota_esphome.h @@ -2,7 +2,7 @@ #include "esphome/core/defines.h" #ifdef USE_OTA -#include "esphome/components/ota/ota_backend.h" +#include "esphome/components/ota/ota_backend_factory.h" #include "esphome/components/socket/socket.h" #include "esphome/core/helpers.h" #include "esphome/core/log.h" @@ -12,7 +12,7 @@ namespace esphome { /// ESPHomeOTAComponent provides a simple way to integrate Over-the-Air updates into your app using ArduinoOTA. -class ESPHomeOTAComponent : public ota::OTAComponent { +class ESPHomeOTAComponent final : public ota::OTAComponent { public: enum class OTAState : uint8_t { IDLE, @@ -66,6 +66,7 @@ class ESPHomeOTAComponent : public ota::OTAComponent { this->handshake_buf_pos_ = 0; // Reset buffer position for next state } + void server_failed_(const LogString *msg); void log_socket_error_(const LogString *msg); void log_read_error_(const LogString *what); void log_start_(const LogString *phase); @@ -83,9 +84,9 @@ class ESPHomeOTAComponent : public ota::OTAComponent { std::unique_ptr auth_buf_; #endif // USE_OTA_PASSWORD - std::unique_ptr server_; + socket::ListenSocket *server_{nullptr}; std::unique_ptr client_; - std::unique_ptr backend_; + ota::OTABackendPtr backend_; uint32_t client_connect_time_{0}; uint16_t port_; diff --git a/esphome/components/espnow/__init__.py b/esphome/components/espnow/__init__.py index 1f5ca1104a..00703bc228 100644 --- a/esphome/components/espnow/__init__.py +++ b/esphome/components/espnow/__init__.py @@ -13,7 +13,7 @@ from esphome.const import ( CONF_TRIGGER_ID, CONF_WIFI, ) -from esphome.core import CORE, HexInt +from esphome.core import HexInt from esphome.types import ConfigType CODEOWNERS = ["@jesserockz"] @@ -124,9 +124,6 @@ async def to_code(config): var = cg.new_Pvariable(config[CONF_ID]) await cg.register_component(var, config) - if CORE.using_arduino: - cg.add_library("WiFi", None) - # ESP-NOW uses wake_loop_threadsafe() to wake the main loop from ESP-NOW callbacks # This enables low-latency event processing instead of waiting for select() timeout socket.require_wake_loop_threadsafe() @@ -135,6 +132,7 @@ async def to_code(config): if wifi_channel := config.get(CONF_CHANNEL): cg.add(var.set_wifi_channel(wifi_channel)) + cg.add(var.set_enable_on_boot(config[CONF_ENABLE_ON_BOOT])) cg.add(var.set_auto_add_peer(config[CONF_AUTO_ADD_PEER])) for peer in config.get(CONF_PEERS, []): @@ -223,6 +221,7 @@ SEND_SCHEMA.add_extra(_validate_send_action) "espnow.send", SendAction, SEND_SCHEMA, + synchronous=False, ) @automation.register_action( "espnow.broadcast", @@ -235,6 +234,7 @@ SEND_SCHEMA.add_extra(_validate_send_action) ), key=CONF_DATA, ), + synchronous=False, ) async def send_action( config: ConfigType, @@ -274,6 +274,7 @@ async def send_action( PEER_SCHEMA, key=CONF_ADDRESS, ), + synchronous=True, ) @automation.register_action( "espnow.peer.delete", @@ -282,6 +283,7 @@ async def send_action( PEER_SCHEMA, key=CONF_ADDRESS, ), + synchronous=True, ) async def peer_action( config: ConfigType, @@ -306,6 +308,7 @@ async def peer_action( }, key=CONF_CHANNEL, ), + synchronous=True, ) async def channel_action( config: ConfigType, diff --git a/esphome/components/espnow/automation.h b/esphome/components/espnow/automation.h index 0b26681400..0fbb14e388 100644 --- a/esphome/components/espnow/automation.h +++ b/esphome/components/espnow/automation.h @@ -138,7 +138,7 @@ class OnReceiveTrigger : public Trigger, public ESPNowUnknownPeerHandler { @@ -167,7 +167,7 @@ class OnBroadcastedTrigger : public Trigger + #include "esphome/core/application.h" #include "esphome/core/defines.h" #include "esphome/core/helpers.h" @@ -87,7 +89,8 @@ void on_send_report(const uint8_t *mac_addr, esp_now_send_status_t status) // Push the packet to the queue global_esp_now->receive_packet_queue_.push(packet); - // Push always because we're the only producer and the pool ensures we never exceed queue size + // Push always succeeds: pool is sized to queue capacity (SIZE-1), so if + // allocate() returned non-null, the queue cannot be full. // Wake main loop immediately to process ESP-NOW send event instead of waiting for select() timeout #if defined(USE_SOCKET_SELECT_SUPPORT) && defined(USE_WAKE_LOOP_THREADSAFE) @@ -109,7 +112,8 @@ void on_data_received(const esp_now_recv_info_t *info, const uint8_t *data, int // Push the packet to the queue global_esp_now->receive_packet_queue_.push(packet); - // Push always because we're the only producer and the pool ensures we never exceed queue size + // Push always succeeds: pool is sized to queue capacity (SIZE-1), so if + // allocate() returned non-null, the queue cannot be full. // Wake main loop immediately to process ESP-NOW receive event instead of waiting for select() timeout #if defined(USE_SOCKET_SELECT_SUPPORT) && defined(USE_WAKE_LOOP_THREADSAFE) @@ -264,7 +268,7 @@ void ESPNowComponent::loop() { if (wifi::global_wifi_component != nullptr && wifi::global_wifi_component->is_connected()) { int32_t new_channel = wifi::global_wifi_component->get_wifi_channel(); if (new_channel != this->wifi_channel_) { - ESP_LOGI(TAG, "Wifi Channel is changed from %d to %d.", this->wifi_channel_, new_channel); + ESP_LOGI(TAG, "Wifi Channel is changed from %d to %" PRId32 ".", this->wifi_channel_, new_channel); this->wifi_channel_ = new_channel; } } diff --git a/esphome/components/espnow/espnow_component.h b/esphome/components/espnow/espnow_component.h index 9941e97227..ee4adc1b4d 100644 --- a/esphome/components/espnow/espnow_component.h +++ b/esphome/components/espnow/espnow_component.h @@ -163,10 +163,14 @@ class ESPNowComponent : public Component { uint8_t own_address_[ESP_NOW_ETH_ALEN]{0}; LockFreeQueue receive_packet_queue_{}; - EventPool receive_packet_pool_{}; + // Pool sized to queue capacity (SIZE-1) because LockFreeQueue is a ring + // buffer that holds N-1 elements. This guarantees allocate() returns nullptr + // before push() can fail, preventing a pool slot leak. + EventPool receive_packet_pool_{}; LockFreeQueue send_packet_queue_{}; - EventPool send_packet_pool_{}; + // Pool sized to queue capacity (SIZE-1) — see receive_packet_pool_ comment. + EventPool send_packet_pool_{}; ESPNowSendPacket *current_send_packet_{nullptr}; // Currently sending packet, nullptr if none uint8_t wifi_channel_{0}; diff --git a/esphome/components/espnow/packet_transport/espnow_transport.cpp b/esphome/components/espnow/packet_transport/espnow_transport.cpp index 3d16f28c7d..6e4f606466 100644 --- a/esphome/components/espnow/packet_transport/espnow_transport.cpp +++ b/esphome/components/espnow/packet_transport/espnow_transport.cpp @@ -21,11 +21,9 @@ void ESPNowTransport::setup() { return; } - ESP_LOGI(TAG, - "Registering ESP-NOW handlers\n" - "Peer address: %02X:%02X:%02X:%02X:%02X:%02X", - this->peer_address_[0], this->peer_address_[1], this->peer_address_[2], this->peer_address_[3], - this->peer_address_[4], this->peer_address_[5]); + ESP_LOGI(TAG, "Registering ESP-NOW handlers, peer: %02X:%02X:%02X:%02X:%02X:%02X", this->peer_address_[0], + this->peer_address_[1], this->peer_address_[2], this->peer_address_[3], this->peer_address_[4], + this->peer_address_[5]); // Register received handler this->parent_->register_received_handler(this); diff --git a/esphome/components/ethernet/__init__.py b/esphome/components/ethernet/__init__.py index 8b3061036c..8918cec193 100644 --- a/esphome/components/ethernet/__init__.py +++ b/esphome/components/ethernet/__init__.py @@ -1,22 +1,10 @@ +from dataclasses import dataclass import logging -from esphome import pins +from esphome import automation, pins import esphome.codegen as cg -from esphome.components.esp32 import ( - VARIANT_ESP32, - VARIANT_ESP32C3, - VARIANT_ESP32C5, - VARIANT_ESP32C6, - VARIANT_ESP32C61, - VARIANT_ESP32P4, - VARIANT_ESP32S2, - VARIANT_ESP32S3, - add_idf_component, - add_idf_sdkconfig_option, - get_esp32_variant, -) from esphome.components.network import ip_address_literal -from esphome.components.spi import CONF_INTERFACE_INDEX, get_spi_interface +from esphome.config_helpers import filter_source_files_from_platform import esphome.config_validation as cv from esphome.const import ( CONF_ADDRESS, @@ -34,6 +22,8 @@ from esphome.const import ( CONF_MODE, CONF_MOSI_PIN, CONF_NUMBER, + CONF_ON_CONNECT, + CONF_ON_DISCONNECT, CONF_PAGE_ID, CONF_PIN, CONF_POLLING_INTERVAL, @@ -46,6 +36,9 @@ from esphome.const import ( CONF_VALUE, KEY_CORE, KEY_FRAMEWORK_VERSION, + KEY_NATIVE_IDF, + Platform, + PlatformFramework, ) from esphome.core import ( CORE, @@ -57,12 +50,14 @@ import esphome.final_validate as fv from esphome.types import ConfigType CONFLICTS_WITH = ["wifi"] -DEPENDENCIES = ["esp32"] AUTO_LOAD = ["network"] LOGGER = logging.getLogger(__name__) # Key for tracking IP state listener count in CORE.data ETHERNET_IP_STATE_LISTENERS_KEY = "ethernet_ip_state_listeners" +# Key for tracking configured ethernet type +ETHERNET_TYPE_KEY = "ethernet_type" +KEY_ETHERNET = "ethernet" def request_ethernet_ip_state_listener() -> None: @@ -122,21 +117,66 @@ ETHERNET_TYPES = { "JL1101": EthernetType.ETHERNET_TYPE_JL1101, "KSZ8081": EthernetType.ETHERNET_TYPE_KSZ8081, "KSZ8081RNA": EthernetType.ETHERNET_TYPE_KSZ8081RNA, + "W5100": EthernetType.ETHERNET_TYPE_W5100, "W5500": EthernetType.ETHERNET_TYPE_W5500, "OPENETH": EthernetType.ETHERNET_TYPE_OPENETH, "DM9051": EthernetType.ETHERNET_TYPE_DM9051, "LAN8670": EthernetType.ETHERNET_TYPE_LAN8670, + "ENC28J60": EthernetType.ETHERNET_TYPE_ENC28J60, } # PHY types that need compile-time defines for conditional compilation +# Each RMII PHY type gets a define so unused PHY drivers are excluded by the linker _PHY_TYPE_TO_DEFINE = { + "LAN8720": "USE_ETHERNET_LAN8720", + "RTL8201": "USE_ETHERNET_RTL8201", + "DP83848": "USE_ETHERNET_DP83848", + "IP101": "USE_ETHERNET_IP101", + "JL1101": "USE_ETHERNET_JL1101", "KSZ8081": "USE_ETHERNET_KSZ8081", "KSZ8081RNA": "USE_ETHERNET_KSZ8081", + "W5100": "USE_ETHERNET_W5100", + "W5500": "USE_ETHERNET_W5500", + "DM9051": "USE_ETHERNET_DM9051", "LAN8670": "USE_ETHERNET_LAN8670", - # Add other PHY types here only if they need conditional compilation + "ENC28J60": "USE_ETHERNET_ENC28J60", } -SPI_ETHERNET_TYPES = ["W5500", "DM9051"] + +@dataclass(frozen=True) +class IDFRegistryComponent: + """An ESP-IDF component from the Espressif Component Registry.""" + + name: str + version: str + + +# IDF 6.0 moved per-chip PHY/MAC drivers to the Espressif Component Registry. +_IDF6_ETHERNET_COMPONENTS: dict[str, IDFRegistryComponent] = { + "LAN8720": IDFRegistryComponent("espressif/lan87xx", "1.0.0"), + "RTL8201": IDFRegistryComponent("espressif/rtl8201", "1.0.1"), + "DP83848": IDFRegistryComponent("espressif/dp83848", "1.0.0"), + "IP101": IDFRegistryComponent("espressif/ip101", "1.0.0"), + "KSZ8081": IDFRegistryComponent("espressif/ksz80xx", "1.0.0"), + "KSZ8081RNA": IDFRegistryComponent("espressif/ksz80xx", "1.0.0"), + "W5500": IDFRegistryComponent("espressif/w5500", "1.0.1"), + "DM9051": IDFRegistryComponent("espressif/dm9051", "1.0.0"), + "ENC28J60": IDFRegistryComponent("espressif/enc28j60", "1.0.1"), + "LAN8670": IDFRegistryComponent("espressif/lan867x", "2.0.0"), +} + +# These types are always external IDF components (never built-in to ESP-IDF) +_ALWAYS_EXTERNAL_IDF_COMPONENTS = {"LAN8670", "ENC28J60"} + +# ESP32-only SPI ethernet types (W5100 is RP2040-only, no ESP-IDF driver) +SPI_ETHERNET_TYPES = {"W5500", "DM9051", "ENC28J60"} +# RP2040-supported SPI ethernet types +RP2040_SPI_ETHERNET_TYPES = {"W5100", "W5500", "ENC28J60"} +_RP2040_SPI_LIBRARIES = { + "W5100": "lwIP_w5100", + "W5500": "lwIP_w5500", + "ENC28J60": "lwIP_enc28j60", +} SPI_ETHERNET_DEFAULT_POLLING_INTERVAL = TimePeriodMilliseconds(milliseconds=10) emac_rmii_clock_mode_t = cg.global_ns.enum("emac_rmii_clock_mode_t") @@ -172,16 +212,22 @@ EthernetComponent = ethernet_ns.class_("EthernetComponent", cg.Component) ManualIP = ethernet_ns.struct("ManualIP") -def _is_framework_spi_polling_mode_supported(): - # SPI Ethernet without IRQ feature is added in - # esp-idf >= (5.3+ ,5.2.1+, 5.1.4) - # Note: Arduino now uses ESP-IDF as a component, so we only check IDF version - framework_version = CORE.data[KEY_CORE][KEY_FRAMEWORK_VERSION] - if framework_version >= cv.Version(5, 3, 0): +def _is_framework_spi_polling_mode_supported() -> bool: + """Check if ESP-IDF framework supports SPI polling mode (ESP32 only). + + SPI Ethernet without IRQ feature is added in + esp-idf >= (5.3+, 5.2.1+, 5.1.4) + """ + if not CORE.is_esp32: + return False + from esphome.components.esp32 import idf_version + + ver = idf_version() + if ver >= cv.Version(5, 3, 0): return True - if cv.Version(5, 3, 0) > framework_version >= cv.Version(5, 2, 1): + if cv.Version(5, 3, 0) > ver >= cv.Version(5, 2, 1): return True - if cv.Version(5, 2, 0) > framework_version >= cv.Version(5, 1, 4): # noqa: SIM103 + if cv.Version(5, 2, 0) > ver >= cv.Version(5, 1, 4): # noqa: SIM103 return True return False @@ -194,58 +240,102 @@ def _validate(config): use_address = CORE.name + config[CONF_DOMAIN] config[CONF_USE_ADDRESS] = use_address - if config[CONF_TYPE] in SPI_ETHERNET_TYPES: - if _is_framework_spi_polling_mode_supported(): - if CONF_POLLING_INTERVAL in config and CONF_INTERRUPT_PIN in config: - raise cv.Invalid( - f"Cannot specify more than one of {CONF_INTERRUPT_PIN}, {CONF_POLLING_INTERVAL}" - ) - if CONF_POLLING_INTERVAL not in config and CONF_INTERRUPT_PIN not in config: - config[CONF_POLLING_INTERVAL] = SPI_ETHERNET_DEFAULT_POLLING_INTERVAL - else: - if CONF_POLLING_INTERVAL in config: - raise cv.Invalid( - "In this version of the framework " - f"({CORE.target_framework} {CORE.data[KEY_CORE][KEY_FRAMEWORK_VERSION]}), " - f"'{CONF_POLLING_INTERVAL}' is not supported." - ) - if CONF_INTERRUPT_PIN not in config: - raise cv.Invalid( - "In this version of the framework " - f"({CORE.target_framework} {CORE.data[KEY_CORE][KEY_FRAMEWORK_VERSION]}), " - f"'{CONF_INTERRUPT_PIN}' is a required option for [ethernet]." - ) - if CONF_INTERFACE not in config: - variant = get_esp32_variant() - if variant in ( - VARIANT_ESP32C3, - VARIANT_ESP32C5, - VARIANT_ESP32C6, - VARIANT_ESP32C61, - VARIANT_ESP32S2, - VARIANT_ESP32S3, - ): - config[CONF_INTERFACE] = "spi2" + if CORE.is_esp32: + if config[CONF_TYPE] in SPI_ETHERNET_TYPES: + # ENC28J60 driver does not support polling mode - interrupt is required + if config[CONF_TYPE] == "ENC28J60": + if CONF_POLLING_INTERVAL in config: + raise cv.Invalid( + f"'{CONF_POLLING_INTERVAL}' is not supported for ENC28J60. " + f"'{CONF_INTERRUPT_PIN}' is required." + ) + if CONF_INTERRUPT_PIN not in config: + raise cv.Invalid( + f"'{CONF_INTERRUPT_PIN}' is a required option for ENC28J60." + ) + elif _is_framework_spi_polling_mode_supported(): + if CONF_POLLING_INTERVAL in config and CONF_INTERRUPT_PIN in config: + raise cv.Invalid( + f"Cannot specify more than one of {CONF_INTERRUPT_PIN}, {CONF_POLLING_INTERVAL}" + ) + if ( + CONF_POLLING_INTERVAL not in config + and CONF_INTERRUPT_PIN not in config + ): + config[CONF_POLLING_INTERVAL] = ( + SPI_ETHERNET_DEFAULT_POLLING_INTERVAL + ) else: - config[CONF_INTERFACE] = "spi3" - elif config[CONF_TYPE] != "OPENETH": - if CONF_CLK_MODE in config: - LOGGER.warning( - "[ethernet] The 'clk_mode' option is deprecated and will be removed in ESPHome 2026.1. " - "Please update your configuration to use 'clk' instead." - ) - mode = CLK_MODES_DEPRECATED[config[CONF_CLK_MODE]] - config[CONF_CLK] = CLK_SCHEMA({CONF_MODE: mode[0], CONF_PIN: mode[1]}) - del config[CONF_CLK_MODE] - elif CONF_CLK not in config: - raise cv.Invalid("'clk' is a required option for [ethernet].") - variant = get_esp32_variant() - if variant not in (VARIANT_ESP32, VARIANT_ESP32P4): - raise cv.Invalid( - f"{config[CONF_TYPE]} PHY requires RMII interface and is only supported " - f"on ESP32 classic and ESP32-P4, not {variant}" + if CONF_POLLING_INTERVAL in config: + raise cv.Invalid( + "In this version of the framework " + f"({CORE.target_framework} {CORE.data[KEY_CORE][KEY_FRAMEWORK_VERSION]}), " + f"'{CONF_POLLING_INTERVAL}' is not supported." + ) + if CONF_INTERRUPT_PIN not in config: + raise cv.Invalid( + "In this version of the framework " + f"({CORE.target_framework} {CORE.data[KEY_CORE][KEY_FRAMEWORK_VERSION]}), " + f"'{CONF_INTERRUPT_PIN}' is a required option for [ethernet]." + ) + if CONF_INTERFACE not in config: + from esphome.components.esp32 import ( + VARIANT_ESP32C3, + VARIANT_ESP32C5, + VARIANT_ESP32C6, + VARIANT_ESP32C61, + VARIANT_ESP32S2, + VARIANT_ESP32S3, + get_esp32_variant, + ) + + variant = get_esp32_variant() + if variant in ( + VARIANT_ESP32C3, + VARIANT_ESP32C5, + VARIANT_ESP32C6, + VARIANT_ESP32C61, + VARIANT_ESP32S2, + VARIANT_ESP32S3, + ): + config[CONF_INTERFACE] = "spi2" + else: + config[CONF_INTERFACE] = "spi3" + elif config[CONF_TYPE] != "OPENETH": + from esphome.components.esp32 import ( + VARIANT_ESP32, + VARIANT_ESP32P4, + get_esp32_variant, ) + if CONF_CLK_MODE in config: + mode, pin = CLK_MODES_DEPRECATED[config[CONF_CLK_MODE]] + LOGGER.warning( + "[ethernet] The 'clk_mode' option is deprecated. " + "Please replace 'clk_mode: %s' with:\n" + " clk:\n" + " mode: %s\n" + " pin: %s\n" + "Removal scheduled for 2026.7.0.", + config[CONF_CLK_MODE], + mode, + pin, + ) + config[CONF_CLK] = CLK_SCHEMA({CONF_MODE: mode, CONF_PIN: pin}) + del config[CONF_CLK_MODE] + elif CONF_CLK not in config: + raise cv.Invalid("'clk' is a required option for [ethernet].") + variant = get_esp32_variant() + if variant not in (VARIANT_ESP32, VARIANT_ESP32P4): + raise cv.Invalid( + f"{config[CONF_TYPE]} PHY requires RMII interface and is only supported " + f"on ESP32 classic and ESP32-P4, not {variant}" + ) + elif CORE.is_rp2040 and config[CONF_TYPE] not in RP2040_SPI_ETHERNET_TYPES: + raise cv.Invalid( + f"Only {', '.join(sorted(RP2040_SPI_ETHERNET_TYPES))} are supported on RP2040, " + f"not {config[CONF_TYPE]}" + ) return config @@ -256,6 +346,8 @@ BASE_SCHEMA = cv.Schema( cv.Optional(CONF_DOMAIN, default=".local"): cv.domain_name, cv.Optional(CONF_USE_ADDRESS): cv.string_strict, cv.Optional(CONF_MAC_ADDRESS): cv.mac_address, + cv.Optional(CONF_ON_CONNECT): automation.validate_automation(single=True), + cv.Optional(CONF_ON_DISCONNECT): automation.validate_automation(single=True), } ).extend(cv.COMPONENT_SCHEMA) @@ -272,42 +364,54 @@ CLK_SCHEMA = cv.Schema( cv.Required(CONF_PIN): pins.internal_gpio_pin_number, } ) -RMII_SCHEMA = BASE_SCHEMA.extend( - cv.Schema( - { - cv.Required(CONF_MDC_PIN): pins.internal_gpio_output_pin_number, - cv.Required(CONF_MDIO_PIN): pins.internal_gpio_output_pin_number, - cv.Optional(CONF_CLK_MODE): cv.enum( - CLK_MODES_DEPRECATED, upper=True, space="_" - ), - cv.Optional(CONF_CLK): CLK_SCHEMA, - cv.Optional(CONF_PHY_ADDR, default=0): cv.int_range(min=0, max=31), - cv.Optional(CONF_POWER_PIN): pins.internal_gpio_output_pin_number, - cv.Optional(CONF_PHY_REGISTERS): cv.ensure_list(PHY_REGISTER_SCHEMA), - } - ) +RMII_SCHEMA = cv.All( + BASE_SCHEMA.extend( + cv.Schema( + { + cv.Required(CONF_MDC_PIN): pins.internal_gpio_output_pin_number, + cv.Required(CONF_MDIO_PIN): pins.internal_gpio_output_pin_number, + cv.Optional(CONF_CLK_MODE): cv.enum( + CLK_MODES_DEPRECATED, upper=True, space="_" + ), + cv.Optional(CONF_CLK): CLK_SCHEMA, + cv.Optional(CONF_PHY_ADDR, default=0): cv.int_range(min=0, max=31), + cv.Optional(CONF_POWER_PIN): pins.internal_gpio_output_pin_number, + cv.Optional(CONF_PHY_REGISTERS): cv.ensure_list(PHY_REGISTER_SCHEMA), + } + ) + ), + cv.only_on([Platform.ESP32]), ) -SPI_SCHEMA = BASE_SCHEMA.extend( - cv.Schema( - { - cv.Required(CONF_CLK_PIN): pins.internal_gpio_output_pin_number, - cv.Required(CONF_MISO_PIN): pins.internal_gpio_input_pin_number, - cv.Required(CONF_MOSI_PIN): pins.internal_gpio_output_pin_number, - cv.Required(CONF_CS_PIN): pins.internal_gpio_output_pin_number, - cv.Optional(CONF_INTERRUPT_PIN): pins.internal_gpio_input_pin_number, - cv.Optional(CONF_RESET_PIN): pins.internal_gpio_output_pin_number, - cv.Optional(CONF_CLOCK_SPEED, default="26.67MHz"): cv.All( - cv.frequency, cv.int_range(int(8e6), int(80e6)) - ), - cv.Optional(CONF_INTERFACE): cv.one_of(*SPI_INTERFACE_MAP.keys()), - # Set default value (SPI_ETHERNET_DEFAULT_POLLING_INTERVAL) at _validate() - cv.Optional(CONF_POLLING_INTERVAL): cv.All( - cv.positive_time_period_milliseconds, - cv.Range(min=TimePeriodMilliseconds(milliseconds=1)), - ), - } +SPI_SCHEMA = cv.All( + BASE_SCHEMA.extend( + cv.Schema( + { + cv.Required(CONF_CLK_PIN): pins.internal_gpio_output_pin_number, + cv.Required(CONF_MISO_PIN): pins.internal_gpio_input_pin_number, + cv.Required(CONF_MOSI_PIN): pins.internal_gpio_output_pin_number, + cv.Required(CONF_CS_PIN): pins.internal_gpio_output_pin_number, + cv.Optional(CONF_INTERRUPT_PIN): pins.internal_gpio_input_pin_number, + cv.Optional(CONF_RESET_PIN): pins.internal_gpio_output_pin_number, + cv.SplitDefault(CONF_CLOCK_SPEED, esp32="26.67MHz"): cv.All( + cv.only_on_esp32, + cv.frequency, + cv.int_range(int(8e6), int(80e6)), + ), + cv.Optional(CONF_INTERFACE): cv.All( + cv.only_on_esp32, + cv.one_of(*SPI_INTERFACE_MAP.keys()), + ), + # Set default value (SPI_ETHERNET_DEFAULT_POLLING_INTERVAL) at _validate() + cv.Optional(CONF_POLLING_INTERVAL): cv.All( + cv.only_on_esp32, + cv.positive_time_period_milliseconds, + cv.Range(min=TimePeriodMilliseconds(milliseconds=1)), + ), + } + ), ), + cv.only_on([Platform.ESP32, Platform.RP2040]), ) CONFIG_SCHEMA = cv.All( @@ -320,9 +424,11 @@ CONFIG_SCHEMA = cv.All( "JL1101": RMII_SCHEMA, "KSZ8081": RMII_SCHEMA, "KSZ8081RNA": RMII_SCHEMA, + "W5100": cv.All(SPI_SCHEMA, cv.only_on([Platform.RP2040])), "W5500": SPI_SCHEMA, - "OPENETH": BASE_SCHEMA, + "OPENETH": cv.All(BASE_SCHEMA, cv.only_on([Platform.ESP32])), "DM9051": SPI_SCHEMA, + "ENC28J60": SPI_SCHEMA, "LAN8670": RMII_SCHEMA, }, upper=True, @@ -332,8 +438,12 @@ CONFIG_SCHEMA = cv.All( def _final_validate_spi(config): + if not CORE.is_esp32: + return # SPI interface validation is ESP32-only if config[CONF_TYPE] not in SPI_ETHERNET_TYPES: return + from esphome.components.spi import CONF_INTERFACE_INDEX, get_spi_interface + if spi_configs := fv.full_config.get().get(CONF_SPI): spi_host = SPI_INTERFACE_MAP[config[CONF_INTERFACE]] for spi_conf in spi_configs: @@ -371,6 +481,51 @@ async def to_code(config): var = cg.new_Pvariable(config[CONF_ID]) await cg.register_component(var, config) + if CORE.is_esp32: + await _to_code_esp32(var, config) + elif CORE.is_rp2040: + await _to_code_rp2040(var, config) + + cg.add(var.set_type(ETHERNET_TYPES[config[CONF_TYPE]])) + cg.add(var.set_use_address(config[CONF_USE_ADDRESS])) + CORE.data.setdefault(KEY_ETHERNET, {})[ETHERNET_TYPE_KEY] = config[CONF_TYPE] + + if CONF_MANUAL_IP in config: + cg.add_define("USE_ETHERNET_MANUAL_IP") + cg.add(var.set_manual_ip(manual_ip(config[CONF_MANUAL_IP]))) + + # Add compile-time define for PHY types with specific code + if phy_define := _PHY_TYPE_TO_DEFINE.get(config[CONF_TYPE]): + cg.add_define(phy_define) + + if mac_address := config.get(CONF_MAC_ADDRESS): + cg.add(var.set_fixed_mac(mac_address.parts)) + + cg.add_define("USE_ETHERNET") + + if on_connect_config := config.get(CONF_ON_CONNECT): + cg.add_define("USE_ETHERNET_CONNECT_TRIGGER") + await automation.build_automation( + var.get_connect_trigger(), [], on_connect_config + ) + + if on_disconnect_config := config.get(CONF_ON_DISCONNECT): + cg.add_define("USE_ETHERNET_DISCONNECT_TRIGGER") + await automation.build_automation( + var.get_disconnect_trigger(), [], on_disconnect_config + ) + + CORE.add_job(final_step) + + +async def _to_code_esp32(var: cg.Pvariable, config: ConfigType) -> None: + from esphome.components.esp32 import ( + add_idf_component, + add_idf_sdkconfig_option, + idf_version, + include_builtin_idf_component, + ) + if config[CONF_TYPE] in SPI_ETHERNET_TYPES: cg.add(var.set_clk_pin(config[CONF_CLK_PIN])) cg.add(var.set_miso_pin(config[CONF_MISO_PIN])) @@ -396,7 +551,12 @@ async def to_code(config): } cg.add(var.set_interface(map[config[CONF_INTERFACE]])) add_idf_sdkconfig_option("CONFIG_ETH_USE_SPI_ETHERNET", True) - add_idf_sdkconfig_option(f"CONFIG_ETH_SPI_ETHERNET_{config[CONF_TYPE]}", True) + # CONFIG_ETH_SPI_ETHERNET_{TYPE} Kconfig options were removed in IDF 6.0 + # ENC28J60 was never built-in to IDF, so it has no Kconfig option + if idf_version() < cv.Version(6, 0, 0) and config[CONF_TYPE] != "ENC28J60": + add_idf_sdkconfig_option( + f"CONFIG_ETH_SPI_ETHERNET_{config[CONF_TYPE]}", True + ) elif config[CONF_TYPE] == "OPENETH": cg.add_define("USE_ETHERNET_OPENETH") add_idf_sdkconfig_option("CONFIG_ETH_USE_OPENETH", True) @@ -416,43 +576,52 @@ async def to_code(config): ) cg.add(var.add_phy_register(reg)) - cg.add(var.set_type(ETHERNET_TYPES[config[CONF_TYPE]])) - cg.add(var.set_use_address(config[CONF_USE_ADDRESS])) - - if CONF_MANUAL_IP in config: - cg.add_define("USE_ETHERNET_MANUAL_IP") - cg.add(var.set_manual_ip(manual_ip(config[CONF_MANUAL_IP]))) - - # Add compile-time define for PHY types with specific code - if phy_define := _PHY_TYPE_TO_DEFINE.get(config[CONF_TYPE]): - cg.add_define(phy_define) - - if mac_address := config.get(CONF_MAC_ADDRESS): - cg.add(var.set_fixed_mac(mac_address.parts)) - - cg.add_define("USE_ETHERNET") - # Disable WiFi when using Ethernet to save memory add_idf_sdkconfig_option("CONFIG_ESP_WIFI_ENABLED", False) # Also disable WiFi/BT coexistence since WiFi is disabled add_idf_sdkconfig_option("CONFIG_SW_COEXIST_ENABLE", False) - if config[CONF_TYPE] == "LAN8670": - # Add LAN867x 10BASE-T1S PHY support component - add_idf_component(name="espressif/lan867x", ref="2.0.0") + # Re-enable ESP-IDF's Ethernet driver (excluded by default to save compile time) + include_builtin_idf_component("esp_eth") - if CORE.using_arduino: - cg.add_library("WiFi", None) + if config[CONF_TYPE] in _ALWAYS_EXTERNAL_IDF_COMPONENTS: + component = _IDF6_ETHERNET_COMPONENTS[config[CONF_TYPE]] + add_idf_component(name=component.name, ref=component.version) + elif idf_version() >= cv.Version(6, 0, 0) and ( + # IDF 6.0 moved per-chip PHY/MAC drivers to the Espressif Component Registry + component := _IDF6_ETHERNET_COMPONENTS.get(config[CONF_TYPE]) + ): + add_idf_component(name=component.name, ref=component.version) - CORE.add_job(final_step) + +async def _to_code_rp2040(var: cg.Pvariable, config: ConfigType) -> None: + cg.add(var.set_clk_pin(config[CONF_CLK_PIN])) + cg.add(var.set_miso_pin(config[CONF_MISO_PIN])) + cg.add(var.set_mosi_pin(config[CONF_MOSI_PIN])) + cg.add(var.set_cs_pin(config[CONF_CS_PIN])) + if CONF_INTERRUPT_PIN in config: + cg.add(var.set_interrupt_pin(config[CONF_INTERRUPT_PIN])) + if CONF_RESET_PIN in config: + cg.add(var.set_reset_pin(config[CONF_RESET_PIN])) + + cg.add_define("USE_ETHERNET_SPI") + cg.add_library(_RP2040_SPI_LIBRARIES[config[CONF_TYPE]], None) def _final_validate_rmii_pins(config: ConfigType) -> None: """Validate that RMII pins are not used by other components.""" + if not CORE.is_esp32: + return # RMII validation is ESP32-only # Only validate for RMII-based PHYs on ESP32/ESP32P4 if config[CONF_TYPE] in SPI_ETHERNET_TYPES or config[CONF_TYPE] == "OPENETH": return # SPI and OPENETH don't use RMII + from esphome.components.esp32 import ( + VARIANT_ESP32, + VARIANT_ESP32P4, + get_esp32_variant, + ) + variant = get_esp32_variant() if variant == VARIANT_ESP32: rmii_pins = ESP32_RMII_FIXED_PINS @@ -510,3 +679,39 @@ async def final_step(): if ip_state_count := CORE.data.get(ETHERNET_IP_STATE_LISTENERS_KEY, 0): cg.add_define("USE_ETHERNET_IP_STATE_LISTENERS") cg.add_define("ESPHOME_ETHERNET_IP_STATE_LISTENERS", ip_state_count) + + +_platform_filter = filter_source_files_from_platform( + { + "ethernet_component_esp32.cpp": { + PlatformFramework.ESP32_IDF, + PlatformFramework.ESP32_ARDUINO, + }, + "ethernet_component_rp2040.cpp": {PlatformFramework.RP2040_ARDUINO}, + "esp_eth_phy_jl1101.c": { + PlatformFramework.ESP32_IDF, + PlatformFramework.ESP32_ARDUINO, + }, + } +) + + +def _filter_source_files() -> list[str]: + excluded = _platform_filter() + eth_data = CORE.data.get(KEY_ETHERNET, {}) + eth_type = eth_data.get(ETHERNET_TYPE_KEY) + # Only compile the custom JL1101 driver when JL1101 is configured + # and pioarduino doesn't have it builtin (IDF 5.4.2 to 5.x) + if eth_type != "JL1101": + excluded.append("esp_eth_phy_jl1101.c") + elif CORE.is_esp32 and not CORE.data.get(KEY_NATIVE_IDF, False): + from esphome.components.esp32 import idf_version + + # pioarduino has JL1101 builtin on IDF 5.4.2-5.x; exclude custom driver + # to avoid shadowing. Native IDF builds always need the custom driver. + if cv.Version(5, 4, 2) <= idf_version() < cv.Version(6, 0, 0): + excluded.append("esp_eth_phy_jl1101.c") + return excluded + + +FILTER_SOURCE_FILES = _filter_source_files diff --git a/esphome/components/ethernet/esp_eth_phy_jl1101.c b/esphome/components/ethernet/esp_eth_phy_jl1101.c index 5e73e99101..46671a2dd0 100644 --- a/esphome/components/ethernet/esp_eth_phy_jl1101.c +++ b/esphome/components/ethernet/esp_eth_phy_jl1101.c @@ -12,6 +12,8 @@ // See the License for the specific language governing permissions and // limitations under the License. +#include "esphome/core/defines.h" + #ifdef USE_ESP32 #include @@ -27,7 +29,8 @@ #include "esp_rom_sys.h" #include "esp_idf_version.h" -#if defined(USE_ARDUINO) || ESP_IDF_VERSION < ESP_IDF_VERSION_VAL(5, 4, 2) +#if defined(USE_ETHERNET_JL1101) && (ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(6, 0, 0) || \ + ESP_IDF_VERSION < ESP_IDF_VERSION_VAL(5, 4, 2) || !defined(PLATFORMIO)) static const char *TAG = "jl1101"; #define PHY_CHECK(a, str, goto_tag, ...) \ diff --git a/esphome/components/ethernet/ethernet_component.cpp b/esphome/components/ethernet/ethernet_component.cpp index 208a919a0f..42cb0b3cfc 100644 --- a/esphome/components/ethernet/ethernet_component.cpp +++ b/esphome/components/ethernet/ethernet_component.cpp @@ -1,535 +1,22 @@ #include "ethernet_component.h" -#include "esphome/core/application.h" -#include "esphome/core/helpers.h" + +#ifdef USE_ETHERNET + #include "esphome/core/log.h" -#include "esphome/core/util.h" -#ifdef USE_ESP32 - -#include -#include -#include "esp_event.h" - -#ifdef USE_ETHERNET_LAN8670 -#include "esp_eth_phy_lan867x.h" -#endif - -#ifdef USE_ETHERNET_SPI -#include -#include -#endif - -namespace esphome { -namespace ethernet { - -#if ESP_IDF_VERSION < ESP_IDF_VERSION_VAL(5, 4, 2) -// work around IDF compile issue on P4 https://github.com/espressif/esp-idf/pull/15637 -#ifdef USE_ESP32_VARIANT_ESP32P4 -#undef ETH_ESP32_EMAC_DEFAULT_CONFIG -#define ETH_ESP32_EMAC_DEFAULT_CONFIG() \ - { \ - .smi_gpio = {.mdc_num = 31, .mdio_num = 52}, .interface = EMAC_DATA_INTERFACE_RMII, \ - .clock_config = {.rmii = {.clock_mode = EMAC_CLK_EXT_IN, .clock_gpio = (emac_rmii_clock_gpio_t) 50}}, \ - .dma_burst_len = ETH_DMA_BURST_LEN_32, .intr_priority = 0, \ - .emac_dataif_gpio = \ - {.rmii = {.tx_en_num = 49, .txd0_num = 34, .txd1_num = 35, .crs_dv_num = 28, .rxd0_num = 29, .rxd1_num = 30}}, \ - .clock_config_out_in = {.rmii = {.clock_mode = EMAC_CLK_EXT_IN, .clock_gpio = (emac_rmii_clock_gpio_t) -1}}, \ - } -#endif -#endif - -static const char *const TAG = "ethernet"; - -// PHY register size for hex logging -static constexpr size_t PHY_REG_SIZE = 2; +namespace esphome::ethernet { EthernetComponent *global_eth_component; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) -void EthernetComponent::log_error_and_mark_failed_(esp_err_t err, const char *message) { - ESP_LOGE(TAG, "%s: (%d) %s", message, err, esp_err_to_name(err)); - this->mark_failed(); -} - -#define ESPHL_ERROR_CHECK(err, message) \ - if ((err) != ESP_OK) { \ - this->log_error_and_mark_failed_(err, message); \ - return; \ - } - -#define ESPHL_ERROR_CHECK_RET(err, message, ret) \ - if ((err) != ESP_OK) { \ - this->log_error_and_mark_failed_(err, message); \ - return ret; \ - } - EthernetComponent::EthernetComponent() { global_eth_component = this; } -void EthernetComponent::setup() { - if (esp_reset_reason() != ESP_RST_DEEPSLEEP) { - // Delay here to allow power to stabilise before Ethernet is initialized. - delay(300); // NOLINT - } - - esp_err_t err; - -#ifdef USE_ETHERNET_SPI - // Install GPIO ISR handler to be able to service SPI Eth modules interrupts - gpio_install_isr_service(0); - - spi_bus_config_t buscfg = { - .mosi_io_num = this->mosi_pin_, - .miso_io_num = this->miso_pin_, - .sclk_io_num = this->clk_pin_, - .quadwp_io_num = -1, - .quadhd_io_num = -1, - .data4_io_num = -1, - .data5_io_num = -1, - .data6_io_num = -1, - .data7_io_num = -1, - .max_transfer_sz = 0, - .flags = 0, - .intr_flags = 0, - }; - - auto host = this->interface_; - - err = spi_bus_initialize(host, &buscfg, SPI_DMA_CH_AUTO); - ESPHL_ERROR_CHECK(err, "SPI bus initialize error"); -#endif - - err = esp_netif_init(); - ESPHL_ERROR_CHECK(err, "ETH netif init error"); - err = esp_event_loop_create_default(); - ESPHL_ERROR_CHECK(err, "ETH event loop error"); - - esp_netif_config_t cfg = ESP_NETIF_DEFAULT_ETH(); - this->eth_netif_ = esp_netif_new(&cfg); - - // Init MAC and PHY configs to default - eth_phy_config_t phy_config = ETH_PHY_DEFAULT_CONFIG(); - eth_mac_config_t mac_config = ETH_MAC_DEFAULT_CONFIG(); - -#ifdef USE_ETHERNET_SPI // Configure SPI interface and Ethernet driver for specific SPI module - spi_device_interface_config_t devcfg = { - .command_bits = 0, - .address_bits = 0, - .dummy_bits = 0, - .mode = 0, - .duty_cycle_pos = 0, - .cs_ena_pretrans = 0, - .cs_ena_posttrans = 0, - .clock_speed_hz = this->clock_speed_, - .input_delay_ns = 0, - .spics_io_num = this->cs_pin_, - .flags = 0, - .queue_size = 20, - .pre_cb = nullptr, - .post_cb = nullptr, - }; - -#if CONFIG_ETH_SPI_ETHERNET_W5500 - eth_w5500_config_t w5500_config = ETH_W5500_DEFAULT_CONFIG(host, &devcfg); -#endif -#if CONFIG_ETH_SPI_ETHERNET_DM9051 - eth_dm9051_config_t dm9051_config = ETH_DM9051_DEFAULT_CONFIG(host, &devcfg); -#endif - -#if CONFIG_ETH_SPI_ETHERNET_W5500 - w5500_config.int_gpio_num = this->interrupt_pin_; -#ifdef USE_ETHERNET_SPI_POLLING_SUPPORT - w5500_config.poll_period_ms = this->polling_interval_; -#endif -#endif - -#if CONFIG_ETH_SPI_ETHERNET_DM9051 - dm9051_config.int_gpio_num = this->interrupt_pin_; -#ifdef USE_ETHERNET_SPI_POLLING_SUPPORT - dm9051_config.poll_period_ms = this->polling_interval_; -#endif -#endif - - phy_config.phy_addr = this->phy_addr_spi_; - phy_config.reset_gpio_num = this->reset_pin_; - - esp_eth_mac_t *mac = nullptr; -#elif defined(USE_ETHERNET_OPENETH) - esp_eth_mac_t *mac = esp_eth_mac_new_openeth(&mac_config); -#else - phy_config.phy_addr = this->phy_addr_; - phy_config.reset_gpio_num = this->power_pin_; - - eth_esp32_emac_config_t esp32_emac_config = ETH_ESP32_EMAC_DEFAULT_CONFIG(); -#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 3, 0) - esp32_emac_config.smi_gpio.mdc_num = this->mdc_pin_; - esp32_emac_config.smi_gpio.mdio_num = this->mdio_pin_; -#else - esp32_emac_config.smi_mdc_gpio_num = this->mdc_pin_; - esp32_emac_config.smi_mdio_gpio_num = this->mdio_pin_; -#endif - esp32_emac_config.clock_config.rmii.clock_mode = this->clk_mode_; - esp32_emac_config.clock_config.rmii.clock_gpio = (emac_rmii_clock_gpio_t) this->clk_pin_; - - esp_eth_mac_t *mac = esp_eth_mac_new_esp32(&esp32_emac_config, &mac_config); -#endif - - switch (this->type_) { -#ifdef USE_ETHERNET_OPENETH - case ETHERNET_TYPE_OPENETH: { - phy_config.autonego_timeout_ms = 1000; - this->phy_ = esp_eth_phy_new_dp83848(&phy_config); - break; - } -#endif -#if CONFIG_ETH_USE_ESP32_EMAC - case ETHERNET_TYPE_LAN8720: { - this->phy_ = esp_eth_phy_new_lan87xx(&phy_config); - break; - } - case ETHERNET_TYPE_RTL8201: { - this->phy_ = esp_eth_phy_new_rtl8201(&phy_config); - break; - } - case ETHERNET_TYPE_DP83848: { - this->phy_ = esp_eth_phy_new_dp83848(&phy_config); - break; - } - case ETHERNET_TYPE_IP101: { - this->phy_ = esp_eth_phy_new_ip101(&phy_config); - break; - } - case ETHERNET_TYPE_JL1101: { - this->phy_ = esp_eth_phy_new_jl1101(&phy_config); - break; - } - case ETHERNET_TYPE_KSZ8081: - case ETHERNET_TYPE_KSZ8081RNA: { - this->phy_ = esp_eth_phy_new_ksz80xx(&phy_config); - break; - } -#ifdef USE_ETHERNET_LAN8670 - case ETHERNET_TYPE_LAN8670: { - this->phy_ = esp_eth_phy_new_lan867x(&phy_config); - break; - } -#endif -#endif -#ifdef USE_ETHERNET_SPI -#if CONFIG_ETH_SPI_ETHERNET_W5500 - case ETHERNET_TYPE_W5500: { - mac = esp_eth_mac_new_w5500(&w5500_config, &mac_config); - this->phy_ = esp_eth_phy_new_w5500(&phy_config); - break; - } -#endif -#if CONFIG_ETH_SPI_ETHERNET_DM9051 - case ETHERNET_TYPE_DM9051: { - mac = esp_eth_mac_new_dm9051(&dm9051_config, &mac_config); - this->phy_ = esp_eth_phy_new_dm9051(&phy_config); - break; - } -#endif -#endif - default: { - this->mark_failed(); - return; - } - } - - esp_eth_config_t eth_config = ETH_DEFAULT_CONFIG(mac, this->phy_); - this->eth_handle_ = nullptr; - err = esp_eth_driver_install(ð_config, &this->eth_handle_); - ESPHL_ERROR_CHECK(err, "ETH driver install error"); - -#ifndef USE_ETHERNET_SPI -#ifdef USE_ETHERNET_KSZ8081 - if (this->type_ == ETHERNET_TYPE_KSZ8081RNA && this->clk_mode_ == EMAC_CLK_OUT) { - // KSZ8081RNA default is incorrect. It expects a 25MHz clock instead of the 50MHz we provide. - this->ksz8081_set_clock_reference_(mac); - } -#endif // USE_ETHERNET_KSZ8081 - - for (const auto &phy_register : this->phy_registers_) { - this->write_phy_register_(mac, phy_register); - } -#endif - - // use ESP internal eth mac - uint8_t mac_addr[6]; - if (this->fixed_mac_.has_value()) { - memcpy(mac_addr, this->fixed_mac_->data(), 6); - } else { - esp_read_mac(mac_addr, ESP_MAC_ETH); - } - err = esp_eth_ioctl(this->eth_handle_, ETH_CMD_S_MAC_ADDR, mac_addr); - ESPHL_ERROR_CHECK(err, "set mac address error"); - - /* attach Ethernet driver to TCP/IP stack */ - err = esp_netif_attach(this->eth_netif_, esp_eth_new_netif_glue(this->eth_handle_)); - ESPHL_ERROR_CHECK(err, "ETH netif attach error"); - - // Register user defined event handers - err = esp_event_handler_register(ETH_EVENT, ESP_EVENT_ANY_ID, &EthernetComponent::eth_event_handler, nullptr); - ESPHL_ERROR_CHECK(err, "ETH event handler register error"); - err = esp_event_handler_register(IP_EVENT, IP_EVENT_ETH_GOT_IP, &EthernetComponent::got_ip_event_handler, nullptr); - ESPHL_ERROR_CHECK(err, "GOT IP event handler register error"); -#if USE_NETWORK_IPV6 - err = esp_event_handler_register(IP_EVENT, IP_EVENT_GOT_IP6, &EthernetComponent::got_ip6_event_handler, nullptr); - ESPHL_ERROR_CHECK(err, "GOT IPv6 event handler register error"); -#endif /* USE_NETWORK_IPV6 */ - - /* start Ethernet driver state machine */ - err = esp_eth_start(this->eth_handle_); - ESPHL_ERROR_CHECK(err, "ETH start error"); -} - -void EthernetComponent::loop() { - const uint32_t now = App.get_loop_component_start_time(); - - switch (this->state_) { - case EthernetComponentState::STOPPED: - if (this->started_) { - ESP_LOGI(TAG, "Starting connection"); - this->state_ = EthernetComponentState::CONNECTING; - this->start_connect_(); - } - break; - case EthernetComponentState::CONNECTING: - if (!this->started_) { - ESP_LOGI(TAG, "Stopped connection"); - this->state_ = EthernetComponentState::STOPPED; - } else if (this->connected_) { - // connection established - ESP_LOGI(TAG, "Connected"); - this->state_ = EthernetComponentState::CONNECTED; - - this->dump_connect_params_(); - this->status_clear_warning(); - } else if (now - this->connect_begin_ > 15000) { - ESP_LOGW(TAG, "Connecting failed; reconnecting"); - this->start_connect_(); - } - break; - case EthernetComponentState::CONNECTED: - if (!this->started_) { - ESP_LOGI(TAG, "Stopped connection"); - this->state_ = EthernetComponentState::STOPPED; - } else if (!this->connected_) { - ESP_LOGW(TAG, "Connection lost; reconnecting"); - this->state_ = EthernetComponentState::CONNECTING; - this->start_connect_(); - } else { - this->finish_connect_(); - // When connected and stable, disable the loop to save CPU cycles - this->disable_loop(); - } - break; - } -} - -void EthernetComponent::dump_config() { - const char *eth_type; - switch (this->type_) { - case ETHERNET_TYPE_LAN8720: - eth_type = "LAN8720"; - break; - - case ETHERNET_TYPE_RTL8201: - eth_type = "RTL8201"; - break; - - case ETHERNET_TYPE_DP83848: - eth_type = "DP83848"; - break; - - case ETHERNET_TYPE_IP101: - eth_type = "IP101"; - break; - - case ETHERNET_TYPE_JL1101: - eth_type = "JL1101"; - break; - - case ETHERNET_TYPE_KSZ8081: - eth_type = "KSZ8081"; - break; - - case ETHERNET_TYPE_KSZ8081RNA: - eth_type = "KSZ8081RNA"; - break; - - case ETHERNET_TYPE_W5500: - eth_type = "W5500"; - break; - - case ETHERNET_TYPE_OPENETH: - eth_type = "OPENETH"; - break; - - case ETHERNET_TYPE_DM9051: - eth_type = "DM9051"; - break; - -#ifdef USE_ETHERNET_LAN8670 - case ETHERNET_TYPE_LAN8670: - eth_type = "LAN8670"; - break; -#endif - - default: - eth_type = "Unknown"; - break; - } - - ESP_LOGCONFIG(TAG, - "Ethernet:\n" - " Connected: %s", - YESNO(this->is_connected())); - this->dump_connect_params_(); -#ifdef USE_ETHERNET_SPI - ESP_LOGCONFIG(TAG, - " CLK Pin: %u\n" - " MISO Pin: %u\n" - " MOSI Pin: %u\n" - " CS Pin: %u", - this->clk_pin_, this->miso_pin_, this->mosi_pin_, this->cs_pin_); - { - constexpr std::array values{"spi1", "spi2", "spi3"}; - char const *const value{this->interface_ < values.size() ? values[this->interface_] : nullptr}; - if (value) { - ESP_LOGCONFIG(TAG, " Interface: %s", value); - } - } -#ifdef USE_ETHERNET_SPI_POLLING_SUPPORT - if (this->polling_interval_ != 0) { - ESP_LOGCONFIG(TAG, " Polling Interval: %lu ms", this->polling_interval_); - } else -#endif - { - ESP_LOGCONFIG(TAG, " IRQ Pin: %d", this->interrupt_pin_); - } - ESP_LOGCONFIG(TAG, - " Reset Pin: %d\n" - " Clock Speed: %d MHz", - this->reset_pin_, this->clock_speed_ / 1000000); -#else - if (this->power_pin_ != -1) { - ESP_LOGCONFIG(TAG, " Power Pin: %u", this->power_pin_); - } - ESP_LOGCONFIG(TAG, - " CLK Pin: %u\n" - " MDC Pin: %u\n" - " MDIO Pin: %u\n" - " PHY addr: %u", - this->clk_pin_, this->mdc_pin_, this->mdio_pin_, this->phy_addr_); -#endif - ESP_LOGCONFIG(TAG, " Type: %s", eth_type); -} - float EthernetComponent::get_setup_priority() const { return setup_priority::WIFI; } -network::IPAddresses EthernetComponent::get_ip_addresses() { - network::IPAddresses addresses; - esp_netif_ip_info_t ip; - esp_err_t err = esp_netif_get_ip_info(this->eth_netif_, &ip); - if (err != ESP_OK) { - ESP_LOGV(TAG, "esp_netif_get_ip_info failed: %s", esp_err_to_name(err)); - // TODO: do something smarter - // return false; - } else { - addresses[0] = network::IPAddress(&ip.ip); - } -#if USE_NETWORK_IPV6 - struct esp_ip6_addr if_ip6s[CONFIG_LWIP_IPV6_NUM_ADDRESSES]; - uint8_t count = 0; - count = esp_netif_get_all_ip6(this->eth_netif_, if_ip6s); - assert(count <= CONFIG_LWIP_IPV6_NUM_ADDRESSES); - for (int i = 0; i < count; i++) { - addresses[i + 1] = network::IPAddress(&if_ip6s[i]); - } -#endif /* USE_NETWORK_IPV6 */ +void EthernetComponent::set_type(EthernetType type) { this->type_ = type; } - return addresses; -} - -network::IPAddress EthernetComponent::get_dns_address(uint8_t num) { - LwIPLock lock; - const ip_addr_t *dns_ip = dns_getserver(num); - return dns_ip; -} - -void EthernetComponent::eth_event_handler(void *arg, esp_event_base_t event_base, int32_t event, void *event_data) { - const char *event_name; - - switch (event) { - case ETHERNET_EVENT_START: - event_name = "ETH started"; - global_eth_component->started_ = true; - global_eth_component->enable_loop_soon_any_context(); - break; - case ETHERNET_EVENT_STOP: - event_name = "ETH stopped"; - global_eth_component->started_ = false; - global_eth_component->connected_ = false; - global_eth_component->enable_loop_soon_any_context(); // Enable loop when connection state changes - break; - case ETHERNET_EVENT_CONNECTED: - event_name = "ETH connected"; - // For static IP configurations, GOT_IP event may not fire, so notify IP listeners here -#if defined(USE_ETHERNET_IP_STATE_LISTENERS) && defined(USE_ETHERNET_MANUAL_IP) - if (global_eth_component->manual_ip_.has_value()) { - global_eth_component->notify_ip_state_listeners_(); - } +#ifdef USE_ETHERNET_MANUAL_IP +void EthernetComponent::set_manual_ip(const ManualIP &manual_ip) { this->manual_ip_ = manual_ip; } #endif - break; - case ETHERNET_EVENT_DISCONNECTED: - event_name = "ETH disconnected"; - global_eth_component->connected_ = false; - global_eth_component->enable_loop_soon_any_context(); // Enable loop when connection state changes - break; - default: - return; - } - - ESP_LOGV(TAG, "[Ethernet event] %s (num=%" PRId32 ")", event_name, event); -} - -void EthernetComponent::got_ip_event_handler(void *arg, esp_event_base_t event_base, int32_t event_id, - void *event_data) { - ip_event_got_ip_t *event = (ip_event_got_ip_t *) event_data; - const esp_netif_ip_info_t *ip_info = &event->ip_info; - ESP_LOGV(TAG, "[Ethernet event] ETH Got IP " IPSTR, IP2STR(&ip_info->ip)); - global_eth_component->got_ipv4_address_ = true; -#if USE_NETWORK_IPV6 && (USE_NETWORK_MIN_IPV6_ADDR_COUNT > 0) - global_eth_component->connected_ = global_eth_component->ipv6_count_ >= USE_NETWORK_MIN_IPV6_ADDR_COUNT; - global_eth_component->enable_loop_soon_any_context(); // Enable loop when connection state changes -#else - global_eth_component->connected_ = true; - global_eth_component->enable_loop_soon_any_context(); // Enable loop when connection state changes -#endif /* USE_NETWORK_IPV6 */ -#ifdef USE_ETHERNET_IP_STATE_LISTENERS - global_eth_component->notify_ip_state_listeners_(); -#endif -} - -#if USE_NETWORK_IPV6 -void EthernetComponent::got_ip6_event_handler(void *arg, esp_event_base_t event_base, int32_t event_id, - void *event_data) { - ip_event_got_ip6_t *event = (ip_event_got_ip6_t *) event_data; - ESP_LOGV(TAG, "[Ethernet event] ETH Got IPv6: " IPV6STR, IPV62STR(event->ip6_info.ip)); - global_eth_component->ipv6_count_ += 1; -#if (USE_NETWORK_MIN_IPV6_ADDR_COUNT > 0) - global_eth_component->connected_ = - global_eth_component->got_ipv4_address_ && (global_eth_component->ipv6_count_ >= USE_NETWORK_MIN_IPV6_ADDR_COUNT); - global_eth_component->enable_loop_soon_any_context(); // Enable loop when connection state changes -#else - global_eth_component->connected_ = global_eth_component->got_ipv4_address_; - global_eth_component->enable_loop_soon_any_context(); // Enable loop when connection state changes -#endif -#ifdef USE_ETHERNET_IP_STATE_LISTENERS - global_eth_component->notify_ip_state_listeners_(); -#endif -} -#endif /* USE_NETWORK_IPV6 */ #ifdef USE_ETHERNET_IP_STATE_LISTENERS void EthernetComponent::notify_ip_state_listeners_() { @@ -542,320 +29,6 @@ void EthernetComponent::notify_ip_state_listeners_() { } #endif // USE_ETHERNET_IP_STATE_LISTENERS -void EthernetComponent::finish_connect_() { -#if USE_NETWORK_IPV6 - // Retry IPv6 link-local setup if it failed during initial connect - // This handles the case where min_ipv6_addr_count is NOT set (or is 0), - // allowing us to reach CONNECTED state with just IPv4. - // If IPv6 setup failed in start_connect_() because the interface wasn't ready: - // - Bootup timing issues (#10281) - // - Cable unplugged/network interruption (#10705) - // We can now retry since we're in CONNECTED state and the interface is definitely up. - if (!this->ipv6_setup_done_) { - esp_err_t err = esp_netif_create_ip6_linklocal(this->eth_netif_); - if (err == ESP_OK) { - ESP_LOGD(TAG, "IPv6 link-local address created (retry succeeded)"); - } - // Always set the flag to prevent continuous retries - // If IPv6 setup fails here with the interface up and stable, it's - // likely a persistent issue (IPv6 disabled at router, hardware - // limitation, etc.) that won't be resolved by further retries. - // The device continues to work with IPv4. - this->ipv6_setup_done_ = true; - } -#endif /* USE_NETWORK_IPV6 */ -} +} // namespace esphome::ethernet -void EthernetComponent::start_connect_() { - global_eth_component->got_ipv4_address_ = false; -#if USE_NETWORK_IPV6 - global_eth_component->ipv6_count_ = 0; - this->ipv6_setup_done_ = false; -#endif /* USE_NETWORK_IPV6 */ - this->connect_begin_ = millis(); - this->status_set_warning(LOG_STR("waiting for IP configuration")); - - esp_err_t err; - err = esp_netif_set_hostname(this->eth_netif_, App.get_name().c_str()); - if (err != ERR_OK) { - ESP_LOGW(TAG, "esp_netif_set_hostname failed: %s", esp_err_to_name(err)); - } - - esp_netif_ip_info_t info; -#ifdef USE_ETHERNET_MANUAL_IP - if (this->manual_ip_.has_value()) { - info.ip = this->manual_ip_->static_ip; - info.gw = this->manual_ip_->gateway; - info.netmask = this->manual_ip_->subnet; - } else -#endif - { - info.ip.addr = 0; - info.gw.addr = 0; - info.netmask.addr = 0; - } - - esp_netif_dhcp_status_t status = ESP_NETIF_DHCP_INIT; - - err = esp_netif_dhcpc_get_status(this->eth_netif_, &status); - ESPHL_ERROR_CHECK(err, "DHCPC Get Status Failed!"); - - ESP_LOGV(TAG, "DHCP Client Status: %d", status); - - err = esp_netif_dhcpc_stop(this->eth_netif_); - if (err != ESP_ERR_ESP_NETIF_DHCP_ALREADY_STOPPED) { - ESPHL_ERROR_CHECK(err, "DHCPC stop error"); - } - - err = esp_netif_set_ip_info(this->eth_netif_, &info); - ESPHL_ERROR_CHECK(err, "DHCPC set IP info error"); - -#ifdef USE_ETHERNET_MANUAL_IP - if (this->manual_ip_.has_value()) { - LwIPLock lock; - if (this->manual_ip_->dns1.is_set()) { - ip_addr_t d; - d = this->manual_ip_->dns1; - dns_setserver(0, &d); - } - if (this->manual_ip_->dns2.is_set()) { - ip_addr_t d; - d = this->manual_ip_->dns2; - dns_setserver(1, &d); - } - } else -#endif - { - err = esp_netif_dhcpc_start(this->eth_netif_); - if (err != ESP_ERR_ESP_NETIF_DHCP_ALREADY_STARTED) { - ESPHL_ERROR_CHECK(err, "DHCPC start error"); - } - } -#if USE_NETWORK_IPV6 - // Attempt to create IPv6 link-local address - // We MUST attempt this here, not just in finish_connect_(), because with - // min_ipv6_addr_count set, the component won't reach CONNECTED state without IPv6. - // However, this may fail with ESP_FAIL if the interface is not up yet: - // - At bootup when link isn't ready (#10281) - // - After disconnection/cable unplugged (#10705) - // We'll retry in finish_connect_() if it fails here. - err = esp_netif_create_ip6_linklocal(this->eth_netif_); - if (err != ESP_OK) { - if (err == ESP_ERR_ESP_NETIF_INVALID_PARAMS) { - // This is a programming error, not a transient failure - ESPHL_ERROR_CHECK(err, "esp_netif_create_ip6_linklocal invalid parameters"); - } else { - // ESP_FAIL means the interface isn't up yet - // This is expected and non-fatal, happens in multiple scenarios: - // - During reconnection after network interruptions (#10705) - // - At bootup when the link isn't ready yet (#10281) - // We'll retry once we reach CONNECTED state and the interface is up - ESP_LOGW(TAG, "esp_netif_create_ip6_linklocal failed: %s", esp_err_to_name(err)); - // Don't mark component as failed - this is a transient error - } - } -#endif /* USE_NETWORK_IPV6 */ - - this->connect_begin_ = millis(); - this->status_set_warning(); -} - -bool EthernetComponent::is_connected() { return this->state_ == EthernetComponentState::CONNECTED; } - -void EthernetComponent::dump_connect_params_() { - esp_netif_ip_info_t ip; - esp_netif_get_ip_info(this->eth_netif_, &ip); - const ip_addr_t *dns_ip1; - const ip_addr_t *dns_ip2; - { - LwIPLock lock; - dns_ip1 = dns_getserver(0); - dns_ip2 = dns_getserver(1); - } - - // Use stack buffers for IP address formatting to avoid heap allocations - char ip_buf[network::IP_ADDRESS_BUFFER_SIZE]; - char subnet_buf[network::IP_ADDRESS_BUFFER_SIZE]; - char gateway_buf[network::IP_ADDRESS_BUFFER_SIZE]; - char dns1_buf[network::IP_ADDRESS_BUFFER_SIZE]; - char dns2_buf[network::IP_ADDRESS_BUFFER_SIZE]; - ESP_LOGCONFIG(TAG, - " IP Address: %s\n" - " Hostname: '%s'\n" - " Subnet: %s\n" - " Gateway: %s\n" - " DNS1: %s\n" - " DNS2: %s", - network::IPAddress(&ip.ip).str_to(ip_buf), App.get_name().c_str(), - network::IPAddress(&ip.netmask).str_to(subnet_buf), network::IPAddress(&ip.gw).str_to(gateway_buf), - network::IPAddress(dns_ip1).str_to(dns1_buf), network::IPAddress(dns_ip2).str_to(dns2_buf)); - -#if USE_NETWORK_IPV6 - struct esp_ip6_addr if_ip6s[CONFIG_LWIP_IPV6_NUM_ADDRESSES]; - uint8_t count = 0; - count = esp_netif_get_all_ip6(this->eth_netif_, if_ip6s); - assert(count <= CONFIG_LWIP_IPV6_NUM_ADDRESSES); - for (int i = 0; i < count; i++) { - ESP_LOGCONFIG(TAG, " IPv6: " IPV6STR, IPV62STR(if_ip6s[i])); - } -#endif /* USE_NETWORK_IPV6 */ - - char mac_buf[MAC_ADDRESS_PRETTY_BUFFER_SIZE]; - ESP_LOGCONFIG(TAG, - " MAC Address: %s\n" - " Is Full Duplex: %s\n" - " Link Speed: %u", - this->get_eth_mac_address_pretty_into_buffer(mac_buf), - YESNO(this->get_duplex_mode() == ETH_DUPLEX_FULL), this->get_link_speed() == ETH_SPEED_100M ? 100 : 10); -} - -#ifdef USE_ETHERNET_SPI -void EthernetComponent::set_clk_pin(uint8_t clk_pin) { this->clk_pin_ = clk_pin; } -void EthernetComponent::set_miso_pin(uint8_t miso_pin) { this->miso_pin_ = miso_pin; } -void EthernetComponent::set_mosi_pin(uint8_t mosi_pin) { this->mosi_pin_ = mosi_pin; } -void EthernetComponent::set_cs_pin(uint8_t cs_pin) { this->cs_pin_ = cs_pin; } -void EthernetComponent::set_interrupt_pin(uint8_t interrupt_pin) { this->interrupt_pin_ = interrupt_pin; } -void EthernetComponent::set_reset_pin(uint8_t reset_pin) { this->reset_pin_ = reset_pin; } -void EthernetComponent::set_clock_speed(int clock_speed) { this->clock_speed_ = clock_speed; } -void EthernetComponent::set_interface(spi_host_device_t interface) { this->interface_ = interface; } -#ifdef USE_ETHERNET_SPI_POLLING_SUPPORT -void EthernetComponent::set_polling_interval(uint32_t polling_interval) { this->polling_interval_ = polling_interval; } -#endif -#else -void EthernetComponent::set_phy_addr(uint8_t phy_addr) { this->phy_addr_ = phy_addr; } -void EthernetComponent::set_power_pin(int power_pin) { this->power_pin_ = power_pin; } -void EthernetComponent::set_mdc_pin(uint8_t mdc_pin) { this->mdc_pin_ = mdc_pin; } -void EthernetComponent::set_mdio_pin(uint8_t mdio_pin) { this->mdio_pin_ = mdio_pin; } -void EthernetComponent::set_clk_pin(uint8_t clk_pin) { this->clk_pin_ = clk_pin; } -void EthernetComponent::set_clk_mode(emac_rmii_clock_mode_t clk_mode) { this->clk_mode_ = clk_mode; } -void EthernetComponent::add_phy_register(PHYRegister register_value) { this->phy_registers_.push_back(register_value); } -#endif -void EthernetComponent::set_type(EthernetType type) { this->type_ = type; } -#ifdef USE_ETHERNET_MANUAL_IP -void EthernetComponent::set_manual_ip(const ManualIP &manual_ip) { this->manual_ip_ = manual_ip; } -#endif - -// set_use_address() is guaranteed to be called during component setup by Python code generation, -// so use_address_ will always be valid when get_use_address() is called - no fallback needed. -const char *EthernetComponent::get_use_address() const { return this->use_address_; } - -void EthernetComponent::set_use_address(const char *use_address) { this->use_address_ = use_address; } - -void EthernetComponent::get_eth_mac_address_raw(uint8_t *mac) { - esp_err_t err; - err = esp_eth_ioctl(this->eth_handle_, ETH_CMD_G_MAC_ADDR, mac); - ESPHL_ERROR_CHECK(err, "ETH_CMD_G_MAC error"); -} - -std::string EthernetComponent::get_eth_mac_address_pretty() { - char buf[MAC_ADDRESS_PRETTY_BUFFER_SIZE]; - return std::string(this->get_eth_mac_address_pretty_into_buffer(buf)); -} - -const char *EthernetComponent::get_eth_mac_address_pretty_into_buffer( - std::span buf) { - uint8_t mac[6]; - get_eth_mac_address_raw(mac); - format_mac_addr_upper(mac, buf.data()); - return buf.data(); -} - -eth_duplex_t EthernetComponent::get_duplex_mode() { - esp_err_t err; - eth_duplex_t duplex_mode; - err = esp_eth_ioctl(this->eth_handle_, ETH_CMD_G_DUPLEX_MODE, &duplex_mode); - ESPHL_ERROR_CHECK_RET(err, "ETH_CMD_G_DUPLEX_MODE error", ETH_DUPLEX_HALF); - return duplex_mode; -} - -eth_speed_t EthernetComponent::get_link_speed() { - esp_err_t err; - eth_speed_t speed; - err = esp_eth_ioctl(this->eth_handle_, ETH_CMD_G_SPEED, &speed); - ESPHL_ERROR_CHECK_RET(err, "ETH_CMD_G_SPEED error", ETH_SPEED_10M); - return speed; -} - -bool EthernetComponent::powerdown() { - ESP_LOGI(TAG, "Powering down ethernet PHY"); - if (this->phy_ == nullptr) { - ESP_LOGE(TAG, "Ethernet PHY not assigned"); - return false; - } - this->connected_ = false; - this->started_ = false; - // No need to enable_loop() here as this is only called during shutdown/reboot - if (this->phy_->pwrctl(this->phy_, false) != ESP_OK) { - ESP_LOGE(TAG, "Error powering down ethernet PHY"); - return false; - } - return true; -} - -#ifndef USE_ETHERNET_SPI - -#ifdef USE_ETHERNET_KSZ8081 -constexpr uint8_t KSZ80XX_PC2R_REG_ADDR = 0x1F; - -void EthernetComponent::ksz8081_set_clock_reference_(esp_eth_mac_t *mac) { - esp_err_t err; - - uint32_t phy_control_2; - err = mac->read_phy_reg(mac, this->phy_addr_, KSZ80XX_PC2R_REG_ADDR, &(phy_control_2)); - ESPHL_ERROR_CHECK(err, "Read PHY Control 2 failed"); -#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERY_VERBOSE - char hex_buf[format_hex_pretty_size(PHY_REG_SIZE)]; -#endif - ESP_LOGVV(TAG, "KSZ8081 PHY Control 2: %s", format_hex_pretty_to(hex_buf, (uint8_t *) &phy_control_2, PHY_REG_SIZE)); - - /* - * Bit 7 is `RMII Reference Clock Select`. Default is `0`. - * KSZ8081RNA: - * 0 - clock input to XI (Pin 8) is 25 MHz for RMII - 25 MHz clock mode. - * 1 - clock input to XI (Pin 8) is 50 MHz for RMII - 50 MHz clock mode. - * KSZ8081RND: - * 0 - clock input to XI (Pin 8) is 50 MHz for RMII - 50 MHz clock mode. - * 1 - clock input to XI (Pin 8) is 25 MHz (driven clock only, not a crystal) for RMII - 25 MHz clock mode. - */ - if ((phy_control_2 & (1 << 7)) != (1 << 7)) { - phy_control_2 |= 1 << 7; - err = mac->write_phy_reg(mac, this->phy_addr_, KSZ80XX_PC2R_REG_ADDR, phy_control_2); - ESPHL_ERROR_CHECK(err, "Write PHY Control 2 failed"); - err = mac->read_phy_reg(mac, this->phy_addr_, KSZ80XX_PC2R_REG_ADDR, &(phy_control_2)); - ESPHL_ERROR_CHECK(err, "Read PHY Control 2 failed"); - ESP_LOGVV(TAG, "KSZ8081 PHY Control 2: %s", - format_hex_pretty_to(hex_buf, (uint8_t *) &phy_control_2, PHY_REG_SIZE)); - } -} -#endif // USE_ETHERNET_KSZ8081 - -void EthernetComponent::write_phy_register_(esp_eth_mac_t *mac, PHYRegister register_data) { - esp_err_t err; - constexpr uint8_t eth_phy_psr_reg_addr = 0x1F; - - if (this->type_ == ETHERNET_TYPE_RTL8201 && register_data.page) { - ESP_LOGD(TAG, "Select PHY Register Page: 0x%02" PRIX32, register_data.page); - err = mac->write_phy_reg(mac, this->phy_addr_, eth_phy_psr_reg_addr, register_data.page); - ESPHL_ERROR_CHECK(err, "Select PHY Register page failed"); - } - - ESP_LOGD(TAG, - "Writing to PHY Register Address: 0x%02" PRIX32 "\n" - "Writing to PHY Register Value: 0x%04" PRIX32, - register_data.address, register_data.value); - err = mac->write_phy_reg(mac, this->phy_addr_, register_data.address, register_data.value); - ESPHL_ERROR_CHECK(err, "Writing PHY Register failed"); - - if (this->type_ == ETHERNET_TYPE_RTL8201 && register_data.page) { - ESP_LOGD(TAG, "Select PHY Register Page 0x00"); - err = mac->write_phy_reg(mac, this->phy_addr_, eth_phy_psr_reg_addr, 0x0); - ESPHL_ERROR_CHECK(err, "Select PHY Register Page 0 failed"); - } -} - -#endif - -} // namespace ethernet -} // namespace esphome - -#endif // USE_ESP32 +#endif // USE_ETHERNET diff --git a/esphome/components/ethernet/ethernet_component.h b/esphome/components/ethernet/ethernet_component.h index 2e3631d546..72d308ad54 100644 --- a/esphome/components/ethernet/ethernet_component.h +++ b/esphome/components/ethernet/ethernet_component.h @@ -4,24 +4,40 @@ #include "esphome/core/defines.h" #include "esphome/core/hal.h" #include "esphome/core/helpers.h" +#include "esphome/core/automation.h" #include "esphome/components/network/ip_address.h" -#ifdef USE_ESP_IDF - -#include "driver/spi_master.h" - -#endif // USE_ESP_IDF +#ifdef USE_ETHERNET #ifdef USE_ESP32 - #include "esp_eth.h" +#ifdef USE_ETHERNET_SPI +#include "hal/spi_types.h" +#endif #include "esp_eth_mac.h" +#include "esp_eth_mac_esp.h" #include "esp_netif.h" #include "esp_mac.h" #include "esp_idf_version.h" -namespace esphome { -namespace ethernet { +#if CONFIG_ETH_USE_ESP32_EMAC +extern "C" eth_esp32_emac_config_t eth_esp32_emac_default_config(void); +#endif +#endif // USE_ESP32 + +#ifdef USE_RP2040 +#if defined(USE_ETHERNET_W5500) +#include +#elif defined(USE_ETHERNET_W5100) +#include +#elif defined(USE_ETHERNET_ENC28J60) +#include +#else +#error "Unsupported RP2040 SPI Ethernet type" +#endif +#endif + +namespace esphome::ethernet { #ifdef USE_ETHERNET_IP_STATE_LISTENERS /** Listener interface for Ethernet IP state changes. @@ -48,10 +64,12 @@ enum EthernetType : uint8_t { ETHERNET_TYPE_JL1101, ETHERNET_TYPE_KSZ8081, ETHERNET_TYPE_KSZ8081RNA, + ETHERNET_TYPE_W5100, ETHERNET_TYPE_W5500, ETHERNET_TYPE_OPENETH, ETHERNET_TYPE_DM9051, ETHERNET_TYPE_LAN8670, + ETHERNET_TYPE_ENC28J60, }; struct ManualIP { @@ -74,7 +92,13 @@ enum class EthernetComponentState : uint8_t { CONNECTED, }; -class EthernetComponent : public Component { +// Platform-neutral duplex/speed types +#ifndef USE_ESP32 +enum eth_duplex_t { ETH_DUPLEX_HALF, ETH_DUPLEX_FULL }; +enum eth_speed_t { ETH_SPEED_10M, ETH_SPEED_100M }; +#endif + +class EthernetComponent final : public Component { public: EthernetComponent(); void setup() override; @@ -82,7 +106,29 @@ class EthernetComponent : public Component { void dump_config() override; float get_setup_priority() const override; void on_powerdown() override { powerdown(); } - bool is_connected(); + bool is_connected() { return this->state_ == EthernetComponentState::CONNECTED; } + + void set_type(EthernetType type); +#ifdef USE_ETHERNET_MANUAL_IP + void set_manual_ip(const ManualIP &manual_ip); +#endif + void set_fixed_mac(const std::array &mac) { this->fixed_mac_ = mac; } + + network::IPAddresses get_ip_addresses(); + network::IPAddress get_dns_address(uint8_t num); + const char *get_use_address() const { return this->use_address_; } + void set_use_address(const char *use_address) { this->use_address_ = use_address; } + void get_eth_mac_address_raw(uint8_t *mac); + // Remove before 2026.9.0 + ESPDEPRECATED("Use get_eth_mac_address_pretty_into_buffer() instead. Removed in 2026.9.0", "2026.3.0") + std::string get_eth_mac_address_pretty(); + const char *get_eth_mac_address_pretty_into_buffer(std::span buf); + eth_duplex_t get_duplex_mode(); + eth_speed_t get_link_speed(); + bool powerdown(); + +#ifdef USE_ESP32 + esp_eth_handle_t get_eth_handle() const { return this->eth_handle_; } #ifdef USE_ETHERNET_SPI void set_clk_pin(uint8_t clk_pin); @@ -104,41 +150,44 @@ class EthernetComponent : public Component { void set_clk_pin(uint8_t clk_pin); void set_clk_mode(emac_rmii_clock_mode_t clk_mode); void add_phy_register(PHYRegister register_value); -#endif - void set_type(EthernetType type); -#ifdef USE_ETHERNET_MANUAL_IP - void set_manual_ip(const ManualIP &manual_ip); -#endif - void set_fixed_mac(const std::array &mac) { this->fixed_mac_ = mac; } +#endif // USE_ETHERNET_SPI +#endif // USE_ESP32 - network::IPAddresses get_ip_addresses(); - network::IPAddress get_dns_address(uint8_t num); - const char *get_use_address() const; - void set_use_address(const char *use_address); - void get_eth_mac_address_raw(uint8_t *mac); - std::string get_eth_mac_address_pretty(); - const char *get_eth_mac_address_pretty_into_buffer(std::span buf); - eth_duplex_t get_duplex_mode(); - eth_speed_t get_link_speed(); - bool powerdown(); +#ifdef USE_RP2040 + void set_clk_pin(uint8_t clk_pin); + void set_miso_pin(uint8_t miso_pin); + void set_mosi_pin(uint8_t mosi_pin); + void set_cs_pin(uint8_t cs_pin); + void set_interrupt_pin(int8_t interrupt_pin); + void set_reset_pin(int8_t reset_pin); +#endif // USE_RP2040 #ifdef USE_ETHERNET_IP_STATE_LISTENERS void add_ip_state_listener(EthernetIPStateListener *listener) { this->ip_state_listeners_.push_back(listener); } #endif +#ifdef USE_ETHERNET_CONNECT_TRIGGER + Trigger<> *get_connect_trigger() { return &this->connect_trigger_; } +#endif +#ifdef USE_ETHERNET_DISCONNECT_TRIGGER + Trigger<> *get_disconnect_trigger() { return &this->disconnect_trigger_; } +#endif + protected: + void start_connect_(); + void finish_connect_(); + void dump_connect_params_(); + +#ifdef USE_ETHERNET_IP_STATE_LISTENERS + void notify_ip_state_listeners_(); +#endif + +#ifdef USE_ESP32 static void eth_event_handler(void *arg, esp_event_base_t event_base, int32_t event_id, void *event_data); static void got_ip_event_handler(void *arg, esp_event_base_t event_base, int32_t event_id, void *event_data); #if LWIP_IPV6 static void got_ip6_event_handler(void *arg, esp_event_base_t event_base, int32_t event_id, void *event_data); #endif /* LWIP_IPV6 */ -#ifdef USE_ETHERNET_IP_STATE_LISTENERS - void notify_ip_state_listeners_(); -#endif - - void start_connect_(); - void finish_connect_(); - void dump_connect_params_(); void log_error_and_mark_failed_(esp_err_t err, const char *message); #ifdef USE_ETHERNET_KSZ8081 /// @brief Set `RMII Reference Clock Select` bit for KSZ8081. @@ -171,7 +220,40 @@ class EthernetComponent : public Component { uint8_t phy_addr_{0}; uint8_t mdc_pin_{23}; uint8_t mdio_pin_{18}; +#endif // USE_ETHERNET_SPI + + // ESP32 pointers + esp_netif_t *eth_netif_{nullptr}; + esp_eth_handle_t eth_handle_; + esp_eth_phy_t *phy_{nullptr}; +#endif // USE_ESP32 + +#ifdef USE_RP2040 + static constexpr uint32_t LINK_CHECK_INTERVAL = 500; // ms between link/IP polls +#if defined(USE_ETHERNET_W5100) + static constexpr uint32_t RESET_DELAY_MS = 150; // W5100S PLL lock time +#else + static constexpr uint32_t RESET_DELAY_MS = 10; #endif +#if defined(USE_ETHERNET_W5500) + Wiznet5500lwIP *eth_{nullptr}; +#elif defined(USE_ETHERNET_W5100) + Wiznet5100lwIP *eth_{nullptr}; +#elif defined(USE_ETHERNET_ENC28J60) + ENC28J60lwIP *eth_{nullptr}; +#else +#error "Unsupported RP2040 SPI Ethernet type" +#endif + uint32_t last_link_check_{0}; + uint8_t clk_pin_; + uint8_t miso_pin_; + uint8_t mosi_pin_; + uint8_t cs_pin_; + int8_t interrupt_pin_{-1}; + int8_t reset_pin_{-1}; +#endif // USE_RP2040 + + // Common members #ifdef USE_ETHERNET_MANUAL_IP optional manual_ip_{}; #endif @@ -188,16 +270,18 @@ class EthernetComponent : public Component { bool ipv6_setup_done_{false}; #endif /* LWIP_IPV6 */ - // Pointers at the end (naturally aligned) - esp_netif_t *eth_netif_{nullptr}; - esp_eth_handle_t eth_handle_; - esp_eth_phy_t *phy_{nullptr}; optional> fixed_mac_; #ifdef USE_ETHERNET_IP_STATE_LISTENERS StaticVector ip_state_listeners_; #endif +#ifdef USE_ETHERNET_CONNECT_TRIGGER + Trigger<> connect_trigger_; +#endif +#ifdef USE_ETHERNET_DISCONNECT_TRIGGER + Trigger<> disconnect_trigger_; +#endif private: // Stores a pointer to a string literal (static storage duration). // ONLY set from Python-generated code with string literals - never dynamic strings. @@ -207,11 +291,13 @@ class EthernetComponent : public Component { // NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables) extern EthernetComponent *global_eth_component; -#if ESP_IDF_VERSION < ESP_IDF_VERSION_VAL(5, 4, 2) +#ifdef USE_ESP32 +#if defined(USE_ETHERNET_JL1101) && (ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(6, 0, 0) || \ + ESP_IDF_VERSION < ESP_IDF_VERSION_VAL(5, 4, 2) || !defined(PLATFORMIO)) extern "C" esp_eth_phy_t *esp_eth_phy_new_jl1101(const eth_phy_config_t *config); #endif - -} // namespace ethernet -} // namespace esphome - #endif // USE_ESP32 + +} // namespace esphome::ethernet + +#endif // USE_ETHERNET diff --git a/esphome/components/ethernet/ethernet_component_esp32.cpp b/esphome/components/ethernet/ethernet_component_esp32.cpp new file mode 100644 index 0000000000..5745ccf3a7 --- /dev/null +++ b/esphome/components/ethernet/ethernet_component_esp32.cpp @@ -0,0 +1,895 @@ +#include "ethernet_component.h" + +#if defined(USE_ETHERNET) && defined(USE_ESP32) + +#include "esphome/core/application.h" +#include "esphome/core/helpers.h" +#include "esphome/core/log.h" + +#include +#include +#include "esp_event.h" + +// IDF 6.0 moved per-chip PHY/MAC drivers to the Espressif Component Registry; +// they are no longer included via esp_eth.h and need explicit includes. +// On IDF 5.x these headers don't exist as standalone files. +#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(6, 0, 0) +#ifdef USE_ETHERNET_LAN8720 +#include "esp_eth_phy_lan87xx.h" +#endif +#ifdef USE_ETHERNET_RTL8201 +#include "esp_eth_phy_rtl8201.h" +#endif +#ifdef USE_ETHERNET_DP83848 +#include "esp_eth_phy_dp83848.h" +#endif +#ifdef USE_ETHERNET_IP101 +#include "esp_eth_phy_ip101.h" +#endif +#ifdef USE_ETHERNET_KSZ8081 +#include "esp_eth_phy_ksz80xx.h" +#endif +#ifdef USE_ETHERNET_W5500 +#include "esp_eth_mac_w5500.h" +#include "esp_eth_phy_w5500.h" +#endif +#ifdef USE_ETHERNET_DM9051 +#include "esp_eth_mac_dm9051.h" +#include "esp_eth_phy_dm9051.h" +#endif +#endif // ESP_IDF_VERSION >= 6.0.0 + +// LAN867x header exists on all IDF versions (external component since IDF 5.3) +#ifdef USE_ETHERNET_LAN8670 +#include "esp_eth_phy_lan867x.h" +#endif + +// ENC28J60 header exists on all IDF versions (always an external component) +#ifdef USE_ETHERNET_ENC28J60 +#include "esp_eth_enc28j60.h" +#endif + +#ifdef USE_ETHERNET_SPI +#include +#include +#endif + +namespace esphome::ethernet { + +static const char *const TAG = "ethernet"; + +// PHY register size for hex logging +static constexpr size_t PHY_REG_SIZE = 2; + +void EthernetComponent::log_error_and_mark_failed_(esp_err_t err, const char *message) { + ESP_LOGE(TAG, "%s: (%d) %s", message, err, esp_err_to_name(err)); + this->mark_failed(); +} + +#define ESPHL_ERROR_CHECK(err, message) \ + if ((err) != ESP_OK) { \ + this->log_error_and_mark_failed_(err, message); \ + return; \ + } + +#define ESPHL_ERROR_CHECK_RET(err, message, ret) \ + if ((err) != ESP_OK) { \ + this->log_error_and_mark_failed_(err, message); \ + return ret; \ + } + +void EthernetComponent::loop() { + const uint32_t now = App.get_loop_component_start_time(); + + switch (this->state_) { + case EthernetComponentState::STOPPED: + if (this->started_) { + ESP_LOGI(TAG, "Starting connection"); + this->state_ = EthernetComponentState::CONNECTING; + this->start_connect_(); + } + break; + case EthernetComponentState::CONNECTING: + if (!this->started_) { + ESP_LOGI(TAG, "Stopped connection"); + this->state_ = EthernetComponentState::STOPPED; + } else if (this->connected_) { + // connection established + ESP_LOGI(TAG, "Connected"); + this->state_ = EthernetComponentState::CONNECTED; + + this->dump_connect_params_(); + this->status_clear_warning(); +#ifdef USE_ETHERNET_CONNECT_TRIGGER + this->connect_trigger_.trigger(); +#endif + } else if (now - this->connect_begin_ > 15000) { + ESP_LOGW(TAG, "Connecting failed; reconnecting"); + this->start_connect_(); + } + break; + case EthernetComponentState::CONNECTED: + if (!this->started_) { + ESP_LOGI(TAG, "Stopped connection"); + this->state_ = EthernetComponentState::STOPPED; +#ifdef USE_ETHERNET_DISCONNECT_TRIGGER + this->disconnect_trigger_.trigger(); +#endif + } else if (!this->connected_) { + ESP_LOGW(TAG, "Connection lost; reconnecting"); + this->state_ = EthernetComponentState::CONNECTING; + this->start_connect_(); +#ifdef USE_ETHERNET_DISCONNECT_TRIGGER + this->disconnect_trigger_.trigger(); +#endif + } else { + this->finish_connect_(); + // When connected and stable, disable the loop to save CPU cycles + this->disable_loop(); + } + break; + } +} + +void EthernetComponent::setup() { + if (esp_reset_reason() != ESP_RST_DEEPSLEEP) { + // Delay here to allow power to stabilise before Ethernet is initialized. + delay(300); // NOLINT + } + + esp_err_t err; + +#ifdef USE_ETHERNET_SPI + // Install GPIO ISR handler to be able to service SPI Eth modules interrupts + gpio_install_isr_service(0); + + spi_bus_config_t buscfg = { + .mosi_io_num = this->mosi_pin_, + .miso_io_num = this->miso_pin_, + .sclk_io_num = this->clk_pin_, + .quadwp_io_num = -1, + .quadhd_io_num = -1, + .data4_io_num = -1, + .data5_io_num = -1, + .data6_io_num = -1, + .data7_io_num = -1, + .max_transfer_sz = 0, + .flags = 0, + .intr_flags = 0, + }; + + auto host = this->interface_; + + err = spi_bus_initialize(host, &buscfg, SPI_DMA_CH_AUTO); + ESPHL_ERROR_CHECK(err, "SPI bus initialize error"); +#endif + + err = esp_netif_init(); + ESPHL_ERROR_CHECK(err, "ETH netif init error"); + err = esp_event_loop_create_default(); + ESPHL_ERROR_CHECK(err, "ETH event loop error"); + + esp_netif_config_t cfg = ESP_NETIF_DEFAULT_ETH(); + this->eth_netif_ = esp_netif_new(&cfg); + + // Init MAC and PHY configs to default + eth_phy_config_t phy_config = ETH_PHY_DEFAULT_CONFIG(); + eth_mac_config_t mac_config = ETH_MAC_DEFAULT_CONFIG(); + +#ifdef USE_ETHERNET_SPI // Configure SPI interface and Ethernet driver for specific SPI module + spi_device_interface_config_t devcfg = { + .command_bits = 0, + .address_bits = 0, + .dummy_bits = 0, + .mode = 0, + .duty_cycle_pos = 0, + .cs_ena_pretrans = 0, + .cs_ena_posttrans = 0, + .clock_speed_hz = this->clock_speed_, + .input_delay_ns = 0, + .spics_io_num = this->cs_pin_, + .flags = 0, + .queue_size = 20, + .pre_cb = nullptr, + .post_cb = nullptr, + }; + +#if defined(USE_ETHERNET_W5500) + eth_w5500_config_t w5500_config = ETH_W5500_DEFAULT_CONFIG(host, &devcfg); +#elif defined(USE_ETHERNET_DM9051) + eth_dm9051_config_t dm9051_config = ETH_DM9051_DEFAULT_CONFIG(host, &devcfg); +#elif defined(USE_ETHERNET_ENC28J60) + eth_enc28j60_config_t enc28j60_config = ETH_ENC28J60_DEFAULT_CONFIG(host, &devcfg); +#endif + +#if defined(USE_ETHERNET_W5500) + w5500_config.int_gpio_num = this->interrupt_pin_; +#ifdef USE_ETHERNET_SPI_POLLING_SUPPORT + w5500_config.poll_period_ms = this->polling_interval_; +#endif +#elif defined(USE_ETHERNET_DM9051) + dm9051_config.int_gpio_num = this->interrupt_pin_; +#ifdef USE_ETHERNET_SPI_POLLING_SUPPORT + dm9051_config.poll_period_ms = this->polling_interval_; +#endif +#elif defined(USE_ETHERNET_ENC28J60) + enc28j60_config.int_gpio_num = this->interrupt_pin_; + // ENC28J60 does not support poll_period_ms +#endif + + phy_config.phy_addr = this->phy_addr_spi_; + phy_config.reset_gpio_num = this->reset_pin_; + + esp_eth_mac_t *mac = nullptr; +#elif defined(USE_ETHERNET_OPENETH) + esp_eth_mac_t *mac = esp_eth_mac_new_openeth(&mac_config); +#else + phy_config.phy_addr = this->phy_addr_; + phy_config.reset_gpio_num = this->power_pin_; + + eth_esp32_emac_config_t esp32_emac_config = eth_esp32_emac_default_config(); +#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 3, 0) + esp32_emac_config.smi_gpio.mdc_num = this->mdc_pin_; + esp32_emac_config.smi_gpio.mdio_num = this->mdio_pin_; +#else + esp32_emac_config.smi_mdc_gpio_num = this->mdc_pin_; + esp32_emac_config.smi_mdio_gpio_num = this->mdio_pin_; +#endif + esp32_emac_config.clock_config.rmii.clock_mode = this->clk_mode_; + esp32_emac_config.clock_config.rmii.clock_gpio = + static_cast(this->clk_pin_); + + esp_eth_mac_t *mac = esp_eth_mac_new_esp32(&esp32_emac_config, &mac_config); +#endif + + switch (this->type_) { +#ifdef USE_ETHERNET_OPENETH + case ETHERNET_TYPE_OPENETH: { + phy_config.autonego_timeout_ms = 1000; +#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(6, 0, 0) + this->phy_ = esp_eth_phy_new_generic(&phy_config); +#else + this->phy_ = esp_eth_phy_new_dp83848(&phy_config); +#endif + break; + } +#endif +#if CONFIG_ETH_USE_ESP32_EMAC +#ifdef USE_ETHERNET_LAN8720 + case ETHERNET_TYPE_LAN8720: { + this->phy_ = esp_eth_phy_new_lan87xx(&phy_config); + break; + } +#endif +#ifdef USE_ETHERNET_RTL8201 + case ETHERNET_TYPE_RTL8201: { + this->phy_ = esp_eth_phy_new_rtl8201(&phy_config); + break; + } +#endif +#ifdef USE_ETHERNET_DP83848 + case ETHERNET_TYPE_DP83848: { + this->phy_ = esp_eth_phy_new_dp83848(&phy_config); + break; + } +#endif +#ifdef USE_ETHERNET_IP101 + case ETHERNET_TYPE_IP101: { + this->phy_ = esp_eth_phy_new_ip101(&phy_config); + break; + } +#endif +#ifdef USE_ETHERNET_JL1101 + case ETHERNET_TYPE_JL1101: { + // PlatformIO (pioarduino): builtin esp_eth_phy_new_jl1101() on all IDF versions + // Non-PlatformIO: custom ESPHome driver (esp_eth_phy_jl1101.c) + this->phy_ = esp_eth_phy_new_jl1101(&phy_config); + break; + } +#endif +#ifdef USE_ETHERNET_KSZ8081 + case ETHERNET_TYPE_KSZ8081: + case ETHERNET_TYPE_KSZ8081RNA: { + this->phy_ = esp_eth_phy_new_ksz80xx(&phy_config); + break; + } +#endif +#ifdef USE_ETHERNET_LAN8670 + case ETHERNET_TYPE_LAN8670: { + this->phy_ = esp_eth_phy_new_lan867x(&phy_config); + break; + } +#endif +#endif +#ifdef USE_ETHERNET_SPI +#if defined(USE_ETHERNET_W5500) + case ETHERNET_TYPE_W5500: { + mac = esp_eth_mac_new_w5500(&w5500_config, &mac_config); + this->phy_ = esp_eth_phy_new_w5500(&phy_config); + break; + } +#elif defined(USE_ETHERNET_DM9051) + case ETHERNET_TYPE_DM9051: { + mac = esp_eth_mac_new_dm9051(&dm9051_config, &mac_config); + this->phy_ = esp_eth_phy_new_dm9051(&phy_config); + break; + } +#elif defined(USE_ETHERNET_ENC28J60) + case ETHERNET_TYPE_ENC28J60: { + mac = esp_eth_mac_new_enc28j60(&enc28j60_config, &mac_config); + this->phy_ = esp_eth_phy_new_enc28j60(&phy_config); + break; + } +#endif +#endif + default: { + this->mark_failed(); + return; + } + } + + esp_eth_config_t eth_config = ETH_DEFAULT_CONFIG(mac, this->phy_); + this->eth_handle_ = nullptr; + err = esp_eth_driver_install(ð_config, &this->eth_handle_); + ESPHL_ERROR_CHECK(err, "ETH driver install error"); + +#ifndef USE_ETHERNET_SPI +#ifdef USE_ETHERNET_KSZ8081 + if (this->type_ == ETHERNET_TYPE_KSZ8081RNA && this->clk_mode_ == EMAC_CLK_OUT) { + // KSZ8081RNA default is incorrect. It expects a 25MHz clock instead of the 50MHz we provide. + this->ksz8081_set_clock_reference_(mac); + } +#endif // USE_ETHERNET_KSZ8081 + + for (const auto &phy_register : this->phy_registers_) { + this->write_phy_register_(mac, phy_register); + } +#endif + + // use ESP internal eth mac + uint8_t mac_addr[6]; + if (this->fixed_mac_.has_value()) { + memcpy(mac_addr, this->fixed_mac_->data(), 6); + } else { + esp_read_mac(mac_addr, ESP_MAC_ETH); + } + err = esp_eth_ioctl(this->eth_handle_, ETH_CMD_S_MAC_ADDR, mac_addr); + ESPHL_ERROR_CHECK(err, "set mac address error"); + + /* attach Ethernet driver to TCP/IP stack */ + err = esp_netif_attach(this->eth_netif_, esp_eth_new_netif_glue(this->eth_handle_)); + ESPHL_ERROR_CHECK(err, "ETH netif attach error"); + + // Register user defined event handers + err = esp_event_handler_register(ETH_EVENT, ESP_EVENT_ANY_ID, &EthernetComponent::eth_event_handler, nullptr); + ESPHL_ERROR_CHECK(err, "ETH event handler register error"); + err = esp_event_handler_register(IP_EVENT, IP_EVENT_ETH_GOT_IP, &EthernetComponent::got_ip_event_handler, nullptr); + ESPHL_ERROR_CHECK(err, "GOT IP event handler register error"); +#if USE_NETWORK_IPV6 + err = esp_event_handler_register(IP_EVENT, IP_EVENT_GOT_IP6, &EthernetComponent::got_ip6_event_handler, nullptr); + ESPHL_ERROR_CHECK(err, "GOT IPv6 event handler register error"); +#endif /* USE_NETWORK_IPV6 */ + + /* start Ethernet driver state machine */ + err = esp_eth_start(this->eth_handle_); + ESPHL_ERROR_CHECK(err, "ETH start error"); +} + +void EthernetComponent::dump_config() { + const char *eth_type; + switch (this->type_) { +#ifdef USE_ETHERNET_LAN8720 + case ETHERNET_TYPE_LAN8720: + eth_type = "LAN8720"; + break; +#endif +#ifdef USE_ETHERNET_RTL8201 + case ETHERNET_TYPE_RTL8201: + eth_type = "RTL8201"; + break; +#endif +#ifdef USE_ETHERNET_DP83848 + case ETHERNET_TYPE_DP83848: + eth_type = "DP83848"; + break; +#endif +#ifdef USE_ETHERNET_IP101 + case ETHERNET_TYPE_IP101: + eth_type = "IP101"; + break; +#endif +#ifdef USE_ETHERNET_JL1101 + case ETHERNET_TYPE_JL1101: + eth_type = "JL1101"; + break; +#endif +#ifdef USE_ETHERNET_KSZ8081 + case ETHERNET_TYPE_KSZ8081: + eth_type = "KSZ8081"; + break; + + case ETHERNET_TYPE_KSZ8081RNA: + eth_type = "KSZ8081RNA"; + break; +#endif +#if defined(USE_ETHERNET_W5500) + case ETHERNET_TYPE_W5500: + eth_type = "W5500"; + break; +#elif defined(USE_ETHERNET_DM9051) + case ETHERNET_TYPE_DM9051: + eth_type = "DM9051"; + break; +#elif defined(USE_ETHERNET_ENC28J60) + case ETHERNET_TYPE_ENC28J60: + eth_type = "ENC28J60"; + break; +#endif +#ifdef USE_ETHERNET_OPENETH + case ETHERNET_TYPE_OPENETH: + eth_type = "OPENETH"; + break; +#endif +#ifdef USE_ETHERNET_LAN8670 + case ETHERNET_TYPE_LAN8670: + eth_type = "LAN8670"; + break; +#endif + + default: + eth_type = "Unknown"; + break; + } + + ESP_LOGCONFIG(TAG, + "Ethernet:\n" + " Connected: %s", + YESNO(this->is_connected())); + this->dump_connect_params_(); +#ifdef USE_ETHERNET_SPI + ESP_LOGCONFIG(TAG, + " CLK Pin: %u\n" + " MISO Pin: %u\n" + " MOSI Pin: %u\n" + " CS Pin: %u", + this->clk_pin_, this->miso_pin_, this->mosi_pin_, this->cs_pin_); + { + constexpr std::array values{"spi1", "spi2", "spi3"}; + if (this->interface_ < values.size()) { + ESP_LOGCONFIG(TAG, " Interface: %s", values[this->interface_]); + } + } +#ifdef USE_ETHERNET_SPI_POLLING_SUPPORT + if (this->polling_interval_ != 0) { + ESP_LOGCONFIG(TAG, " Polling Interval: %" PRIu32 " ms", this->polling_interval_); + } else +#endif + { + ESP_LOGCONFIG(TAG, " IRQ Pin: %d", this->interrupt_pin_); + } + ESP_LOGCONFIG(TAG, + " Reset Pin: %d\n" + " Clock Speed: %d MHz", + this->reset_pin_, this->clock_speed_ / 1000000); +#else + if (this->power_pin_ != -1) { + ESP_LOGCONFIG(TAG, " Power Pin: %u", this->power_pin_); + } + ESP_LOGCONFIG(TAG, + " CLK Pin: %u\n" + " MDC Pin: %u\n" + " MDIO Pin: %u\n" + " PHY addr: %u", + this->clk_pin_, this->mdc_pin_, this->mdio_pin_, this->phy_addr_); +#endif + ESP_LOGCONFIG(TAG, " Type: %s", eth_type); +} + +network::IPAddresses EthernetComponent::get_ip_addresses() { + network::IPAddresses addresses; + esp_netif_ip_info_t ip; + esp_err_t err = esp_netif_get_ip_info(this->eth_netif_, &ip); + if (err != ESP_OK) { + ESP_LOGV(TAG, "esp_netif_get_ip_info failed: %s", esp_err_to_name(err)); + // TODO: do something smarter + // return false; + } else { + addresses[0] = network::IPAddress(&ip.ip); + } +#if USE_NETWORK_IPV6 + struct esp_ip6_addr if_ip6s[CONFIG_LWIP_IPV6_NUM_ADDRESSES]; + uint8_t count = 0; + count = esp_netif_get_all_ip6(this->eth_netif_, if_ip6s); + assert(count <= CONFIG_LWIP_IPV6_NUM_ADDRESSES); + assert(count < addresses.size()); + for (int i = 0; i < count; i++) { + addresses[i + 1] = network::IPAddress(&if_ip6s[i]); + } +#endif /* USE_NETWORK_IPV6 */ + + return addresses; +} + +network::IPAddress EthernetComponent::get_dns_address(uint8_t num) { + LwIPLock lock; + const ip_addr_t *dns_ip = dns_getserver(num); + return dns_ip; +} + +void EthernetComponent::eth_event_handler(void *arg, esp_event_base_t event_base, int32_t event, void *event_data) { + const char *event_name; + + switch (event) { + case ETHERNET_EVENT_START: + event_name = "ETH started"; + global_eth_component->started_ = true; + global_eth_component->enable_loop_soon_any_context(); + break; + case ETHERNET_EVENT_STOP: + event_name = "ETH stopped"; + global_eth_component->started_ = false; + global_eth_component->connected_ = false; + global_eth_component->enable_loop_soon_any_context(); // Enable loop when connection state changes + break; + case ETHERNET_EVENT_CONNECTED: + event_name = "ETH connected"; + // For static IP configurations, GOT_IP event may not fire, so notify IP listeners here +#if defined(USE_ETHERNET_IP_STATE_LISTENERS) && defined(USE_ETHERNET_MANUAL_IP) + if (global_eth_component->manual_ip_.has_value()) { + global_eth_component->notify_ip_state_listeners_(); + } +#endif + break; + case ETHERNET_EVENT_DISCONNECTED: + event_name = "ETH disconnected"; + global_eth_component->connected_ = false; + global_eth_component->enable_loop_soon_any_context(); // Enable loop when connection state changes + break; + default: + return; + } + + ESP_LOGV(TAG, "[Ethernet event] %s (num=%" PRId32 ")", event_name, event); +} + +void EthernetComponent::got_ip_event_handler(void *arg, esp_event_base_t event_base, int32_t event_id, + void *event_data) { + ip_event_got_ip_t *event = (ip_event_got_ip_t *) event_data; + const esp_netif_ip_info_t *ip_info = &event->ip_info; + ESP_LOGV(TAG, "[Ethernet event] ETH Got IP " IPSTR, IP2STR(&ip_info->ip)); + global_eth_component->got_ipv4_address_ = true; +#if USE_NETWORK_IPV6 && (USE_NETWORK_MIN_IPV6_ADDR_COUNT > 0) + global_eth_component->connected_ = global_eth_component->ipv6_count_ >= USE_NETWORK_MIN_IPV6_ADDR_COUNT; + global_eth_component->enable_loop_soon_any_context(); // Enable loop when connection state changes +#else + global_eth_component->connected_ = true; + global_eth_component->enable_loop_soon_any_context(); // Enable loop when connection state changes +#endif /* USE_NETWORK_IPV6 */ +#ifdef USE_ETHERNET_IP_STATE_LISTENERS + global_eth_component->notify_ip_state_listeners_(); +#endif +} + +#if USE_NETWORK_IPV6 +void EthernetComponent::got_ip6_event_handler(void *arg, esp_event_base_t event_base, int32_t event_id, + void *event_data) { + ip_event_got_ip6_t *event = (ip_event_got_ip6_t *) event_data; + ESP_LOGV(TAG, "[Ethernet event] ETH Got IPv6: " IPV6STR, IPV62STR(event->ip6_info.ip)); + global_eth_component->ipv6_count_ += 1; +#if (USE_NETWORK_MIN_IPV6_ADDR_COUNT > 0) + global_eth_component->connected_ = + global_eth_component->got_ipv4_address_ && (global_eth_component->ipv6_count_ >= USE_NETWORK_MIN_IPV6_ADDR_COUNT); + global_eth_component->enable_loop_soon_any_context(); // Enable loop when connection state changes +#else + global_eth_component->connected_ = global_eth_component->got_ipv4_address_; + global_eth_component->enable_loop_soon_any_context(); // Enable loop when connection state changes +#endif +#ifdef USE_ETHERNET_IP_STATE_LISTENERS + global_eth_component->notify_ip_state_listeners_(); +#endif +} +#endif /* USE_NETWORK_IPV6 */ + +void EthernetComponent::finish_connect_() { +#if USE_NETWORK_IPV6 + // Retry IPv6 link-local setup if it failed during initial connect + // This handles the case where min_ipv6_addr_count is NOT set (or is 0), + // allowing us to reach CONNECTED state with just IPv4. + // If IPv6 setup failed in start_connect_() because the interface wasn't ready: + // - Bootup timing issues (#10281) + // - Cable unplugged/network interruption (#10705) + // We can now retry since we're in CONNECTED state and the interface is definitely up. + if (!this->ipv6_setup_done_) { + esp_err_t err = esp_netif_create_ip6_linklocal(this->eth_netif_); + if (err == ESP_OK) { + ESP_LOGD(TAG, "IPv6 link-local address created (retry succeeded)"); + } + // Always set the flag to prevent continuous retries + // If IPv6 setup fails here with the interface up and stable, it's + // likely a persistent issue (IPv6 disabled at router, hardware + // limitation, etc.) that won't be resolved by further retries. + // The device continues to work with IPv4. + this->ipv6_setup_done_ = true; + } +#endif /* USE_NETWORK_IPV6 */ +} + +void EthernetComponent::start_connect_() { + global_eth_component->got_ipv4_address_ = false; +#if USE_NETWORK_IPV6 + global_eth_component->ipv6_count_ = 0; + this->ipv6_setup_done_ = false; +#endif /* USE_NETWORK_IPV6 */ + this->connect_begin_ = millis(); + this->status_set_warning(LOG_STR("waiting for IP configuration")); + + esp_err_t err; + err = esp_netif_set_hostname(this->eth_netif_, App.get_name().c_str()); + if (err != ERR_OK) { + ESP_LOGW(TAG, "esp_netif_set_hostname failed: %s", esp_err_to_name(err)); + } + + esp_netif_ip_info_t info; +#ifdef USE_ETHERNET_MANUAL_IP + if (this->manual_ip_.has_value()) { + info.ip = this->manual_ip_->static_ip; + info.gw = this->manual_ip_->gateway; + info.netmask = this->manual_ip_->subnet; + } else +#endif + { + info.ip.addr = 0; + info.gw.addr = 0; + info.netmask.addr = 0; + } + + esp_netif_dhcp_status_t status = ESP_NETIF_DHCP_INIT; + + err = esp_netif_dhcpc_get_status(this->eth_netif_, &status); + ESPHL_ERROR_CHECK(err, "DHCPC Get Status Failed!"); + + ESP_LOGV(TAG, "DHCP Client Status: %d", status); + + err = esp_netif_dhcpc_stop(this->eth_netif_); + if (err != ESP_ERR_ESP_NETIF_DHCP_ALREADY_STOPPED) { + ESPHL_ERROR_CHECK(err, "DHCPC stop error"); + } + + err = esp_netif_set_ip_info(this->eth_netif_, &info); + ESPHL_ERROR_CHECK(err, "DHCPC set IP info error"); + +#ifdef USE_ETHERNET_MANUAL_IP + if (this->manual_ip_.has_value()) { + LwIPLock lock; + if (this->manual_ip_->dns1.is_set()) { + ip_addr_t d; + d = this->manual_ip_->dns1; + dns_setserver(0, &d); + } + if (this->manual_ip_->dns2.is_set()) { + ip_addr_t d; + d = this->manual_ip_->dns2; + dns_setserver(1, &d); + } + } else +#endif + { + err = esp_netif_dhcpc_start(this->eth_netif_); + if (err != ESP_ERR_ESP_NETIF_DHCP_ALREADY_STARTED) { + ESPHL_ERROR_CHECK(err, "DHCPC start error"); + } + } +#if USE_NETWORK_IPV6 + // Attempt to create IPv6 link-local address + // We MUST attempt this here, not just in finish_connect_(), because with + // min_ipv6_addr_count set, the component won't reach CONNECTED state without IPv6. + // However, this may fail with ESP_FAIL if the interface is not up yet: + // - At bootup when link isn't ready (#10281) + // - After disconnection/cable unplugged (#10705) + // We'll retry in finish_connect_() if it fails here. + err = esp_netif_create_ip6_linklocal(this->eth_netif_); + if (err != ESP_OK) { + if (err == ESP_ERR_ESP_NETIF_INVALID_PARAMS) { + // This is a programming error, not a transient failure + ESPHL_ERROR_CHECK(err, "esp_netif_create_ip6_linklocal invalid parameters"); + } else { + // ESP_FAIL means the interface isn't up yet + // This is expected and non-fatal, happens in multiple scenarios: + // - During reconnection after network interruptions (#10705) + // - At bootup when the link isn't ready yet (#10281) + // We'll retry once we reach CONNECTED state and the interface is up + ESP_LOGW(TAG, "esp_netif_create_ip6_linklocal failed: %s", esp_err_to_name(err)); + // Don't mark component as failed - this is a transient error + } + } +#endif /* USE_NETWORK_IPV6 */ + + this->connect_begin_ = millis(); + this->status_set_warning(); +} + +void EthernetComponent::dump_connect_params_() { + esp_netif_ip_info_t ip; + esp_netif_get_ip_info(this->eth_netif_, &ip); + const ip_addr_t *dns_ip1; + const ip_addr_t *dns_ip2; + { + LwIPLock lock; + dns_ip1 = dns_getserver(0); + dns_ip2 = dns_getserver(1); + } + + // Use stack buffers for IP address formatting to avoid heap allocations + char ip_buf[network::IP_ADDRESS_BUFFER_SIZE]; + char subnet_buf[network::IP_ADDRESS_BUFFER_SIZE]; + char gateway_buf[network::IP_ADDRESS_BUFFER_SIZE]; + char dns1_buf[network::IP_ADDRESS_BUFFER_SIZE]; + char dns2_buf[network::IP_ADDRESS_BUFFER_SIZE]; + char mac_buf[MAC_ADDRESS_PRETTY_BUFFER_SIZE]; + ESP_LOGCONFIG(TAG, + " IP Address: %s\n" + " Hostname: '%s'\n" + " Subnet: %s\n" + " Gateway: %s\n" + " DNS1: %s\n" + " DNS2: %s\n" + " MAC Address: %s\n" + " Is Full Duplex: %s\n" + " Link Speed: %u", + network::IPAddress(&ip.ip).str_to(ip_buf), App.get_name().c_str(), + network::IPAddress(&ip.netmask).str_to(subnet_buf), network::IPAddress(&ip.gw).str_to(gateway_buf), + network::IPAddress(dns_ip1).str_to(dns1_buf), network::IPAddress(dns_ip2).str_to(dns2_buf), + this->get_eth_mac_address_pretty_into_buffer(mac_buf), + YESNO(this->get_duplex_mode() == ETH_DUPLEX_FULL), this->get_link_speed() == ETH_SPEED_100M ? 100 : 10); + +#if USE_NETWORK_IPV6 + struct esp_ip6_addr if_ip6s[CONFIG_LWIP_IPV6_NUM_ADDRESSES]; + uint8_t count = 0; + count = esp_netif_get_all_ip6(this->eth_netif_, if_ip6s); + assert(count <= CONFIG_LWIP_IPV6_NUM_ADDRESSES); + for (int i = 0; i < count; i++) { + ESP_LOGCONFIG(TAG, " IPv6: " IPV6STR, IPV62STR(if_ip6s[i])); + } +#endif /* USE_NETWORK_IPV6 */ +} + +#ifdef USE_ETHERNET_SPI +void EthernetComponent::set_clk_pin(uint8_t clk_pin) { this->clk_pin_ = clk_pin; } +void EthernetComponent::set_miso_pin(uint8_t miso_pin) { this->miso_pin_ = miso_pin; } +void EthernetComponent::set_mosi_pin(uint8_t mosi_pin) { this->mosi_pin_ = mosi_pin; } +void EthernetComponent::set_cs_pin(uint8_t cs_pin) { this->cs_pin_ = cs_pin; } +void EthernetComponent::set_interrupt_pin(uint8_t interrupt_pin) { this->interrupt_pin_ = interrupt_pin; } +void EthernetComponent::set_reset_pin(uint8_t reset_pin) { this->reset_pin_ = reset_pin; } +void EthernetComponent::set_clock_speed(int clock_speed) { this->clock_speed_ = clock_speed; } +void EthernetComponent::set_interface(spi_host_device_t interface) { this->interface_ = interface; } +#ifdef USE_ETHERNET_SPI_POLLING_SUPPORT +void EthernetComponent::set_polling_interval(uint32_t polling_interval) { this->polling_interval_ = polling_interval; } +#endif +#else +void EthernetComponent::set_phy_addr(uint8_t phy_addr) { this->phy_addr_ = phy_addr; } +void EthernetComponent::set_power_pin(int power_pin) { this->power_pin_ = power_pin; } +void EthernetComponent::set_mdc_pin(uint8_t mdc_pin) { this->mdc_pin_ = mdc_pin; } +void EthernetComponent::set_mdio_pin(uint8_t mdio_pin) { this->mdio_pin_ = mdio_pin; } +void EthernetComponent::set_clk_pin(uint8_t clk_pin) { this->clk_pin_ = clk_pin; } +void EthernetComponent::set_clk_mode(emac_rmii_clock_mode_t clk_mode) { this->clk_mode_ = clk_mode; } +void EthernetComponent::add_phy_register(PHYRegister register_value) { this->phy_registers_.push_back(register_value); } +#endif + +void EthernetComponent::get_eth_mac_address_raw(uint8_t *mac) { + esp_err_t err; + err = esp_eth_ioctl(this->eth_handle_, ETH_CMD_G_MAC_ADDR, mac); + ESPHL_ERROR_CHECK(err, "ETH_CMD_G_MAC error"); +} + +std::string EthernetComponent::get_eth_mac_address_pretty() { + char buf[MAC_ADDRESS_PRETTY_BUFFER_SIZE]; + return std::string(this->get_eth_mac_address_pretty_into_buffer(buf)); +} + +const char *EthernetComponent::get_eth_mac_address_pretty_into_buffer( + std::span buf) { + uint8_t mac[6]; + get_eth_mac_address_raw(mac); + format_mac_addr_upper(mac, buf.data()); + return buf.data(); +} + +eth_duplex_t EthernetComponent::get_duplex_mode() { + esp_err_t err; + eth_duplex_t duplex_mode; + err = esp_eth_ioctl(this->eth_handle_, ETH_CMD_G_DUPLEX_MODE, &duplex_mode); + ESPHL_ERROR_CHECK_RET(err, "ETH_CMD_G_DUPLEX_MODE error", ETH_DUPLEX_HALF); + return duplex_mode; +} + +eth_speed_t EthernetComponent::get_link_speed() { + esp_err_t err; + eth_speed_t speed; + err = esp_eth_ioctl(this->eth_handle_, ETH_CMD_G_SPEED, &speed); + ESPHL_ERROR_CHECK_RET(err, "ETH_CMD_G_SPEED error", ETH_SPEED_10M); + return speed; +} + +bool EthernetComponent::powerdown() { + ESP_LOGI(TAG, "Powering down ethernet PHY"); + if (this->phy_ == nullptr) { + ESP_LOGE(TAG, "Ethernet PHY not assigned"); + return false; + } + this->connected_ = false; + this->started_ = false; + // No need to enable_loop() here as this is only called during shutdown/reboot + if (this->phy_->pwrctl(this->phy_, false) != ESP_OK) { + ESP_LOGE(TAG, "Error powering down ethernet PHY"); + return false; + } + return true; +} + +#ifndef USE_ETHERNET_SPI + +#ifdef USE_ETHERNET_KSZ8081 +constexpr uint8_t KSZ80XX_PC2R_REG_ADDR = 0x1F; + +void EthernetComponent::ksz8081_set_clock_reference_(esp_eth_mac_t *mac) { + esp_err_t err; + + uint32_t phy_control_2; + err = mac->read_phy_reg(mac, this->phy_addr_, KSZ80XX_PC2R_REG_ADDR, &(phy_control_2)); + ESPHL_ERROR_CHECK(err, "Read PHY Control 2 failed"); +#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERY_VERBOSE + char hex_buf[format_hex_pretty_size(PHY_REG_SIZE)]; +#endif + ESP_LOGVV(TAG, "KSZ8081 PHY Control 2: %s", format_hex_pretty_to(hex_buf, (uint8_t *) &phy_control_2, PHY_REG_SIZE)); + + /* + * Bit 7 is `RMII Reference Clock Select`. Default is `0`. + * KSZ8081RNA: + * 0 - clock input to XI (Pin 8) is 25 MHz for RMII - 25 MHz clock mode. + * 1 - clock input to XI (Pin 8) is 50 MHz for RMII - 50 MHz clock mode. + * KSZ8081RND: + * 0 - clock input to XI (Pin 8) is 50 MHz for RMII - 50 MHz clock mode. + * 1 - clock input to XI (Pin 8) is 25 MHz (driven clock only, not a crystal) for RMII - 25 MHz clock mode. + */ + if ((phy_control_2 & (1 << 7)) != (1 << 7)) { + phy_control_2 |= 1 << 7; + err = mac->write_phy_reg(mac, this->phy_addr_, KSZ80XX_PC2R_REG_ADDR, phy_control_2); + ESPHL_ERROR_CHECK(err, "Write PHY Control 2 failed"); + err = mac->read_phy_reg(mac, this->phy_addr_, KSZ80XX_PC2R_REG_ADDR, &(phy_control_2)); + ESPHL_ERROR_CHECK(err, "Read PHY Control 2 failed"); + ESP_LOGVV(TAG, "KSZ8081 PHY Control 2: %s", + format_hex_pretty_to(hex_buf, (uint8_t *) &phy_control_2, PHY_REG_SIZE)); + } +} +#endif // USE_ETHERNET_KSZ8081 + +void EthernetComponent::write_phy_register_(esp_eth_mac_t *mac, PHYRegister register_data) { + esp_err_t err; + +#ifdef USE_ETHERNET_RTL8201 + constexpr uint8_t eth_phy_psr_reg_addr = 0x1F; + if (this->type_ == ETHERNET_TYPE_RTL8201 && register_data.page) { + ESP_LOGD(TAG, "Select PHY Register Page: 0x%02" PRIX32, register_data.page); + err = mac->write_phy_reg(mac, this->phy_addr_, eth_phy_psr_reg_addr, register_data.page); + ESPHL_ERROR_CHECK(err, "Select PHY Register page failed"); + } +#endif + + ESP_LOGD(TAG, "Writing PHY reg 0x%02" PRIX32 " = 0x%04" PRIX32, register_data.address, register_data.value); + err = mac->write_phy_reg(mac, this->phy_addr_, register_data.address, register_data.value); + ESPHL_ERROR_CHECK(err, "Writing PHY Register failed"); + +#ifdef USE_ETHERNET_RTL8201 + if (this->type_ == ETHERNET_TYPE_RTL8201 && register_data.page) { + ESP_LOGD(TAG, "Select PHY Register Page 0x00"); + err = mac->write_phy_reg(mac, this->phy_addr_, eth_phy_psr_reg_addr, 0x0); + ESPHL_ERROR_CHECK(err, "Select PHY Register Page 0 failed"); + } +#endif +} + +#endif + +} // namespace esphome::ethernet + +#endif // USE_ETHERNET && USE_ESP32 diff --git a/esphome/components/ethernet/ethernet_component_rp2040.cpp b/esphome/components/ethernet/ethernet_component_rp2040.cpp new file mode 100644 index 0000000000..9771bc59d5 --- /dev/null +++ b/esphome/components/ethernet/ethernet_component_rp2040.cpp @@ -0,0 +1,342 @@ +#include "ethernet_component.h" + +#if defined(USE_ETHERNET) && defined(USE_RP2040) + +#include "esphome/core/application.h" +#include "esphome/core/helpers.h" +#include "esphome/core/log.h" + +#include "esphome/components/rp2040/gpio.h" + +#include +#include +#include + +namespace esphome::ethernet { + +static const char *const TAG = "ethernet"; + +void EthernetComponent::setup() { + // Configure SPI pins + SPI.setRX(this->miso_pin_); + SPI.setTX(this->mosi_pin_); + SPI.setSCK(this->clk_pin_); + + // Toggle reset pin if configured + if (this->reset_pin_ >= 0) { + rp2040::RP2040GPIOPin reset_pin; + reset_pin.set_pin(this->reset_pin_); + reset_pin.set_flags(gpio::FLAG_OUTPUT); + reset_pin.setup(); + reset_pin.digital_write(false); + delay(1); // NOLINT + reset_pin.digital_write(true); + // W5100S needs 150ms for PLL lock; W5500/ENC28J60 need ~10ms + delay(RESET_DELAY_MS); // NOLINT + } + + // Create the SPI Ethernet device instance +#if defined(USE_ETHERNET_W5500) + this->eth_ = new Wiznet5500lwIP(this->cs_pin_, SPI, this->interrupt_pin_); // NOLINT +#elif defined(USE_ETHERNET_W5100) + this->eth_ = new Wiznet5100lwIP(this->cs_pin_, SPI, this->interrupt_pin_); // NOLINT +#elif defined(USE_ETHERNET_ENC28J60) + this->eth_ = new ENC28J60lwIP(this->cs_pin_, SPI, this->interrupt_pin_); // NOLINT +#endif + + // Set hostname before begin() so the LWIP netif gets it + this->eth_->hostname(App.get_name().c_str()); + + // Configure static IP if set (must be done before begin()) +#ifdef USE_ETHERNET_MANUAL_IP + if (this->manual_ip_.has_value()) { + IPAddress ip(this->manual_ip_->static_ip); + IPAddress gateway(this->manual_ip_->gateway); + IPAddress subnet(this->manual_ip_->subnet); + IPAddress dns1(this->manual_ip_->dns1); + IPAddress dns2(this->manual_ip_->dns2); + this->eth_->config(ip, gateway, subnet, dns1, dns2); + } +#endif + + // Begin with fixed MAC or auto-generated + bool success; + if (this->fixed_mac_.has_value()) { + success = this->eth_->begin(this->fixed_mac_->data()); + } else { + success = this->eth_->begin(); + } + + if (!success) { + ESP_LOGE(TAG, "Failed to initialize Ethernet"); + delete this->eth_; // NOLINT(cppcoreguidelines-owning-memory) + this->eth_ = nullptr; + this->mark_failed(); + return; + } + + // Make this the default interface for routing + this->eth_->setDefault(true); + + // The arduino-pico LwipIntfDev automatically handles packet processing + // via __addEthernetPacketHandler when no interrupt pin is used, + // or via GPIO interrupt when one is provided. + + // Don't set started_ here — let the link polling in loop() set it + // when the link is actually up. Setting it prematurely causes + // a "Starting → Stopped → Starting" log sequence because the chip + // needs time after begin() before the PHY link is ready. +} + +void EthernetComponent::loop() { + // On RP2040, we need to poll connection state since there are no events. + const uint32_t now = App.get_loop_component_start_time(); + + // Throttle link/IP polling to avoid excessive SPI transactions. + // W5500/ENC28J60 read PHY register via SPI on every linkStatus() call. + // W5100 can't detect link state, so we skip the SPI read and assume link-up. + // connected() reads netif->ip_addr without LwIPLock, but this is a single + // 32-bit aligned read (atomic on ARM) — worst case is a one-iteration-stale + // value, which is benign for polling. + if (this->eth_ != nullptr && now - this->last_link_check_ >= LINK_CHECK_INTERVAL) { + this->last_link_check_ = now; +#if defined(USE_ETHERNET_W5100) + // W5100 can't detect link (isLinkDetectable() returns false), so linkStatus() + // returns Unknown — assume link is up after successful begin() + bool link_up = true; +#else + bool link_up = this->eth_->linkStatus() == LinkON; +#endif + bool has_ip = this->eth_->connected(); + + if (!link_up) { + if (this->started_) { + this->started_ = false; + this->connected_ = false; + } + } else { + if (!this->started_) { + this->started_ = true; + } + bool was_connected = this->connected_; + this->connected_ = has_ip; + if (this->connected_ && !was_connected) { +#ifdef USE_ETHERNET_IP_STATE_LISTENERS + this->notify_ip_state_listeners_(); +#endif + } + } + } + + // State machine + switch (this->state_) { + case EthernetComponentState::STOPPED: + if (this->started_) { + ESP_LOGI(TAG, "Starting connection"); + this->state_ = EthernetComponentState::CONNECTING; + this->start_connect_(); + } + break; + case EthernetComponentState::CONNECTING: + if (!this->started_) { + ESP_LOGI(TAG, "Stopped connection"); + this->state_ = EthernetComponentState::STOPPED; + } else if (this->connected_) { + // connection established + ESP_LOGI(TAG, "Connected"); + this->state_ = EthernetComponentState::CONNECTED; + + this->dump_connect_params_(); + this->status_clear_warning(); +#ifdef USE_ETHERNET_CONNECT_TRIGGER + this->connect_trigger_.trigger(); +#endif + } else if (now - this->connect_begin_ > 15000) { + ESP_LOGW(TAG, "Connecting failed; reconnecting"); + this->start_connect_(); + } + break; + case EthernetComponentState::CONNECTED: + if (!this->started_) { + ESP_LOGI(TAG, "Stopped connection"); + this->state_ = EthernetComponentState::STOPPED; +#ifdef USE_ETHERNET_DISCONNECT_TRIGGER + this->disconnect_trigger_.trigger(); +#endif + } else if (!this->connected_) { + ESP_LOGW(TAG, "Connection lost; reconnecting"); + this->state_ = EthernetComponentState::CONNECTING; + this->start_connect_(); +#ifdef USE_ETHERNET_DISCONNECT_TRIGGER + this->disconnect_trigger_.trigger(); +#endif + } else { + this->finish_connect_(); + } + break; + } +} + +void EthernetComponent::dump_config() { + const char *type_str = "Unknown"; +#if defined(USE_ETHERNET_W5500) + type_str = "W5500"; +#elif defined(USE_ETHERNET_W5100) + type_str = "W5100"; +#elif defined(USE_ETHERNET_ENC28J60) + type_str = "ENC28J60"; +#endif + ESP_LOGCONFIG(TAG, + "Ethernet:\n" + " Type: %s\n" + " Connected: %s\n" + " CLK Pin: %u\n" + " MISO Pin: %u\n" + " MOSI Pin: %u\n" + " CS Pin: %u\n" + " IRQ Pin: %d\n" + " Reset Pin: %d", + type_str, YESNO(this->is_connected()), this->clk_pin_, this->miso_pin_, this->mosi_pin_, this->cs_pin_, + this->interrupt_pin_, this->reset_pin_); + this->dump_connect_params_(); +} + +network::IPAddresses EthernetComponent::get_ip_addresses() { + network::IPAddresses addresses; + if (this->eth_ != nullptr) { + LwIPLock lock; + addresses[0] = network::IPAddress(this->eth_->localIP()); + } + return addresses; +} + +network::IPAddress EthernetComponent::get_dns_address(uint8_t num) { + LwIPLock lock; + const ip_addr_t *dns_ip = dns_getserver(num); + return dns_ip; +} + +void EthernetComponent::get_eth_mac_address_raw(uint8_t *mac) { + if (this->eth_ != nullptr) { + this->eth_->macAddress(mac); + } else { + memset(mac, 0, 6); + } +} + +std::string EthernetComponent::get_eth_mac_address_pretty() { + char buf[MAC_ADDRESS_PRETTY_BUFFER_SIZE]; + return std::string(this->get_eth_mac_address_pretty_into_buffer(buf)); +} + +const char *EthernetComponent::get_eth_mac_address_pretty_into_buffer( + std::span buf) { + uint8_t mac[6]; + get_eth_mac_address_raw(mac); + format_mac_addr_upper(mac, buf.data()); + return buf.data(); +} + +eth_duplex_t EthernetComponent::get_duplex_mode() { + // W5100, W5500, and ENC28J60 are full-duplex on RP2040 + return ETH_DUPLEX_FULL; +} + +eth_speed_t EthernetComponent::get_link_speed() { +#ifdef USE_ETHERNET_ENC28J60 + // ENC28J60 is 10Mbps only + return ETH_SPEED_10M; +#else + // W5100 and W5500 are 100Mbps + return ETH_SPEED_100M; +#endif +} + +bool EthernetComponent::powerdown() { + ESP_LOGI(TAG, "Powering down ethernet"); + if (this->eth_ != nullptr) { + this->eth_->end(); + } + this->connected_ = false; + this->started_ = false; + return true; +} + +void EthernetComponent::start_connect_() { + this->got_ipv4_address_ = false; + this->connect_begin_ = millis(); + this->status_set_warning(LOG_STR("waiting for IP configuration")); + + // Hostname is already set in setup() via LwipIntf::setHostname() + +#ifdef USE_ETHERNET_MANUAL_IP + if (this->manual_ip_.has_value()) { + // Static IP was already configured before begin() in setup() + // Set DNS servers + LwIPLock lock; + if (this->manual_ip_->dns1.is_set()) { + ip_addr_t d; + d = this->manual_ip_->dns1; + dns_setserver(0, &d); + } + if (this->manual_ip_->dns2.is_set()) { + ip_addr_t d; + d = this->manual_ip_->dns2; + dns_setserver(1, &d); + } + } +#endif +} + +void EthernetComponent::finish_connect_() { + // No additional work needed on RP2040 for now + // IPv6 link-local could be added here in the future +} + +void EthernetComponent::dump_connect_params_() { + if (this->eth_ == nullptr) { + return; + } + + char ip_buf[network::IP_ADDRESS_BUFFER_SIZE]; + char subnet_buf[network::IP_ADDRESS_BUFFER_SIZE]; + char gateway_buf[network::IP_ADDRESS_BUFFER_SIZE]; + char dns1_buf[network::IP_ADDRESS_BUFFER_SIZE]; + char dns2_buf[network::IP_ADDRESS_BUFFER_SIZE]; + char mac_buf[MAC_ADDRESS_PRETTY_BUFFER_SIZE]; + + // Copy all lwIP state under the lock to avoid races with IRQ callbacks + ip_addr_t ip_addr, netmask, gw, dns1_addr, dns2_addr; + { + LwIPLock lock; + auto *netif = this->eth_->getNetIf(); + ip_addr = netif->ip_addr; + netmask = netif->netmask; + gw = netif->gw; + dns1_addr = *dns_getserver(0); + dns2_addr = *dns_getserver(1); + } + ESP_LOGCONFIG(TAG, + " IP Address: %s\n" + " Hostname: '%s'\n" + " Subnet: %s\n" + " Gateway: %s\n" + " DNS1: %s\n" + " DNS2: %s\n" + " MAC Address: %s", + network::IPAddress(&ip_addr).str_to(ip_buf), App.get_name().c_str(), + network::IPAddress(&netmask).str_to(subnet_buf), network::IPAddress(&gw).str_to(gateway_buf), + network::IPAddress(&dns1_addr).str_to(dns1_buf), network::IPAddress(&dns2_addr).str_to(dns2_buf), + this->get_eth_mac_address_pretty_into_buffer(mac_buf)); +} + +void EthernetComponent::set_clk_pin(uint8_t clk_pin) { this->clk_pin_ = clk_pin; } +void EthernetComponent::set_miso_pin(uint8_t miso_pin) { this->miso_pin_ = miso_pin; } +void EthernetComponent::set_mosi_pin(uint8_t mosi_pin) { this->mosi_pin_ = mosi_pin; } +void EthernetComponent::set_cs_pin(uint8_t cs_pin) { this->cs_pin_ = cs_pin; } +void EthernetComponent::set_interrupt_pin(int8_t interrupt_pin) { this->interrupt_pin_ = interrupt_pin; } +void EthernetComponent::set_reset_pin(int8_t reset_pin) { this->reset_pin_ = reset_pin; } + +} // namespace esphome::ethernet + +#endif // USE_ETHERNET && USE_RP2040 diff --git a/esphome/components/ethernet/ethernet_helpers.c b/esphome/components/ethernet/ethernet_helpers.c new file mode 100644 index 0000000000..49fbe825c8 --- /dev/null +++ b/esphome/components/ethernet/ethernet_helpers.c @@ -0,0 +1,13 @@ +#include "esphome/core/defines.h" +#ifdef USE_ESP32 +#include "esp_eth_mac_esp.h" + +// ETH_ESP32_EMAC_DEFAULT_CONFIG() uses out-of-order designated initializers +// which are valid in C but not in C++. This wrapper allows C++ code to get +// the default config without replicating the macro's contents. +#if CONFIG_ETH_USE_ESP32_EMAC +eth_esp32_emac_config_t eth_esp32_emac_default_config(void) { + return (eth_esp32_emac_config_t) ETH_ESP32_EMAC_DEFAULT_CONFIG(); +} +#endif +#endif // USE_ESP32 diff --git a/esphome/components/ethernet_info/ethernet_info_text_sensor.cpp b/esphome/components/ethernet_info/ethernet_info_text_sensor.cpp index 72ce9c86e2..15ef6a1f20 100644 --- a/esphome/components/ethernet_info/ethernet_info_text_sensor.cpp +++ b/esphome/components/ethernet_info/ethernet_info_text_sensor.cpp @@ -1,7 +1,7 @@ #include "ethernet_info_text_sensor.h" #include "esphome/core/log.h" -#ifdef USE_ESP32 +#ifdef USE_ETHERNET namespace esphome::ethernet_info { @@ -49,4 +49,4 @@ void MACAddressEthernetInfo::dump_config() { LOG_TEXT_SENSOR("", "EthernetInfo M } // namespace esphome::ethernet_info -#endif // USE_ESP32 +#endif // USE_ETHERNET diff --git a/esphome/components/ethernet_info/ethernet_info_text_sensor.h b/esphome/components/ethernet_info/ethernet_info_text_sensor.h index 912a39a83f..11002d51ba 100644 --- a/esphome/components/ethernet_info/ethernet_info_text_sensor.h +++ b/esphome/components/ethernet_info/ethernet_info_text_sensor.h @@ -4,7 +4,7 @@ #include "esphome/components/text_sensor/text_sensor.h" #include "esphome/components/ethernet/ethernet_component.h" -#ifdef USE_ESP32 +#ifdef USE_ETHERNET namespace esphome::ethernet_info { @@ -50,4 +50,4 @@ class MACAddressEthernetInfo final : public Component, public text_sensor::TextS } // namespace esphome::ethernet_info -#endif // USE_ESP32 +#endif // USE_ETHERNET diff --git a/esphome/components/event/__init__.py b/esphome/components/event/__init__.py index 8fac7a279c..527bb4ebba 100644 --- a/esphome/components/event/__init__.py +++ b/esphome/components/event/__init__.py @@ -10,7 +10,6 @@ from esphome.const import ( CONF_ID, CONF_MQTT_ID, CONF_ON_EVENT, - CONF_TRIGGER_ID, CONF_WEB_SERVER, DEVICE_CLASS_BUTTON, DEVICE_CLASS_DOORBELL, @@ -18,7 +17,11 @@ from esphome.const import ( DEVICE_CLASS_MOTION, ) from esphome.core import CORE, CoroPriority, coroutine_with_priority -from esphome.core.entity_helpers import entity_duplicate_validator, setup_entity +from esphome.core.entity_helpers import ( + entity_duplicate_validator, + setup_device_class, + setup_entity, +) from esphome.cpp_generator import MockObjClass CODEOWNERS = ["@nohat"] @@ -37,8 +40,6 @@ EventPtr = Event.operator("ptr") TriggerEventAction = event_ns.class_("TriggerEventAction", automation.Action) -EventTrigger = event_ns.class_("EventTrigger", automation.Trigger.template()) - validate_device_class = cv.one_of(*DEVICE_CLASSES, lower=True, space="_") _EVENT_SCHEMA = ( @@ -49,11 +50,7 @@ _EVENT_SCHEMA = ( cv.OnlyWith(CONF_MQTT_ID, "mqtt"): cv.declare_id(mqtt.MQTTEventComponent), cv.GenerateID(): cv.declare_id(Event), cv.Optional(CONF_DEVICE_CLASS): validate_device_class, - cv.Optional(CONF_ON_EVENT): automation.validate_automation( - { - cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(EventTrigger), - } - ), + cv.Optional(CONF_ON_EVENT): automation.validate_automation({}), } ) ) @@ -85,17 +82,16 @@ def event_schema( return _EVENT_SCHEMA.extend(schema) +@setup_entity("event") async def setup_event_core_(var, config, *, event_types: list[str]): - await setup_entity(var, config, "event") - for conf in config.get(CONF_ON_EVENT, []): - trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) - await automation.build_automation(trigger, [(cg.StringRef, "event_type")], conf) + await automation.build_callback_automation( + var, "add_on_event_callback", [(cg.StringRef, "event_type")], conf + ) cg.add(var.set_event_types(event_types)) - if (device_class := config.get(CONF_DEVICE_CLASS)) is not None: - cg.add(var.set_device_class(device_class)) + setup_device_class(config) if mqtt_id := config.get(CONF_MQTT_ID): mqtt_ = cg.new_Pvariable(mqtt_id, var) @@ -127,7 +123,9 @@ TRIGGER_EVENT_SCHEMA = cv.Schema( ) -@automation.register_action("event.trigger", TriggerEventAction, TRIGGER_EVENT_SCHEMA) +@automation.register_action( + "event.trigger", TriggerEventAction, TRIGGER_EVENT_SCHEMA, synchronous=True +) async def event_fire_to_code(config, action_id, template_arg, args): var = cg.new_Pvariable(action_id, template_arg) await cg.register_parented(var, config[CONF_ID]) diff --git a/esphome/components/event/event.cpp b/esphome/components/event/event.cpp index 667d4218f3..a5d64a2748 100644 --- a/esphome/components/event/event.cpp +++ b/esphome/components/event/event.cpp @@ -22,7 +22,7 @@ void Event::trigger(const std::string &event_type) { return; } this->last_event_type_ = found; - ESP_LOGD(TAG, "'%s' >> '%s'", this->get_name().c_str(), this->last_event_type_); + ESP_LOGV(TAG, "'%s' >> '%s'", this->get_name().c_str(), this->last_event_type_); this->event_callback_.call(StringRef(found)); #if defined(USE_EVENT) && defined(USE_CONTROLLER_REGISTRY) ControllerRegistry::notify_event(this); @@ -45,9 +45,5 @@ void Event::set_event_types(const std::vector &event_types) { this->last_event_type_ = nullptr; // Reset when types change } -void Event::add_on_event_callback(std::function &&callback) { - this->event_callback_.add(std::move(callback)); -} - } // namespace event } // namespace esphome diff --git a/esphome/components/event/event.h b/esphome/components/event/event.h index a7451407bb..ebbee0bfe2 100644 --- a/esphome/components/event/event.h +++ b/esphome/components/event/event.h @@ -20,7 +20,7 @@ namespace event { LOG_ENTITY_DEVICE_CLASS(TAG, prefix, *(obj)); \ } -class Event : public EntityBase, public EntityBase_DeviceClass { +class Event : public EntityBase { public: void trigger(const std::string &event_type); @@ -66,7 +66,9 @@ class Event : public EntityBase, public EntityBase_DeviceClass { /// Check if an event has been triggered. bool has_event() const { return this->last_event_type_ != nullptr; } - void add_on_event_callback(std::function &&callback); + template void add_on_event_callback(F &&callback) { + this->event_callback_.add(std::forward(callback)); + } protected: LazyCallbackManager event_callback_; diff --git a/esphome/components/ezo/automation.h b/esphome/components/ezo/automation.h deleted file mode 100644 index a4a6fa3014..0000000000 --- a/esphome/components/ezo/automation.h +++ /dev/null @@ -1,53 +0,0 @@ -#pragma once -#include - -#include "esphome/core/automation.h" -#include "ezo.h" - -namespace esphome { -namespace ezo { - -class LedTrigger : public Trigger { - public: - explicit LedTrigger(EZOSensor *ezo) { - ezo->add_led_state_callback([this](bool value) { this->trigger(value); }); - } -}; - -class CustomTrigger : public Trigger { - public: - explicit CustomTrigger(EZOSensor *ezo) { - ezo->add_custom_callback([this](const std::string &value) { this->trigger(value); }); - } -}; - -class TTrigger : public Trigger { - public: - explicit TTrigger(EZOSensor *ezo) { - ezo->add_t_callback([this](const std::string &value) { this->trigger(value); }); - } -}; - -class CalibrationTrigger : public Trigger { - public: - explicit CalibrationTrigger(EZOSensor *ezo) { - ezo->add_calibration_callback([this](const std::string &value) { this->trigger(value); }); - } -}; - -class SlopeTrigger : public Trigger { - public: - explicit SlopeTrigger(EZOSensor *ezo) { - ezo->add_slope_callback([this](const std::string &value) { this->trigger(value); }); - } -}; - -class DeviceInformationTrigger : public Trigger { - public: - explicit DeviceInformationTrigger(EZOSensor *ezo) { - ezo->add_device_infomation_callback([this](const std::string &value) { this->trigger(value); }); - } -}; - -} // namespace ezo -} // namespace esphome diff --git a/esphome/components/ezo/ezo.cpp b/esphome/components/ezo/ezo.cpp index e4036021df..2dc65b7d14 100644 --- a/esphome/components/ezo/ezo.cpp +++ b/esphome/components/ezo/ezo.cpp @@ -66,8 +66,9 @@ void EZOSensor::loop() { if (to_run->command_type == EzoCommandType::EZO_SLEEP || to_run->command_type == EzoCommandType::EZO_I2C) { // Commands with no return data + bool update_address = to_run->command_type == EzoCommandType::EZO_I2C; this->commands_.pop_front(); - if (to_run->command_type == EzoCommandType::EZO_I2C) + if (update_address) this->address_ = this->new_address_; return; } diff --git a/esphome/components/ezo/ezo.h b/esphome/components/ezo/ezo.h index f1a2802cbd..d80869fbd9 100644 --- a/esphome/components/ezo/ezo.h +++ b/esphome/components/ezo/ezo.h @@ -44,8 +44,8 @@ class EZOSensor : public sensor::Sensor, public PollingComponent, public i2c::I2 // Device Information void get_device_information(); - void add_device_infomation_callback(std::function &&callback) { - this->device_infomation_callback_.add(std::move(callback)); + template void add_device_infomation_callback(F &&callback) { + this->device_infomation_callback_.add(std::forward(callback)); } // Sleep @@ -56,15 +56,13 @@ class EZOSensor : public sensor::Sensor, public PollingComponent, public i2c::I2 // Slope void get_slope(); - void add_slope_callback(std::function &&callback) { - this->slope_callback_.add(std::move(callback)); - } + template void add_slope_callback(F &&callback) { this->slope_callback_.add(std::forward(callback)); } // T void get_t(); void set_t(float value); void set_tempcomp_value(float temp); // For backwards compatibility - void add_t_callback(std::function &&callback) { this->t_callback_.add(std::move(callback)); } + template void add_t_callback(F &&callback) { this->t_callback_.add(std::forward(callback)); } // Calibration void get_calibration(); @@ -73,20 +71,18 @@ class EZOSensor : public sensor::Sensor, public PollingComponent, public i2c::I2 void set_calibration_point_high(float value); void set_calibration_generic(float value); void clear_calibration(); - void add_calibration_callback(std::function &&callback) { - this->calibration_callback_.add(std::move(callback)); + template void add_calibration_callback(F &&callback) { + this->calibration_callback_.add(std::forward(callback)); } // LED void get_led_state(); void set_led_state(bool on); - void add_led_state_callback(std::function &&callback) { this->led_callback_.add(std::move(callback)); } + template void add_led_state_callback(F &&callback) { this->led_callback_.add(std::forward(callback)); } // Custom void send_custom(const std::string &to_send); - void add_custom_callback(std::function &&callback) { - this->custom_callback_.add(std::move(callback)); - } + template void add_custom_callback(F &&callback) { this->custom_callback_.add(std::forward(callback)); } protected: std::deque> commands_; diff --git a/esphome/components/ezo/sensor.py b/esphome/components/ezo/sensor.py index cf240faec3..7c81f9c848 100644 --- a/esphome/components/ezo/sensor.py +++ b/esphome/components/ezo/sensor.py @@ -2,7 +2,7 @@ from esphome import automation import esphome.codegen as cg from esphome.components import i2c, sensor import esphome.config_validation as cv -from esphome.const import CONF_ID, CONF_TRIGGER_ID +from esphome.const import CONF_ID CODEOWNERS = ["@ssieb"] @@ -21,61 +21,16 @@ EZOSensor = ezo_ns.class_( "EZOSensor", sensor.Sensor, cg.PollingComponent, i2c.I2CDevice ) -CustomTrigger = ezo_ns.class_( - "CustomTrigger", automation.Trigger.template(cg.std_string) -) - - -TTrigger = ezo_ns.class_("TTrigger", automation.Trigger.template(cg.std_string)) - -SlopeTrigger = ezo_ns.class_("SlopeTrigger", automation.Trigger.template(cg.std_string)) - -CalibrationTrigger = ezo_ns.class_( - "CalibrationTrigger", automation.Trigger.template(cg.std_string) -) - -DeviceInformationTrigger = ezo_ns.class_( - "DeviceInformationTrigger", automation.Trigger.template(cg.std_string) -) - -LedTrigger = ezo_ns.class_("LedTrigger", automation.Trigger.template(cg.bool_)) - CONFIG_SCHEMA = ( sensor.sensor_schema(EZOSensor) .extend( { - cv.Optional(CONF_ON_CUSTOM): automation.validate_automation( - { - cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(CustomTrigger), - } - ), - cv.Optional(CONF_ON_CALIBRATION): automation.validate_automation( - { - cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(CalibrationTrigger), - } - ), - cv.Optional(CONF_ON_SLOPE): automation.validate_automation( - { - cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(SlopeTrigger), - } - ), - cv.Optional(CONF_ON_T): automation.validate_automation( - { - cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(TTrigger), - } - ), - cv.Optional(CONF_ON_DEVICE_INFORMATION): automation.validate_automation( - { - cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id( - DeviceInformationTrigger - ), - } - ), - cv.Optional(CONF_ON_LED): automation.validate_automation( - { - cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(LedTrigger), - } - ), + cv.Optional(CONF_ON_CUSTOM): automation.validate_automation({}), + cv.Optional(CONF_ON_CALIBRATION): automation.validate_automation({}), + cv.Optional(CONF_ON_SLOPE): automation.validate_automation({}), + cv.Optional(CONF_ON_T): automation.validate_automation({}), + cv.Optional(CONF_ON_DEVICE_INFORMATION): automation.validate_automation({}), + cv.Optional(CONF_ON_LED): automation.validate_automation({}), } ) .extend(cv.polling_component_schema("60s")) @@ -90,25 +45,26 @@ async def to_code(config): await i2c.register_i2c_device(var, config) for conf in config.get(CONF_ON_CUSTOM, []): - trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) - await automation.build_automation(trigger, [(cg.std_string, "x")], conf) - + await automation.build_callback_automation( + var, "add_custom_callback", [(cg.std_string, "x")], conf + ) for conf in config.get(CONF_ON_LED, []): - trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) - await automation.build_automation(trigger, [(bool, "x")], conf) - + await automation.build_callback_automation( + var, "add_led_state_callback", [(bool, "x")], conf + ) for conf in config.get(CONF_ON_DEVICE_INFORMATION, []): - trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) - await automation.build_automation(trigger, [(cg.std_string, "x")], conf) - + await automation.build_callback_automation( + var, "add_device_infomation_callback", [(cg.std_string, "x")], conf + ) for conf in config.get(CONF_ON_SLOPE, []): - trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) - await automation.build_automation(trigger, [(cg.std_string, "x")], conf) - + await automation.build_callback_automation( + var, "add_slope_callback", [(cg.std_string, "x")], conf + ) for conf in config.get(CONF_ON_CALIBRATION, []): - trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) - await automation.build_automation(trigger, [(cg.std_string, "x")], conf) - + await automation.build_callback_automation( + var, "add_calibration_callback", [(cg.std_string, "x")], conf + ) for conf in config.get(CONF_ON_T, []): - trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) - await automation.build_automation(trigger, [(cg.std_string, "x")], conf) + await automation.build_callback_automation( + var, "add_t_callback", [(cg.std_string, "x")], conf + ) diff --git a/esphome/components/ezo_pmp/__init__.py b/esphome/components/ezo_pmp/__init__.py index c1f72bb05d..3de796dd25 100644 --- a/esphome/components/ezo_pmp/__init__.py +++ b/esphome/components/ezo_pmp/__init__.py @@ -81,7 +81,10 @@ EzoPMPArbitraryCommandAction = ezo_pmp_ns.class_( @automation.register_action( - "ezo_pmp.find", EzoPMPFindAction, EZO_PMP_NO_ARGS_ACTION_SCHEMA + "ezo_pmp.find", + EzoPMPFindAction, + EZO_PMP_NO_ARGS_ACTION_SCHEMA, + synchronous=True, ) async def ezo_pmp_find_to_code(config, action_id, template_arg, args): paren = await cg.get_variable(config[CONF_ID]) @@ -92,6 +95,7 @@ async def ezo_pmp_find_to_code(config, action_id, template_arg, args): "ezo_pmp.dose_continuously", EzoPMPDoseContinuouslyAction, EZO_PMP_NO_ARGS_ACTION_SCHEMA, + synchronous=True, ) async def ezo_pmp_dose_continuously_to_code(config, action_id, template_arg, args): paren = await cg.get_variable(config[CONF_ID]) @@ -102,6 +106,7 @@ async def ezo_pmp_dose_continuously_to_code(config, action_id, template_arg, arg "ezo_pmp.clear_total_volume_dosed", EzoPMPClearTotalVolumeDispensedAction, EZO_PMP_NO_ARGS_ACTION_SCHEMA, + synchronous=True, ) async def ezo_pmp_clear_total_volume_dosed_to_code( config, action_id, template_arg, args @@ -114,6 +119,7 @@ async def ezo_pmp_clear_total_volume_dosed_to_code( "ezo_pmp.clear_calibration", EzoPMPClearCalibrationAction, EZO_PMP_NO_ARGS_ACTION_SCHEMA, + synchronous=True, ) async def ezo_pmp_clear_calibration_to_code(config, action_id, template_arg, args): paren = await cg.get_variable(config[CONF_ID]) @@ -121,7 +127,10 @@ async def ezo_pmp_clear_calibration_to_code(config, action_id, template_arg, arg @automation.register_action( - "ezo_pmp.pause_dosing", EzoPMPPauseDosingAction, EZO_PMP_NO_ARGS_ACTION_SCHEMA + "ezo_pmp.pause_dosing", + EzoPMPPauseDosingAction, + EZO_PMP_NO_ARGS_ACTION_SCHEMA, + synchronous=True, ) async def ezo_pmp_pause_dosing_to_code(config, action_id, template_arg, args): paren = await cg.get_variable(config[CONF_ID]) @@ -129,7 +138,10 @@ async def ezo_pmp_pause_dosing_to_code(config, action_id, template_arg, args): @automation.register_action( - "ezo_pmp.stop_dosing", EzoPMPStopDosingAction, EZO_PMP_NO_ARGS_ACTION_SCHEMA + "ezo_pmp.stop_dosing", + EzoPMPStopDosingAction, + EZO_PMP_NO_ARGS_ACTION_SCHEMA, + synchronous=True, ) async def ezo_pmp_stop_dosing_to_code(config, action_id, template_arg, args): paren = await cg.get_variable(config[CONF_ID]) @@ -149,7 +161,10 @@ EZO_PMP_DOSE_VOLUME_ACTION_SCHEMA = cv.All( @automation.register_action( - "ezo_pmp.dose_volume", EzoPMPDoseVolumeAction, EZO_PMP_DOSE_VOLUME_ACTION_SCHEMA + "ezo_pmp.dose_volume", + EzoPMPDoseVolumeAction, + EZO_PMP_DOSE_VOLUME_ACTION_SCHEMA, + synchronous=True, ) async def ezo_pmp_dose_volume_to_code(config, action_id, template_arg, args): paren = await cg.get_variable(config[CONF_ID]) @@ -178,6 +193,7 @@ EZO_PMP_DOSE_VOLUME_OVER_TIME_ACTION_SCHEMA = cv.All( "ezo_pmp.dose_volume_over_time", EzoPMPDoseVolumeOverTimeAction, EZO_PMP_DOSE_VOLUME_OVER_TIME_ACTION_SCHEMA, + synchronous=True, ) async def ezo_pmp_dose_volume_over_time_to_code(config, action_id, template_arg, args): paren = await cg.get_variable(config[CONF_ID]) @@ -209,6 +225,7 @@ EZO_PMP_DOSE_WITH_CONSTANT_FLOW_RATE_ACTION_SCHEMA = cv.All( "ezo_pmp.dose_with_constant_flow_rate", EzoPMPDoseWithConstantFlowRateAction, EZO_PMP_DOSE_WITH_CONSTANT_FLOW_RATE_ACTION_SCHEMA, + synchronous=True, ) async def ezo_pmp_dose_with_constant_flow_rate_to_code( config, action_id, template_arg, args @@ -239,6 +256,7 @@ EZO_PMP_SET_CALIBRATION_VOLUME_ACTION_SCHEMA = cv.All( "ezo_pmp.set_calibration_volume", EzoPMPSetCalibrationVolumeAction, EZO_PMP_SET_CALIBRATION_VOLUME_ACTION_SCHEMA, + synchronous=True, ) async def ezo_pmp_set_calibration_volume_to_code(config, action_id, template_arg, args): paren = await cg.get_variable(config[CONF_ID]) @@ -262,12 +280,13 @@ EZO_PMP_CHANGE_I2C_ADDRESS_ACTION_SCHEMA = cv.All( "ezo_pmp.change_i2c_address", EzoPMPChangeI2CAddressAction, EZO_PMP_CHANGE_I2C_ADDRESS_ACTION_SCHEMA, + synchronous=True, ) async def ezo_pmp_change_i2c_address_to_code(config, action_id, template_arg, args): paren = await cg.get_variable(config[CONF_ID]) var = cg.new_Pvariable(action_id, template_arg, paren) - template_ = await cg.templatable(config[CONF_ADDRESS], args, cg.double) + template_ = await cg.templatable(config[CONF_ADDRESS], args, cg.int_) cg.add(var.set_address(template_)) return var @@ -285,6 +304,7 @@ EZO_PMP_ARBITRARY_COMMAND_ACTION_SCHEMA = cv.All( "ezo_pmp.arbitrary_command", EzoPMPArbitraryCommandAction, EZO_PMP_ARBITRARY_COMMAND_ACTION_SCHEMA, + synchronous=True, ) async def ezo_pmp_arbitrary_command_to_code(config, action_id, template_arg, args): paren = await cg.get_variable(config[CONF_ID]) diff --git a/esphome/components/ezo_pmp/ezo_pmp.cpp b/esphome/components/ezo_pmp/ezo_pmp.cpp index 9d2f4fc687..4ce4da57ff 100644 --- a/esphome/components/ezo_pmp/ezo_pmp.cpp +++ b/esphome/components/ezo_pmp/ezo_pmp.cpp @@ -150,9 +150,9 @@ void EzoPMP::read_command_result_() { if (current_char == '\0') { ESP_LOGV(TAG, "Read Response from device: %s\n" - "First Component: %s\n" - "Second Component: %s\n" - "Third Component: %s", + " First Component: %s\n" + " Second Component: %s\n" + " Third Component: %s", (char *) response_buffer, (char *) first_parameter_buffer, (char *) second_parameter_buffer, (char *) third_parameter_buffer); @@ -165,22 +165,23 @@ void EzoPMP::read_command_result_() { continue; } - switch (current_parameter) { - case 1: - first_parameter_buffer[position_in_parameter_buffer] = current_char; - first_parameter_buffer[position_in_parameter_buffer + 1] = '\0'; - break; - case 2: - second_parameter_buffer[position_in_parameter_buffer] = current_char; - second_parameter_buffer[position_in_parameter_buffer + 1] = '\0'; - break; - case 3: - third_parameter_buffer[position_in_parameter_buffer] = current_char; - third_parameter_buffer[position_in_parameter_buffer + 1] = '\0'; - break; + if (position_in_parameter_buffer < sizeof(first_parameter_buffer) - 1) { + switch (current_parameter) { + case 1: + first_parameter_buffer[position_in_parameter_buffer] = current_char; + first_parameter_buffer[position_in_parameter_buffer + 1] = '\0'; + break; + case 2: + second_parameter_buffer[position_in_parameter_buffer] = current_char; + second_parameter_buffer[position_in_parameter_buffer + 1] = '\0'; + break; + case 3: + third_parameter_buffer[position_in_parameter_buffer] = current_char; + third_parameter_buffer[position_in_parameter_buffer + 1] = '\0'; + break; + } + position_in_parameter_buffer++; } - - position_in_parameter_buffer++; } auto parsed_first_parameter = parse_number(first_parameter_buffer); @@ -404,7 +405,8 @@ void EzoPMP::send_next_command_() { break; case EZO_PMP_COMMAND_EXEC_ARBITRARY_COMMAND_ADDRESS: // Run an arbitrary command - command_buffer_length = snprintf((char *) command_buffer, sizeof(command_buffer), "%s", this->arbitrary_command_); + command_buffer_length = + snprintf((char *) command_buffer, sizeof(command_buffer), "%s", this->arbitrary_command_.c_str()); ESP_LOGI(TAG, "Sending arbitrary command: %s", (char *) command_buffer); break; @@ -541,7 +543,7 @@ void EzoPMP::change_i2c_address(int address) { } void EzoPMP::exec_arbitrary_command(const std::basic_string &command) { - this->arbitrary_command_ = command.c_str(); + this->arbitrary_command_ = command; this->queue_command_(EZO_PMP_COMMAND_EXEC_ARBITRARY_COMMAND_ADDRESS, 0, 0, true); } diff --git a/esphome/components/ezo_pmp/ezo_pmp.h b/esphome/components/ezo_pmp/ezo_pmp.h index d4917e7f4b..bbfd899170 100644 --- a/esphome/components/ezo_pmp/ezo_pmp.h +++ b/esphome/components/ezo_pmp/ezo_pmp.h @@ -85,7 +85,7 @@ class EzoPMP : public PollingComponent, public i2c::I2CDevice { bool is_paused_flag_ = false; bool is_dosing_flag_ = false; - const char *arbitrary_command_{nullptr}; + std::string arbitrary_command_{}; void send_next_command_(); void read_command_result_(); diff --git a/esphome/components/factory_reset/__init__.py b/esphome/components/factory_reset/__init__.py index 5784d09ce6..20b191a2b7 100644 --- a/esphome/components/factory_reset/__init__.py +++ b/esphome/components/factory_reset/__init__.py @@ -1,10 +1,9 @@ -from esphome.automation import Trigger, build_automation, validate_automation +from esphome import automation import esphome.codegen as cg from esphome.components.esp8266 import CONF_RESTORE_FROM_FLASH, KEY_ESP8266 import esphome.config_validation as cv from esphome.const import ( CONF_ID, - CONF_TRIGGER_ID, PLATFORM_BK72XX, PLATFORM_ESP32, PLATFORM_ESP8266, @@ -18,7 +17,6 @@ CODEOWNERS = ["@anatoly-savchenkov"] factory_reset_ns = cg.esphome_ns.namespace("factory_reset") FactoryResetComponent = factory_reset_ns.class_("FactoryResetComponent", cg.Component) -FastBootTrigger = factory_reset_ns.class_("FastBootTrigger", Trigger, cg.Component) CONF_MAX_DELAY = "max_delay" CONF_RESETS_REQUIRED = "resets_required" @@ -55,11 +53,7 @@ CONFIG_SCHEMA = cv.All( ), ), cv.Optional(CONF_RESETS_REQUIRED): cv.positive_not_null_int, - cv.Optional(CONF_ON_INCREMENT): validate_automation( - { - cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(FastBootTrigger), - } - ), + cv.Optional(CONF_ON_INCREMENT): automation.validate_automation({}), } ).extend(cv.COMPONENT_SCHEMA), _validate, @@ -88,12 +82,9 @@ async def to_code(config): ) await cg.register_component(var, config) for conf in config.get(CONF_ON_INCREMENT, []): - trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) - await build_automation( - trigger, - [ - (cg.uint8, "x"), - (cg.uint8, "target"), - ], + await automation.build_callback_automation( + var, + "add_increment_callback", + [(cg.uint8, "x"), (cg.uint8, "target")], conf, ) diff --git a/esphome/components/factory_reset/factory_reset.h b/esphome/components/factory_reset/factory_reset.h index 990bb2edb6..41ee627c4b 100644 --- a/esphome/components/factory_reset/factory_reset.h +++ b/esphome/components/factory_reset/factory_reset.h @@ -17,8 +17,8 @@ class FactoryResetComponent : public Component { void dump_config() override; void setup() override; - void add_increment_callback(std::function &&callback) { - this->increment_callback_.add(std::move(callback)); + template void add_increment_callback(F &&callback) { + this->increment_callback_.add(std::forward(callback)); } protected: @@ -30,12 +30,6 @@ class FactoryResetComponent : public Component { uint8_t required_count_; // The number of boot attempts before fast boot is enabled }; -class FastBootTrigger : public Trigger { - public: - explicit FastBootTrigger(FactoryResetComponent *parent) { - parent->add_increment_callback([this](uint8_t current, uint8_t target) { this->trigger(current, target); }); - } -}; } // namespace esphome::factory_reset #endif // !defined(USE_RP2040) && !defined(USE_HOST) diff --git a/esphome/components/fan/__init__.py b/esphome/components/fan/__init__.py index 6010aa8ed4..df71c6ab3f 100644 --- a/esphome/components/fan/__init__.py +++ b/esphome/components/fan/__init__.py @@ -222,9 +222,8 @@ def validate_preset_modes(value): return value +@setup_entity("fan") async def setup_fan_core_(var, config): - await setup_entity(var, config, "fan") - cg.add(var.set_restore_mode(config[CONF_RESTORE_MODE])) if (mqtt_id := config.get(CONF_MQTT_ID)) is not None: @@ -311,13 +310,17 @@ FAN_ACTION_SCHEMA = maybe_simple_id( ) -@automation.register_action("fan.toggle", ToggleAction, FAN_ACTION_SCHEMA) +@automation.register_action( + "fan.toggle", ToggleAction, FAN_ACTION_SCHEMA, synchronous=True +) async def fan_toggle_to_code(config, action_id, template_arg, args): paren = await cg.get_variable(config[CONF_ID]) return cg.new_Pvariable(action_id, template_arg, paren) -@automation.register_action("fan.turn_off", TurnOffAction, FAN_ACTION_SCHEMA) +@automation.register_action( + "fan.turn_off", TurnOffAction, FAN_ACTION_SCHEMA, synchronous=True +) async def fan_turn_off_to_code(config, action_id, template_arg, args): paren = await cg.get_variable(config[CONF_ID]) return cg.new_Pvariable(action_id, template_arg, paren) @@ -336,6 +339,7 @@ async def fan_turn_off_to_code(config, action_id, template_arg, args): ), } ), + synchronous=True, ) async def fan_turn_on_to_code(config, action_id, template_arg, args): paren = await cg.get_variable(config[CONF_ID]) @@ -361,6 +365,7 @@ async def fan_turn_on_to_code(config, action_id, template_arg, args): cv.Optional(CONF_OFF_SPEED_CYCLE, default=True): cv.boolean, } ), + synchronous=True, ) async def fan_cycle_speed_to_code(config, action_id, template_arg, args): paren = await cg.get_variable(config[CONF_ID]) diff --git a/esphome/components/fan/automation.h b/esphome/components/fan/automation.h index 3c3b0ce519..3ee6f89e55 100644 --- a/esphome/components/fan/automation.h +++ b/esphome/components/fan/automation.h @@ -113,16 +113,19 @@ template class FanIsOffCondition : public Condition { class FanStateTrigger : public Trigger { public: - FanStateTrigger(Fan *state) { - state->add_on_state_callback([this, state]() { this->trigger(state); }); + FanStateTrigger(Fan *state) : fan_(state) { + state->add_on_state_callback([this]() { this->trigger(this->fan_); }); } + + protected: + Fan *fan_; }; class FanTurnOnTrigger : public Trigger<> { public: - FanTurnOnTrigger(Fan *state) { - state->add_on_state_callback([this, state]() { - auto is_on = state->state; + FanTurnOnTrigger(Fan *state) : fan_(state) { + state->add_on_state_callback([this]() { + auto is_on = this->fan_->state; auto should_trigger = is_on && !this->last_on_; this->last_on_ = is_on; if (should_trigger) { @@ -133,14 +136,15 @@ class FanTurnOnTrigger : public Trigger<> { } protected: + Fan *fan_; bool last_on_; }; class FanTurnOffTrigger : public Trigger<> { public: - FanTurnOffTrigger(Fan *state) { - state->add_on_state_callback([this, state]() { - auto is_on = state->state; + FanTurnOffTrigger(Fan *state) : fan_(state) { + state->add_on_state_callback([this]() { + auto is_on = this->fan_->state; auto should_trigger = !is_on && this->last_on_; this->last_on_ = is_on; if (should_trigger) { @@ -151,14 +155,15 @@ class FanTurnOffTrigger : public Trigger<> { } protected: + Fan *fan_; bool last_on_; }; class FanDirectionSetTrigger : public Trigger { public: - FanDirectionSetTrigger(Fan *state) { - state->add_on_state_callback([this, state]() { - auto direction = state->direction; + FanDirectionSetTrigger(Fan *state) : fan_(state) { + state->add_on_state_callback([this]() { + auto direction = this->fan_->direction; auto should_trigger = direction != this->last_direction_; this->last_direction_ = direction; if (should_trigger) { @@ -169,14 +174,15 @@ class FanDirectionSetTrigger : public Trigger { } protected: + Fan *fan_; FanDirection last_direction_; }; class FanOscillatingSetTrigger : public Trigger { public: - FanOscillatingSetTrigger(Fan *state) { - state->add_on_state_callback([this, state]() { - auto oscillating = state->oscillating; + FanOscillatingSetTrigger(Fan *state) : fan_(state) { + state->add_on_state_callback([this]() { + auto oscillating = this->fan_->oscillating; auto should_trigger = oscillating != this->last_oscillating_; this->last_oscillating_ = oscillating; if (should_trigger) { @@ -187,14 +193,15 @@ class FanOscillatingSetTrigger : public Trigger { } protected: + Fan *fan_; bool last_oscillating_; }; class FanSpeedSetTrigger : public Trigger { public: - FanSpeedSetTrigger(Fan *state) { - state->add_on_state_callback([this, state]() { - auto speed = state->speed; + FanSpeedSetTrigger(Fan *state) : fan_(state) { + state->add_on_state_callback([this]() { + auto speed = this->fan_->speed; auto should_trigger = speed != this->last_speed_; this->last_speed_ = speed; if (should_trigger) { @@ -205,14 +212,15 @@ class FanSpeedSetTrigger : public Trigger { } protected: + Fan *fan_; int last_speed_; }; class FanPresetSetTrigger : public Trigger { public: - FanPresetSetTrigger(Fan *state) { - state->add_on_state_callback([this, state]() { - auto preset_mode = state->get_preset_mode(); + FanPresetSetTrigger(Fan *state) : fan_(state) { + state->add_on_state_callback([this]() { + auto preset_mode = this->fan_->get_preset_mode(); auto should_trigger = preset_mode != this->last_preset_mode_; this->last_preset_mode_ = preset_mode; if (should_trigger) { @@ -223,6 +231,7 @@ class FanPresetSetTrigger : public Trigger { } protected: + Fan *fan_; StringRef last_preset_mode_{}; }; diff --git a/esphome/components/fan/fan.cpp b/esphome/components/fan/fan.cpp index a983babe1c..dc7a75018c 100644 --- a/esphome/components/fan/fan.cpp +++ b/esphome/components/fan/fan.cpp @@ -2,21 +2,18 @@ #include "esphome/core/defines.h" #include "esphome/core/controller_registry.h" #include "esphome/core/log.h" +#include "esphome/core/progmem.h" namespace esphome { namespace fan { static const char *const TAG = "fan"; +// Fan direction strings indexed by FanDirection enum (0-1): FORWARD, REVERSE, plus UNKNOWN +PROGMEM_STRING_TABLE(FanDirectionStrings, "FORWARD", "REVERSE", "UNKNOWN"); + const LogString *fan_direction_to_string(FanDirection direction) { - switch (direction) { - case FanDirection::FORWARD: - return LOG_STR("FORWARD"); - case FanDirection::REVERSE: - return LOG_STR("REVERSE"); - default: - return LOG_STR("UNKNOWN"); - } + return FanDirectionStrings::get_log_str(static_cast(direction), FanDirectionStrings::LAST_INDEX); } FanCall &FanCall::set_preset_mode(const std::string &preset_mode) { @@ -47,22 +44,22 @@ FanCall &FanCall::set_preset_mode(const char *preset_mode, size_t len) { } void FanCall::perform() { - ESP_LOGD(TAG, "'%s' - Setting:", this->parent_.get_name().c_str()); + ESP_LOGV(TAG, "'%s' - Setting:", this->parent_.get_name().c_str()); this->validate_(); if (this->binary_state_.has_value()) { - ESP_LOGD(TAG, " State: %s", ONOFF(*this->binary_state_)); + ESP_LOGV(TAG, " State: %s", ONOFF(*this->binary_state_)); } if (this->oscillating_.has_value()) { - ESP_LOGD(TAG, " Oscillating: %s", YESNO(*this->oscillating_)); + ESP_LOGV(TAG, " Oscillating: %s", YESNO(*this->oscillating_)); } if (this->speed_.has_value()) { - ESP_LOGD(TAG, " Speed: %d", *this->speed_); + ESP_LOGV(TAG, " Speed: %d", *this->speed_); } if (this->direction_.has_value()) { - ESP_LOGD(TAG, " Direction: %s", LOG_STR_ARG(fan_direction_to_string(*this->direction_))); + ESP_LOGV(TAG, " Direction: %s", LOG_STR_ARG(fan_direction_to_string(*this->direction_))); } if (this->preset_mode_ != nullptr) { - ESP_LOGD(TAG, " Preset Mode: %s", this->preset_mode_); + ESP_LOGV(TAG, " Preset Mode: %s", this->preset_mode_); } this->parent_.control(*this); } @@ -196,25 +193,24 @@ void Fan::apply_preset_mode_(const FanCall &call) { } } -void Fan::add_on_state_callback(std::function &&callback) { this->state_callback_.add(std::move(callback)); } void Fan::publish_state() { auto traits = this->get_traits(); - ESP_LOGD(TAG, + ESP_LOGV(TAG, "'%s' >>\n" " State: %s", this->name_.c_str(), ONOFF(this->state)); if (traits.supports_speed()) { - ESP_LOGD(TAG, " Speed: %d", this->speed); + ESP_LOGV(TAG, " Speed: %d", this->speed); } if (traits.supports_oscillation()) { - ESP_LOGD(TAG, " Oscillating: %s", YESNO(this->oscillating)); + ESP_LOGV(TAG, " Oscillating: %s", YESNO(this->oscillating)); } if (traits.supports_direction()) { - ESP_LOGD(TAG, " Direction: %s", LOG_STR_ARG(fan_direction_to_string(this->direction))); + ESP_LOGV(TAG, " Direction: %s", LOG_STR_ARG(fan_direction_to_string(this->direction))); } if (this->preset_mode_ != nullptr) { - ESP_LOGD(TAG, " Preset Mode: %s", this->preset_mode_); + ESP_LOGV(TAG, " Preset Mode: %s", this->preset_mode_); } this->state_callback_.call(); #if defined(USE_FAN) && defined(USE_CONTROLLER_REGISTRY) @@ -224,12 +220,17 @@ void Fan::publish_state() { } // Random 32-bit value, change this every time the layout of the FanRestoreState struct changes. -constexpr uint32_t RESTORE_STATE_VERSION = 0x71700ABA; +constexpr uint32_t RESTORE_STATE_VERSION = 0x71700ABB; optional Fan::restore_state_() { FanRestoreState recovered{}; this->rtc_ = this->make_entity_preference(RESTORE_STATE_VERSION); bool restored = this->rtc_.load(&recovered); + if (!restored) { + // No valid saved data; ensure preset_mode sentinel is set + recovered.preset_mode = FanRestoreState::NO_PRESET; + } + switch (this->restore_mode_) { case FanRestoreMode::NO_RESTORE: return {}; @@ -267,6 +268,7 @@ void Fan::save_state_() { state.oscillating = this->oscillating; state.speed = this->speed; state.direction = this->direction; + state.preset_mode = FanRestoreState::NO_PRESET; if (this->has_preset_mode()) { const auto &preset_modes = traits.supported_preset_modes(); diff --git a/esphome/components/fan/fan.h b/esphome/components/fan/fan.h index 55d4ba8825..e7b3681e32 100644 --- a/esphome/components/fan/fan.h +++ b/esphome/components/fan/fan.h @@ -91,11 +91,13 @@ class FanCall { }; struct FanRestoreState { + static constexpr uint8_t NO_PRESET = UINT8_MAX; + bool state; int speed; bool oscillating; FanDirection direction; - uint8_t preset_mode; + uint8_t preset_mode{NO_PRESET}; /// Convert this struct to a fan call that can be performed. FanCall to_call(Fan &fan); @@ -120,7 +122,9 @@ class Fan : public EntityBase { FanCall make_call(); /// Register a callback that will be called each time the state changes. - void add_on_state_callback(std::function &&callback); + template void add_on_state_callback(F &&callback) { + this->state_callback_.add(std::forward(callback)); + } void publish_state(); diff --git a/esphome/components/fastled_base/__init__.py b/esphome/components/fastled_base/__init__.py index 11e8423258..c944e8a930 100644 --- a/esphome/components/fastled_base/__init__.py +++ b/esphome/components/fastled_base/__init__.py @@ -7,6 +7,7 @@ from esphome.const import ( CONF_OUTPUT_ID, CONF_RGB_ORDER, ) +from esphome.core import CORE CODEOWNERS = ["@OttoWinter"] fastled_base_ns = cg.esphome_ns.namespace("fastled_base") @@ -41,5 +42,9 @@ async def new_fastled_light(config): cg.add(var.set_max_refresh_rate(config[CONF_MAX_REFRESH_RATE])) cg.add_library("fastled/FastLED", "3.9.16") + if CORE.is_esp32: + from esphome.components.esp32 import include_builtin_idf_component + + include_builtin_idf_component("esp_lcd") await light.register_light(var, config) return var diff --git a/esphome/components/fastled_base/fastled_light.cpp b/esphome/components/fastled_base/fastled_light.cpp index b3946a34b5..504b8d473e 100644 --- a/esphome/components/fastled_base/fastled_light.cpp +++ b/esphome/components/fastled_base/fastled_light.cpp @@ -21,12 +21,13 @@ void FastLEDLightOutput::dump_config() { "FastLED light:\n" " Num LEDs: %u\n" " Max refresh rate: %u", - this->num_leds_, *this->max_refresh_rate_); + this->num_leds_, this->max_refresh_rate_.value_or(0)); } void FastLEDLightOutput::write_state(light::LightState *state) { // protect from refreshing too often uint32_t now = micros(); - if (*this->max_refresh_rate_ != 0 && (now - this->last_refresh_) < *this->max_refresh_rate_) { + uint32_t max_rate = this->max_refresh_rate_.value_or(0); + if (max_rate != 0 && (now - this->last_refresh_) < max_rate) { // try again next loop iteration, so that this change won't get lost this->schedule_show(); return; diff --git a/esphome/components/feedback/feedback_cover.cpp b/esphome/components/feedback/feedback_cover.cpp index e419ee6229..1dff210cd6 100644 --- a/esphome/components/feedback/feedback_cover.cpp +++ b/esphome/components/feedback/feedback_cover.cpp @@ -269,9 +269,12 @@ void FeedbackCover::control(const CoverCall &call) { this->start_direction_(COVER_OPERATION_CLOSING); } } - } else if (call.get_position().has_value()) { + } else { + auto pos_opt = call.get_position(); + if (!pos_opt.has_value()) + return; // go to position action - auto pos = *call.get_position(); + auto pos = *pos_opt; if (pos == this->position) { // already at target, @@ -335,18 +338,18 @@ void FeedbackCover::start_direction_(CoverOperation dir) { switch (dir) { case COVER_OPERATION_IDLE: - trig = this->stop_trigger_; + trig = &this->stop_trigger_; break; case COVER_OPERATION_OPENING: this->last_operation_ = dir; - trig = this->open_trigger_; + trig = &this->open_trigger_; #ifdef USE_BINARY_SENSOR obstacle = this->open_obstacle_; #endif break; case COVER_OPERATION_CLOSING: this->last_operation_ = dir; - trig = this->close_trigger_; + trig = &this->close_trigger_; #ifdef USE_BINARY_SENSOR obstacle = this->close_obstacle_; #endif @@ -434,10 +437,15 @@ void FeedbackCover::recompute_position_() { } // check if we have an acceleration_wait_time, and remove from position computation - if (now > (this->start_dir_time_ + this->acceleration_wait_time_)) { - this->position += - dir * (now - std::max(this->start_dir_time_ + this->acceleration_wait_time_, this->last_recompute_time_)) / - (action_dur - this->acceleration_wait_time_); + if (now - this->start_dir_time_ > this->acceleration_wait_time_) { + uint32_t accel_end_time = this->start_dir_time_ + this->acceleration_wait_time_; + uint32_t effective_start; + if (static_cast(accel_end_time - this->last_recompute_time_) >= 0) { + effective_start = accel_end_time; + } else { + effective_start = this->last_recompute_time_; + } + this->position += dir * (now - effective_start) / (action_dur - this->acceleration_wait_time_); this->position = clamp(this->position, min_pos, max_pos); } this->last_recompute_time_ = now; diff --git a/esphome/components/feedback/feedback_cover.h b/esphome/components/feedback/feedback_cover.h index 199d3b520a..6be8939413 100644 --- a/esphome/components/feedback/feedback_cover.h +++ b/esphome/components/feedback/feedback_cover.h @@ -17,9 +17,9 @@ class FeedbackCover : public cover::Cover, public Component { void loop() override; void dump_config() override; - Trigger<> *get_open_trigger() const { return this->open_trigger_; } - Trigger<> *get_close_trigger() const { return this->close_trigger_; } - Trigger<> *get_stop_trigger() const { return this->stop_trigger_; } + Trigger<> *get_open_trigger() { return &this->open_trigger_; } + Trigger<> *get_close_trigger() { return &this->close_trigger_; } + Trigger<> *get_stop_trigger() { return &this->stop_trigger_; } #ifdef USE_BINARY_SENSOR void set_open_endstop(binary_sensor::BinarySensor *open_endstop); @@ -61,9 +61,9 @@ class FeedbackCover : public cover::Cover, public Component { binary_sensor::BinarySensor *close_obstacle_{nullptr}; #endif - Trigger<> *open_trigger_{new Trigger<>()}; - Trigger<> *close_trigger_{new Trigger<>()}; - Trigger<> *stop_trigger_{new Trigger<>()}; + Trigger<> open_trigger_; + Trigger<> close_trigger_; + Trigger<> stop_trigger_; uint32_t open_duration_{0}; uint32_t close_duration_{0}; diff --git a/esphome/components/fingerprint_grow/__init__.py b/esphome/components/fingerprint_grow/__init__.py index 115b89433b..0b01ba7cab 100644 --- a/esphome/components/fingerprint_grow/__init__.py +++ b/esphome/components/fingerprint_grow/__init__.py @@ -21,7 +21,6 @@ from esphome.const import ( CONF_SENSING_PIN, CONF_SPEED, CONF_STATE, - CONF_TRIGGER_ID, ) CODEOWNERS = ["@OnFreund", "@loongyh", "@alexborro"] @@ -38,38 +37,6 @@ FingerprintGrowComponent = fingerprint_grow_ns.class_( "FingerprintGrowComponent", cg.PollingComponent, uart.UARTDevice ) -FingerScanStartTrigger = fingerprint_grow_ns.class_( - "FingerScanStartTrigger", automation.Trigger.template() -) - -FingerScanMatchedTrigger = fingerprint_grow_ns.class_( - "FingerScanMatchedTrigger", automation.Trigger.template(cg.uint16, cg.uint16) -) - -FingerScanUnmatchedTrigger = fingerprint_grow_ns.class_( - "FingerScanUnmatchedTrigger", automation.Trigger.template() -) - -FingerScanMisplacedTrigger = fingerprint_grow_ns.class_( - "FingerScanMisplacedTrigger", automation.Trigger.template() -) - -FingerScanInvalidTrigger = fingerprint_grow_ns.class_( - "FingerScanInvalidTrigger", automation.Trigger.template() -) - -EnrollmentScanTrigger = fingerprint_grow_ns.class_( - "EnrollmentScanTrigger", automation.Trigger.template(cg.uint8, cg.uint16) -) - -EnrollmentDoneTrigger = fingerprint_grow_ns.class_( - "EnrollmentDoneTrigger", automation.Trigger.template(cg.uint16) -) - -EnrollmentFailedTrigger = fingerprint_grow_ns.class_( - "EnrollmentFailedTrigger", automation.Trigger.template(cg.uint16) -) - EnrollmentAction = fingerprint_grow_ns.class_("EnrollmentAction", automation.Action) CancelEnrollmentAction = fingerprint_grow_ns.class_( "CancelEnrollmentAction", automation.Action @@ -125,62 +92,22 @@ CONFIG_SCHEMA = cv.All( ): cv.positive_time_period_milliseconds, cv.Optional(CONF_PASSWORD): cv.uint32_t, cv.Optional(CONF_NEW_PASSWORD): cv.uint32_t, - cv.Optional(CONF_ON_FINGER_SCAN_START): automation.validate_automation( - { - cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id( - FingerScanStartTrigger - ), - } - ), + cv.Optional(CONF_ON_FINGER_SCAN_START): automation.validate_automation({}), cv.Optional(CONF_ON_FINGER_SCAN_MATCHED): automation.validate_automation( - { - cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id( - FingerScanMatchedTrigger - ), - } + {} ), cv.Optional(CONF_ON_FINGER_SCAN_UNMATCHED): automation.validate_automation( - { - cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id( - FingerScanUnmatchedTrigger - ), - } + {} ), cv.Optional(CONF_ON_FINGER_SCAN_MISPLACED): automation.validate_automation( - { - cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id( - FingerScanMisplacedTrigger - ), - } + {} ), cv.Optional(CONF_ON_FINGER_SCAN_INVALID): automation.validate_automation( - { - cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id( - FingerScanInvalidTrigger - ), - } - ), - cv.Optional(CONF_ON_ENROLLMENT_SCAN): automation.validate_automation( - { - cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id( - EnrollmentScanTrigger - ), - } - ), - cv.Optional(CONF_ON_ENROLLMENT_DONE): automation.validate_automation( - { - cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id( - EnrollmentDoneTrigger - ), - } - ), - cv.Optional(CONF_ON_ENROLLMENT_FAILED): automation.validate_automation( - { - cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id( - EnrollmentFailedTrigger - ), - } + {} ), + cv.Optional(CONF_ON_ENROLLMENT_SCAN): automation.validate_automation({}), + cv.Optional(CONF_ON_ENROLLMENT_DONE): automation.validate_automation({}), + cv.Optional(CONF_ON_ENROLLMENT_FAILED): automation.validate_automation({}), } ) .extend(cv.polling_component_schema("500ms")) @@ -214,40 +141,43 @@ async def to_code(config): cg.add(var.set_idle_period_to_sleep_ms(idle_period_to_sleep_ms)) for conf in config.get(CONF_ON_FINGER_SCAN_START, []): - trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) - await automation.build_automation(trigger, [], conf) - + await automation.build_callback_automation( + var, "add_on_finger_scan_start_callback", [], conf + ) for conf in config.get(CONF_ON_FINGER_SCAN_MATCHED, []): - trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) - await automation.build_automation( - trigger, [(cg.uint16, "finger_id"), (cg.uint16, "confidence")], conf + await automation.build_callback_automation( + var, + "add_on_finger_scan_matched_callback", + [(cg.uint16, "finger_id"), (cg.uint16, "confidence")], + conf, ) - for conf in config.get(CONF_ON_FINGER_SCAN_UNMATCHED, []): - trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) - await automation.build_automation(trigger, [], conf) - + await automation.build_callback_automation( + var, "add_on_finger_scan_unmatched_callback", [], conf + ) for conf in config.get(CONF_ON_FINGER_SCAN_MISPLACED, []): - trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) - await automation.build_automation(trigger, [], conf) - + await automation.build_callback_automation( + var, "add_on_finger_scan_misplaced_callback", [], conf + ) for conf in config.get(CONF_ON_FINGER_SCAN_INVALID, []): - trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) - await automation.build_automation(trigger, [], conf) - + await automation.build_callback_automation( + var, "add_on_finger_scan_invalid_callback", [], conf + ) for conf in config.get(CONF_ON_ENROLLMENT_SCAN, []): - trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) - await automation.build_automation( - trigger, [(cg.uint8, "scan_num"), (cg.uint16, "finger_id")], conf + await automation.build_callback_automation( + var, + "add_on_enrollment_scan_callback", + [(cg.uint8, "scan_num"), (cg.uint16, "finger_id")], + conf, ) - for conf in config.get(CONF_ON_ENROLLMENT_DONE, []): - trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) - await automation.build_automation(trigger, [(cg.uint16, "finger_id")], conf) - + await automation.build_callback_automation( + var, "add_on_enrollment_done_callback", [(cg.uint16, "finger_id")], conf + ) for conf in config.get(CONF_ON_ENROLLMENT_FAILED, []): - trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) - await automation.build_automation(trigger, [(cg.uint16, "finger_id")], conf) + await automation.build_callback_automation( + var, "add_on_enrollment_failed_callback", [(cg.uint16, "finger_id")], conf + ) @automation.register_action( @@ -261,6 +191,7 @@ async def to_code(config): }, key=CONF_FINGER_ID, ), + synchronous=True, ) async def fingerprint_grow_enroll_to_code(config, action_id, template_arg, args): var = cg.new_Pvariable(action_id, template_arg) @@ -282,6 +213,7 @@ async def fingerprint_grow_enroll_to_code(config, action_id, template_arg, args) cv.GenerateID(): cv.use_id(FingerprintGrowComponent), } ), + synchronous=True, ) async def fingerprint_grow_cancel_enroll_to_code(config, action_id, template_arg, args): var = cg.new_Pvariable(action_id, template_arg) @@ -299,6 +231,7 @@ async def fingerprint_grow_cancel_enroll_to_code(config, action_id, template_arg }, key=CONF_FINGER_ID, ), + synchronous=True, ) async def fingerprint_grow_delete_to_code(config, action_id, template_arg, args): var = cg.new_Pvariable(action_id, template_arg) @@ -317,6 +250,7 @@ async def fingerprint_grow_delete_to_code(config, action_id, template_arg, args) cv.GenerateID(): cv.use_id(FingerprintGrowComponent), } ), + synchronous=True, ) async def fingerprint_grow_delete_all_to_code(config, action_id, template_arg, args): var = cg.new_Pvariable(action_id, template_arg) @@ -337,6 +271,7 @@ FINGERPRINT_GROW_LED_CONTROL_ACTION_SCHEMA = cv.maybe_simple_value( "fingerprint_grow.led_control", LEDControlAction, FINGERPRINT_GROW_LED_CONTROL_ACTION_SCHEMA, + synchronous=True, ) async def fingerprint_grow_led_control_to_code(config, action_id, template_arg, args): var = cg.new_Pvariable(action_id, template_arg) @@ -359,6 +294,7 @@ async def fingerprint_grow_led_control_to_code(config, action_id, template_arg, cv.Required(CONF_COUNT): cv.templatable(cv.uint8_t), } ), + synchronous=True, ) async def fingerprint_grow_aura_led_control_to_code( config, action_id, template_arg, args diff --git a/esphome/components/fingerprint_grow/fingerprint_grow.cpp b/esphome/components/fingerprint_grow/fingerprint_grow.cpp index da4535fc82..a633fbca28 100644 --- a/esphome/components/fingerprint_grow/fingerprint_grow.cpp +++ b/esphome/components/fingerprint_grow/fingerprint_grow.cpp @@ -361,7 +361,7 @@ void FingerprintGrowComponent::aura_led_control(uint8_t state, uint8_t speed, ui } } -uint8_t FingerprintGrowComponent::transfer_(std::vector *p_data_buffer) { +uint8_t FingerprintGrowComponent::transfer_(std::vector &data_buffer) { while (this->available()) this->read(); this->write((uint8_t) (START_CODE >> 8)); @@ -372,12 +372,12 @@ uint8_t FingerprintGrowComponent::transfer_(std::vector *p_data_buffer) this->write(this->address_[3]); this->write(COMMAND); - uint16_t wire_length = p_data_buffer->size() + 2; + uint16_t wire_length = data_buffer.size() + 2; this->write((uint8_t) (wire_length >> 8)); this->write((uint8_t) (wire_length & 0xFF)); uint16_t sum = (wire_length >> 8) + (wire_length & 0xFF) + COMMAND; - for (auto data : *p_data_buffer) { + for (auto data : data_buffer) { this->write(data); sum += data; } @@ -385,7 +385,7 @@ uint8_t FingerprintGrowComponent::transfer_(std::vector *p_data_buffer) this->write((uint8_t) (sum >> 8)); this->write((uint8_t) (sum & 0xFF)); - p_data_buffer->clear(); + data_buffer.clear(); uint8_t byte; uint16_t idx = 0, length = 0; @@ -431,9 +431,9 @@ uint8_t FingerprintGrowComponent::transfer_(std::vector *p_data_buffer) length |= byte; break; default: - p_data_buffer->push_back(byte); + data_buffer.push_back(byte); if ((idx - 8) == length) { - switch ((*p_data_buffer)[0]) { + switch (data_buffer[0]) { case OK: case NO_FINGER: case IMAGE_FAIL: @@ -453,25 +453,26 @@ uint8_t FingerprintGrowComponent::transfer_(std::vector *p_data_buffer) ESP_LOGE(TAG, "Reader failed to process request"); break; default: - ESP_LOGE(TAG, "Unknown response received from reader: 0x%.2X", (*p_data_buffer)[0]); + ESP_LOGE(TAG, "Unknown response received from reader: 0x%.2X", data_buffer[0]); break; } this->last_transfer_ms_ = millis(); - return (*p_data_buffer)[0]; + return data_buffer[0]; } break; } idx++; } ESP_LOGE(TAG, "No response received from reader"); - (*p_data_buffer)[0] = TIMEOUT; + data_buffer.clear(); + data_buffer.push_back(TIMEOUT); this->last_transfer_ms_ = millis(); return TIMEOUT; } uint8_t FingerprintGrowComponent::send_command_() { this->sensor_wakeup_(); - return this->transfer_(&this->data_); + return this->transfer_(this->data_); } void FingerprintGrowComponent::sensor_wakeup_() { @@ -517,7 +518,7 @@ void FingerprintGrowComponent::sensor_wakeup_() { std::vector buffer = {VERIFY_PASSWORD, (uint8_t) (this->password_ >> 24), (uint8_t) (this->password_ >> 16), (uint8_t) (this->password_ >> 8), (uint8_t) (this->password_ & 0xFF)}; - if (this->transfer_(&buffer) != OK) { + if (this->transfer_(buffer) != OK) { ESP_LOGE(TAG, "Wrong password"); } } diff --git a/esphome/components/fingerprint_grow/fingerprint_grow.h b/esphome/components/fingerprint_grow/fingerprint_grow.h index 370b26f56a..947c701c98 100644 --- a/esphome/components/fingerprint_grow/fingerprint_grow.h +++ b/esphome/components/fingerprint_grow/fingerprint_grow.h @@ -127,30 +127,30 @@ class FingerprintGrowComponent : public PollingComponent, public uart::UARTDevic void set_enrolling_binary_sensor(binary_sensor::BinarySensor *enrolling_binary_sensor) { this->enrolling_binary_sensor_ = enrolling_binary_sensor; } - void add_on_finger_scan_start_callback(std::function callback) { - this->finger_scan_start_callback_.add(std::move(callback)); + template void add_on_finger_scan_start_callback(F &&callback) { + this->finger_scan_start_callback_.add(std::forward(callback)); } - void add_on_finger_scan_matched_callback(std::function callback) { - this->finger_scan_matched_callback_.add(std::move(callback)); + template void add_on_finger_scan_matched_callback(F &&callback) { + this->finger_scan_matched_callback_.add(std::forward(callback)); } - void add_on_finger_scan_unmatched_callback(std::function callback) { - this->finger_scan_unmatched_callback_.add(std::move(callback)); + template void add_on_finger_scan_unmatched_callback(F &&callback) { + this->finger_scan_unmatched_callback_.add(std::forward(callback)); } - void add_on_finger_scan_misplaced_callback(std::function callback) { - this->finger_scan_misplaced_callback_.add(std::move(callback)); + template void add_on_finger_scan_misplaced_callback(F &&callback) { + this->finger_scan_misplaced_callback_.add(std::forward(callback)); } - void add_on_finger_scan_invalid_callback(std::function callback) { - this->finger_scan_invalid_callback_.add(std::move(callback)); + template void add_on_finger_scan_invalid_callback(F &&callback) { + this->finger_scan_invalid_callback_.add(std::forward(callback)); } - void add_on_enrollment_scan_callback(std::function callback) { - this->enrollment_scan_callback_.add(std::move(callback)); + template void add_on_enrollment_scan_callback(F &&callback) { + this->enrollment_scan_callback_.add(std::forward(callback)); } - void add_on_enrollment_done_callback(std::function callback) { - this->enrollment_done_callback_.add(std::move(callback)); + template void add_on_enrollment_done_callback(F &&callback) { + this->enrollment_done_callback_.add(std::forward(callback)); } - void add_on_enrollment_failed_callback(std::function callback) { - this->enrollment_failed_callback_.add(std::move(callback)); + template void add_on_enrollment_failed_callback(F &&callback) { + this->enrollment_failed_callback_.add(std::forward(callback)); } void enroll_fingerprint(uint16_t finger_id, uint8_t num_buffers); @@ -169,7 +169,7 @@ class FingerprintGrowComponent : public PollingComponent, public uart::UARTDevic bool set_password_(); bool get_parameters_(); void get_fingerprint_count_(); - uint8_t transfer_(std::vector *p_data_buffer); + uint8_t transfer_(std::vector &data_buffer); uint8_t send_command_(); void sensor_wakeup_(); void sensor_sleep_(); @@ -190,7 +190,7 @@ class FingerprintGrowComponent : public PollingComponent, public uart::UARTDevic bool is_sensor_awake_ = false; uint32_t last_transfer_ms_ = 0; uint32_t last_aura_led_control_ = 0; - uint16_t last_aura_led_duration_ = 0; + uint32_t last_aura_led_duration_ = 0; uint16_t system_identifier_code_ = 0; uint32_t idle_period_to_sleep_ms_ = UINT32_MAX; sensor::Sensor *fingerprint_count_sensor_{nullptr}; @@ -210,64 +210,6 @@ class FingerprintGrowComponent : public PollingComponent, public uart::UARTDevic CallbackManager enrollment_failed_callback_; }; -class FingerScanStartTrigger : public Trigger<> { - public: - explicit FingerScanStartTrigger(FingerprintGrowComponent *parent) { - parent->add_on_finger_scan_start_callback([this]() { this->trigger(); }); - } -}; - -class FingerScanMatchedTrigger : public Trigger { - public: - explicit FingerScanMatchedTrigger(FingerprintGrowComponent *parent) { - parent->add_on_finger_scan_matched_callback( - [this](uint16_t finger_id, uint16_t confidence) { this->trigger(finger_id, confidence); }); - } -}; - -class FingerScanUnmatchedTrigger : public Trigger<> { - public: - explicit FingerScanUnmatchedTrigger(FingerprintGrowComponent *parent) { - parent->add_on_finger_scan_unmatched_callback([this]() { this->trigger(); }); - } -}; - -class FingerScanMisplacedTrigger : public Trigger<> { - public: - explicit FingerScanMisplacedTrigger(FingerprintGrowComponent *parent) { - parent->add_on_finger_scan_misplaced_callback([this]() { this->trigger(); }); - } -}; - -class FingerScanInvalidTrigger : public Trigger<> { - public: - explicit FingerScanInvalidTrigger(FingerprintGrowComponent *parent) { - parent->add_on_finger_scan_invalid_callback([this]() { this->trigger(); }); - } -}; - -class EnrollmentScanTrigger : public Trigger { - public: - explicit EnrollmentScanTrigger(FingerprintGrowComponent *parent) { - parent->add_on_enrollment_scan_callback( - [this](uint8_t scan_num, uint16_t finger_id) { this->trigger(scan_num, finger_id); }); - } -}; - -class EnrollmentDoneTrigger : public Trigger { - public: - explicit EnrollmentDoneTrigger(FingerprintGrowComponent *parent) { - parent->add_on_enrollment_done_callback([this](uint16_t finger_id) { this->trigger(finger_id); }); - } -}; - -class EnrollmentFailedTrigger : public Trigger { - public: - explicit EnrollmentFailedTrigger(FingerprintGrowComponent *parent) { - parent->add_on_enrollment_failed_callback([this](uint16_t finger_id) { this->trigger(finger_id); }); - } -}; - template class EnrollmentAction : public Action, public Parented { public: TEMPLATABLE_VALUE(uint16_t, finger_id) diff --git a/esphome/components/font/__init__.py b/esphome/components/font/__init__.py index 2667dbdbdf..c8813bf1bc 100644 --- a/esphome/components/font/__init__.py +++ b/esphome/components/font/__init__.py @@ -552,6 +552,7 @@ async def to_code(config): """ # get the codepoints from glyphsets and flatten to a set of chrs. + cg.add_define("USE_FONT") point_set: set[str] = { chr(x) for x in flatten( diff --git a/esphome/components/font/font.cpp b/esphome/components/font/font.cpp index 5e3bf1dd20..ecf0ca6bdd 100644 --- a/esphome/components/font/font.cpp +++ b/esphome/components/font/font.cpp @@ -9,13 +9,87 @@ namespace font { static const char *const TAG = "font"; #ifdef USE_LVGL_FONT -const uint8_t *Font::get_glyph_bitmap(const lv_font_t *font, uint32_t unicode_letter) { - auto *fe = (Font *) font->dsc; - const auto *gd = fe->get_glyph_data_(unicode_letter); +static const uint8_t OPA4_TABLE[16] = {0, 17, 34, 51, 68, 85, 102, 119, 136, 153, 170, 187, 204, 221, 238, 255}; + +static const uint8_t OPA2_TABLE[4] = {0, 85, 170, 255}; + +const void *Font::get_glyph_bitmap(lv_font_glyph_dsc_t *dsc, lv_draw_buf_t *draw_buf) { + const auto *font = dsc->resolved_font; + auto *const fe = (Font *) font->dsc; + + const auto *gd = fe->get_glyph_data_(dsc->gid.index); if (gd == nullptr) { return nullptr; } - return gd->data; + + const uint8_t *bitmap_in = gd->data; + uint8_t *bitmap_out_tmp = draw_buf->data; + int32_t i = 0; + int32_t x, y; + uint32_t stride = lv_draw_buf_width_to_stride(gd->width, LV_COLOR_FORMAT_A8); + + switch (fe->get_bpp()) { + case 1: { + uint8_t mask = 0; + uint8_t byte = 0; + for (y = 0; y != gd->height; y++) { + for (x = 0; x != gd->width; x++) { + if (mask == 0) { + mask = 0x80; + byte = *bitmap_in++; + } + bitmap_out_tmp[x] = byte & mask ? 255 : 0; + mask >>= 1; + } + bitmap_out_tmp += stride; + } + } break; + + case 2: + for (y = 0; y != gd->height; y++) { + for (x = 0; x != gd->width; x++, i++) { + switch (i & 0x3) { + default: + bitmap_out_tmp[x] = OPA2_TABLE[(*bitmap_in) >> 6]; + break; + case 1: + bitmap_out_tmp[x] = OPA2_TABLE[((*bitmap_in) >> 4) & 0x3]; + break; + case 2: + bitmap_out_tmp[x] = OPA2_TABLE[((*bitmap_in) >> 2) & 0x3]; + break; + case 3: + bitmap_out_tmp[x] = OPA2_TABLE[((*bitmap_in) >> 0) & 0x3]; + bitmap_in++; + } + } + bitmap_out_tmp += stride; + } + break; + + case 4: + for (y = 0; y != gd->height; y++) { + for (x = 0; x != gd->width; x++, i++) { + i = i & 0x1; + if (i == 0) { + bitmap_out_tmp[x] = OPA4_TABLE[(*bitmap_in) >> 4]; + } else if (i == 1) { + bitmap_out_tmp[x] = OPA4_TABLE[(*bitmap_in) & 0xF]; + bitmap_in++; + } + } + bitmap_out_tmp += stride; + } + break; + + case 8: + memcpy(bitmap_out_tmp, bitmap_in, gd->width * gd->height); + break; + default: + ESP_LOGD(TAG, "Unknown bpp: %d", fe->get_bpp()); + break; + } + return draw_buf; } bool Font::get_glyph_dsc_cb(const lv_font_t *font, lv_font_glyph_dsc_t *dsc, uint32_t unicode_letter, uint32_t next) { @@ -30,7 +104,8 @@ bool Font::get_glyph_dsc_cb(const lv_font_t *font, lv_font_glyph_dsc_t *dsc, uin dsc->box_w = gd->width; dsc->box_h = gd->height; dsc->is_placeholder = 0; - dsc->bpp = fe->get_bpp(); + dsc->format = (lv_font_glyph_format_t) fe->get_bpp(); + dsc->gid.index = unicode_letter; return true; } diff --git a/esphome/components/font/font.h b/esphome/components/font/font.h index 262ded3be4..4a09d7314d 100644 --- a/esphome/components/font/font.h +++ b/esphome/components/font/font.h @@ -90,7 +90,7 @@ class Font uint8_t bpp_; // bits per pixel #ifdef USE_LVGL_FONT lv_font_t lv_font_{}; - static const uint8_t *get_glyph_bitmap(const lv_font_t *font, uint32_t unicode_letter); + static const void *get_glyph_bitmap(lv_font_glyph_dsc_t *dsc, lv_draw_buf_t *draw_buf); static bool get_glyph_dsc_cb(const lv_font_t *font, lv_font_glyph_dsc_t *dsc, uint32_t unicode_letter, uint32_t next); const Glyph *get_glyph_data_(uint32_t unicode_letter); uint32_t last_letter_{}; diff --git a/esphome/components/fujitsu_general/fujitsu_general.cpp b/esphome/components/fujitsu_general/fujitsu_general.cpp index 6c7adebfea..8aa0f51728 100644 --- a/esphome/components/fujitsu_general/fujitsu_general.cpp +++ b/esphome/components/fujitsu_general/fujitsu_general.cpp @@ -141,7 +141,7 @@ void FujitsuGeneralClimate::transmit_state() { } // Set fan - switch (this->fan_mode.value()) { + switch (this->fan_mode.value_or(climate::CLIMATE_FAN_ON)) { case climate::CLIMATE_FAN_HIGH: SET_NIBBLE(remote_state, FUJITSU_GENERAL_FAN_NIBBLE, FUJITSU_GENERAL_FAN_HIGH); break; diff --git a/esphome/components/gcja5/gcja5.cpp b/esphome/components/gcja5/gcja5.cpp index f7f7f8d02c..43b2fa20d3 100644 --- a/esphome/components/gcja5/gcja5.cpp +++ b/esphome/components/gcja5/gcja5.cpp @@ -97,10 +97,10 @@ void GCJA5Component::parse_data_() { ESP_LOGI(TAG, "GCJA5 Status\n" - "Overall Status : %i\n" - "PD Status : %i\n" - "LD Status : %i\n" - "Fan Status : %i", + " Overall Status : %i\n" + " PD Status : %i\n" + " LD Status : %i\n" + " Fan Status : %i", (status >> 6) & 0x03, (status >> 4) & 0x03, (status >> 2) & 0x03, (status >> 0) & 0x03); } } diff --git a/esphome/components/gdk101/gdk101.cpp b/esphome/components/gdk101/gdk101.cpp index ddf38f2f55..8b381564b2 100644 --- a/esphome/components/gdk101/gdk101.cpp +++ b/esphome/components/gdk101/gdk101.cpp @@ -77,8 +77,6 @@ void GDK101Component::dump_config() { #endif // USE_TEXT_SENSOR } -float GDK101Component::get_setup_priority() const { return setup_priority::DATA; } - bool GDK101Component::read_bytes_with_retry_(uint8_t a_register, uint8_t *data, uint8_t len) { uint8_t retry = NUMBER_OF_READ_RETRIES; bool status = false; diff --git a/esphome/components/gdk101/gdk101.h b/esphome/components/gdk101/gdk101.h index f250a42a54..abe417e0f9 100644 --- a/esphome/components/gdk101/gdk101.h +++ b/esphome/components/gdk101/gdk101.h @@ -40,7 +40,6 @@ class GDK101Component : public PollingComponent, public i2c::I2CDevice { public: void setup() override; void dump_config() override; - float get_setup_priority() const override; void update() override; protected: diff --git a/esphome/components/globals/__init__.py b/esphome/components/globals/__init__.py index fc400c5dd1..fe83b1ea7c 100644 --- a/esphome/components/globals/__init__.py +++ b/esphome/components/globals/__init__.py @@ -51,7 +51,7 @@ _RESTORING_SCHEMA = cv.Schema( def _globals_schema(config: ConfigType) -> ConfigType: """Select schema based on restore_value setting.""" - if config.get(CONF_RESTORE_VALUE, False): + if cv.boolean(config.get(CONF_RESTORE_VALUE, False)): return _RESTORING_SCHEMA(config) return _NON_RESTORING_SCHEMA(config) @@ -102,6 +102,7 @@ async def to_code(config): cv.Required(CONF_VALUE): cv.templatable(cv.string_strict), } ), + synchronous=True, ) async def globals_set_to_code(config, action_id, template_arg, args): full_id, paren = await cg.get_variable_with_full_id(config[CONF_ID]) diff --git a/esphome/components/globals/globals_component.h b/esphome/components/globals/globals_component.h index 3db29bea35..520c068e6f 100644 --- a/esphome/components/globals/globals_component.h +++ b/esphome/components/globals/globals_component.h @@ -84,7 +84,7 @@ template class RestoringGlobalStringComponent : public P this->rtc_ = global_preferences->make_preference(1944399030U ^ this->name_hash_); bool hasdata = this->rtc_.load(&temp); if (hasdata) { - this->value_.assign(temp + 1, temp[0]); + this->value_.assign(temp + 1, static_cast(temp[0])); } this->last_checked_value_.assign(this->value_); } diff --git a/esphome/components/gp8403/gp8403.h b/esphome/components/gp8403/gp8403.h index 6613187b20..a19df15515 100644 --- a/esphome/components/gp8403/gp8403.h +++ b/esphome/components/gp8403/gp8403.h @@ -6,12 +6,12 @@ namespace esphome { namespace gp8403 { -enum GP8403Voltage { +enum GP8403Voltage : uint8_t { GP8403_VOLTAGE_5V = 0x00, GP8403_VOLTAGE_10V = 0x11, }; -enum GP8403Model { +enum GP8403Model : uint8_t { GP8403, GP8413, }; @@ -20,7 +20,6 @@ class GP8403Component : public Component, public i2c::I2CDevice { public: void setup() override; void dump_config() override; - float get_setup_priority() const override { return setup_priority::DATA; } void set_model(GP8403Model model) { this->model_ = model; } void set_voltage(gp8403::GP8403Voltage voltage) { this->voltage_ = voltage; } diff --git a/esphome/components/gpio/binary_sensor/gpio_binary_sensor.cpp b/esphome/components/gpio/binary_sensor/gpio_binary_sensor.cpp index 7a35596194..39b1a2f713 100644 --- a/esphome/components/gpio/binary_sensor/gpio_binary_sensor.cpp +++ b/esphome/components/gpio/binary_sensor/gpio_binary_sensor.cpp @@ -1,5 +1,6 @@ #include "gpio_binary_sensor.h" #include "esphome/core/log.h" +#include "esphome/core/progmem.h" namespace esphome { namespace gpio { @@ -7,17 +8,12 @@ namespace gpio { static const char *const TAG = "gpio.binary_sensor"; #if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_DEBUG +// Interrupt type strings indexed by edge-triggered InterruptType values: +// indices 1-3: RISING_EDGE, FALLING_EDGE, ANY_EDGE; other values (e.g. level-triggered) map to UNKNOWN (index 0). +PROGMEM_STRING_TABLE(InterruptTypeStrings, "UNKNOWN", "RISING_EDGE", "FALLING_EDGE", "ANY_EDGE"); + static const LogString *interrupt_type_to_string(gpio::InterruptType type) { - switch (type) { - case gpio::INTERRUPT_RISING_EDGE: - return LOG_STR("RISING_EDGE"); - case gpio::INTERRUPT_FALLING_EDGE: - return LOG_STR("FALLING_EDGE"); - case gpio::INTERRUPT_ANY_EDGE: - return LOG_STR("ANY_EDGE"); - default: - return LOG_STR("UNKNOWN"); - } + return InterruptTypeStrings::get_log_str(static_cast(type), 0); } static const LogString *gpio_mode_to_string(bool use_interrupt) { @@ -27,9 +23,8 @@ static const LogString *gpio_mode_to_string(bool use_interrupt) { void IRAM_ATTR GPIOBinarySensorStore::gpio_intr(GPIOBinarySensorStore *arg) { bool new_state = arg->isr_pin_.digital_read(); - if (new_state != arg->last_state_) { + if (new_state != arg->state_) { arg->state_ = new_state; - arg->last_state_ = new_state; arg->changed_ = true; // Wake up the component from its disabled loop state if (arg->component_ != nullptr) { @@ -38,28 +33,27 @@ void IRAM_ATTR GPIOBinarySensorStore::gpio_intr(GPIOBinarySensorStore *arg) { } } -void GPIOBinarySensorStore::setup(InternalGPIOPin *pin, gpio::InterruptType type, Component *component) { +void GPIOBinarySensorStore::setup(InternalGPIOPin *pin, Component *component) { pin->setup(); this->isr_pin_ = pin->to_isr(); this->component_ = component; // Read initial state - this->last_state_ = pin->digital_read(); - this->state_ = this->last_state_; + this->state_ = pin->digital_read(); // Attach interrupt - from this point on, any changes will be caught by the interrupt - pin->attach_interrupt(&GPIOBinarySensorStore::gpio_intr, this, type); + pin->attach_interrupt(&GPIOBinarySensorStore::gpio_intr, this, this->interrupt_type_); } void GPIOBinarySensor::setup() { - if (this->use_interrupt_ && !this->pin_->is_internal()) { + if (this->store_.use_interrupt_ && !this->pin_->is_internal()) { ESP_LOGD(TAG, "GPIO is not internal, falling back to polling mode"); - this->use_interrupt_ = false; + this->store_.use_interrupt_ = false; } - if (this->use_interrupt_) { + if (this->store_.use_interrupt_) { auto *internal_pin = static_cast(this->pin_); - this->store_.setup(internal_pin, this->interrupt_type_, this); + this->store_.setup(internal_pin, this); this->publish_initial_state(this->store_.get_state()); } else { this->pin_->setup(); @@ -70,14 +64,14 @@ void GPIOBinarySensor::setup() { void GPIOBinarySensor::dump_config() { LOG_BINARY_SENSOR("", "GPIO Binary Sensor", this); LOG_PIN(" Pin: ", this->pin_); - ESP_LOGCONFIG(TAG, " Mode: %s", LOG_STR_ARG(gpio_mode_to_string(this->use_interrupt_))); - if (this->use_interrupt_) { - ESP_LOGCONFIG(TAG, " Interrupt Type: %s", LOG_STR_ARG(interrupt_type_to_string(this->interrupt_type_))); + ESP_LOGCONFIG(TAG, " Mode: %s", LOG_STR_ARG(gpio_mode_to_string(this->store_.use_interrupt_))); + if (this->store_.use_interrupt_) { + ESP_LOGCONFIG(TAG, " Interrupt Type: %s", LOG_STR_ARG(interrupt_type_to_string(this->store_.interrupt_type_))); } } void GPIOBinarySensor::loop() { - if (this->use_interrupt_) { + if (this->store_.use_interrupt_) { if (this->store_.is_changed()) { // Clear the flag immediately to minimize the window where we might miss changes this->store_.clear_changed(); diff --git a/esphome/components/gpio/binary_sensor/gpio_binary_sensor.h b/esphome/components/gpio/binary_sensor/gpio_binary_sensor.h index 8cf52f540b..24efc2a0e6 100644 --- a/esphome/components/gpio/binary_sensor/gpio_binary_sensor.h +++ b/esphome/components/gpio/binary_sensor/gpio_binary_sensor.h @@ -8,10 +8,10 @@ namespace esphome { namespace gpio { -// Store class for ISR data (no vtables, ISR-safe) +// Store class for ISR data and configuration (no vtables, ISR-safe) class GPIOBinarySensorStore { public: - void setup(InternalGPIOPin *pin, gpio::InterruptType type, Component *component); + void setup(InternalGPIOPin *pin, Component *component); static void gpio_intr(GPIOBinarySensorStore *arg); @@ -32,21 +32,23 @@ class GPIOBinarySensorStore { } protected: + friend class GPIOBinarySensor; ISRInternalGPIOPin isr_pin_; - volatile bool state_{false}; - volatile bool last_state_{false}; - volatile bool changed_{false}; Component *component_{nullptr}; // Pointer to the component for enable_loop_soon_any_context() + volatile bool state_{false}; + volatile bool changed_{false}; + bool use_interrupt_{true}; + gpio::InterruptType interrupt_type_{gpio::INTERRUPT_ANY_EDGE}; }; -class GPIOBinarySensor : public binary_sensor::BinarySensor, public Component { +class GPIOBinarySensor final : public binary_sensor::BinarySensor, public Component { public: // No destructor needed: ESPHome components are created at boot and live forever. // Interrupts are only detached on reboot when memory is cleared anyway. - void set_pin(GPIOPin *pin) { pin_ = pin; } - void set_use_interrupt(bool use_interrupt) { use_interrupt_ = use_interrupt; } - void set_interrupt_type(gpio::InterruptType type) { interrupt_type_ = type; } + void set_pin(GPIOPin *pin) { this->pin_ = pin; } + void set_use_interrupt(bool use_interrupt) { this->store_.use_interrupt_ = use_interrupt; } + void set_interrupt_type(gpio::InterruptType type) { this->store_.interrupt_type_ = type; } // ========== INTERNAL METHODS ========== // (In most use cases you won't need these) /// Setup pin @@ -59,8 +61,6 @@ class GPIOBinarySensor : public binary_sensor::BinarySensor, public Component { protected: GPIOPin *pin_; - bool use_interrupt_{true}; - gpio::InterruptType interrupt_type_{gpio::INTERRUPT_ANY_EDGE}; GPIOBinarySensorStore store_; }; diff --git a/esphome/components/gpio/one_wire/gpio_one_wire.cpp b/esphome/components/gpio/one_wire/gpio_one_wire.cpp index 4191c45de1..4e2a306fc9 100644 --- a/esphome/components/gpio/one_wire/gpio_one_wire.cpp +++ b/esphome/components/gpio/one_wire/gpio_one_wire.cpp @@ -131,7 +131,7 @@ uint8_t IRAM_ATTR GPIOOneWireBus::read8() { uint64_t IRAM_ATTR GPIOOneWireBus::read64() { InterruptLock lock; uint64_t ret = 0; - for (uint8_t i = 0; i < 8; i++) { + for (uint8_t i = 0; i < 64; i++) { ret |= (uint64_t(this->read_bit_()) << i); } return ret; diff --git a/esphome/components/gpio/switch/__init__.py b/esphome/components/gpio/switch/__init__.py index 604de6d809..9462cd0161 100644 --- a/esphome/components/gpio/switch/__init__.py +++ b/esphome/components/gpio/switch/__init__.py @@ -32,6 +32,7 @@ async def to_code(config): cg.add(var.set_pin(pin)) if CONF_INTERLOCK in config: + cg.add_define("USE_GPIO_SWITCH_INTERLOCK") interlock = [] for it in config[CONF_INTERLOCK]: lock = await cg.get_variable(it) diff --git a/esphome/components/gpio/switch/gpio_switch.cpp b/esphome/components/gpio/switch/gpio_switch.cpp index 9043a6a493..9c6464815a 100644 --- a/esphome/components/gpio/switch/gpio_switch.cpp +++ b/esphome/components/gpio/switch/gpio_switch.cpp @@ -5,6 +5,9 @@ namespace esphome { namespace gpio { static const char *const TAG = "switch.gpio"; +#ifdef USE_GPIO_SWITCH_INTERLOCK +static constexpr uint32_t INTERLOCK_TIMEOUT_ID = 0; +#endif float GPIOSwitch::get_setup_priority() const { return setup_priority::HARDWARE; } void GPIOSwitch::setup() { @@ -27,6 +30,7 @@ void GPIOSwitch::setup() { void GPIOSwitch::dump_config() { LOG_SWITCH("", "GPIO Switch", this); LOG_PIN(" Pin: ", this->pin_); +#ifdef USE_GPIO_SWITCH_INTERLOCK if (!this->interlock_.empty()) { ESP_LOGCONFIG(TAG, " Interlocks:"); for (auto *lock : this->interlock_) { @@ -35,8 +39,10 @@ void GPIOSwitch::dump_config() { ESP_LOGCONFIG(TAG, " %s", lock->get_name().c_str()); } } +#endif } void GPIOSwitch::write_state(bool state) { +#ifdef USE_GPIO_SWITCH_INTERLOCK if (state != this->inverted_) { // Turning ON, check interlocking @@ -51,7 +57,7 @@ void GPIOSwitch::write_state(bool state) { } } if (found && this->interlock_wait_time_ != 0) { - this->set_timeout("interlock", this->interlock_wait_time_, [this, state] { + this->set_timeout(INTERLOCK_TIMEOUT_ID, this->interlock_wait_time_, [this, state] { // Don't write directly, call the function again // (some other switch may have changed state while we were waiting) this->write_state(state); @@ -61,13 +67,17 @@ void GPIOSwitch::write_state(bool state) { } else if (this->interlock_wait_time_ != 0) { // If we are switched off during the interlock wait time, cancel any pending // re-activations - this->cancel_timeout("interlock"); + this->cancel_timeout(INTERLOCK_TIMEOUT_ID); } +#endif this->pin_->digital_write(state); this->publish_state(state); } + +#ifdef USE_GPIO_SWITCH_INTERLOCK void GPIOSwitch::set_interlock(const std::initializer_list &interlock) { this->interlock_ = interlock; } +#endif } // namespace gpio } // namespace esphome diff --git a/esphome/components/gpio/switch/gpio_switch.h b/esphome/components/gpio/switch/gpio_switch.h index 080decac08..f7415d1dba 100644 --- a/esphome/components/gpio/switch/gpio_switch.h +++ b/esphome/components/gpio/switch/gpio_switch.h @@ -8,7 +8,7 @@ namespace esphome { namespace gpio { -class GPIOSwitch : public switch_::Switch, public Component { +class GPIOSwitch final : public switch_::Switch, public Component { public: void set_pin(GPIOPin *pin) { pin_ = pin; } @@ -18,15 +18,19 @@ class GPIOSwitch : public switch_::Switch, public Component { void setup() override; void dump_config() override; +#ifdef USE_GPIO_SWITCH_INTERLOCK void set_interlock(const std::initializer_list &interlock); void set_interlock_wait_time(uint32_t interlock_wait_time) { interlock_wait_time_ = interlock_wait_time; } +#endif protected: void write_state(bool state) override; GPIOPin *pin_; +#ifdef USE_GPIO_SWITCH_INTERLOCK FixedVector interlock_; uint32_t interlock_wait_time_{0}; +#endif }; } // namespace gpio diff --git a/esphome/components/gps/__init__.py b/esphome/components/gps/__init__.py index 2135189bd5..ab48417a4e 100644 --- a/esphome/components/gps/__init__.py +++ b/esphome/components/gps/__init__.py @@ -34,7 +34,7 @@ AUTO_LOAD = ["sensor"] CODEOWNERS = ["@coogle", "@ximex"] gps_ns = cg.esphome_ns.namespace("gps") -GPS = gps_ns.class_("GPS", cg.Component, uart.UARTDevice) +GPS = gps_ns.class_("GPS", cg.PollingComponent, uart.UARTDevice) GPSListener = gps_ns.class_("GPSListener") MULTI_CONF = True @@ -98,33 +98,25 @@ async def to_code(config): await cg.register_component(var, config) await uart.register_uart_device(var, config) - if latitude_config := config.get(CONF_LATITUDE): - sens = await sensor.new_sensor(latitude_config) - cg.add(var.set_latitude_sensor(sens)) + # Pre-create all sensor variables so automations that reference + # sibling sensors don't deadlock waiting for unregistered IDs. + sensors = [ + (cg.new_Pvariable(conf[CONF_ID]), conf, setter) + for key, setter in ( + (CONF_LATITUDE, "set_latitude_sensor"), + (CONF_LONGITUDE, "set_longitude_sensor"), + (CONF_SPEED, "set_speed_sensor"), + (CONF_COURSE, "set_course_sensor"), + (CONF_ALTITUDE, "set_altitude_sensor"), + (CONF_SATELLITES, "set_satellites_sensor"), + (CONF_HDOP, "set_hdop_sensor"), + ) + if (conf := config.get(key)) + ] - if longitude_config := config.get(CONF_LONGITUDE): - sens = await sensor.new_sensor(longitude_config) - cg.add(var.set_longitude_sensor(sens)) - - if speed_config := config.get(CONF_SPEED): - sens = await sensor.new_sensor(speed_config) - cg.add(var.set_speed_sensor(sens)) - - if course_config := config.get(CONF_COURSE): - sens = await sensor.new_sensor(course_config) - cg.add(var.set_course_sensor(sens)) - - if altitude_config := config.get(CONF_ALTITUDE): - sens = await sensor.new_sensor(altitude_config) - cg.add(var.set_altitude_sensor(sens)) - - if satellites_config := config.get(CONF_SATELLITES): - sens = await sensor.new_sensor(satellites_config) - cg.add(var.set_satellites_sensor(sens)) - - if hdop_config := config.get(CONF_HDOP): - sens = await sensor.new_sensor(hdop_config) - cg.add(var.set_hdop_sensor(sens)) + for sens, conf, setter in sensors: + await sensor.register_sensor(sens, conf) + cg.add(getattr(var, setter)(sens)) # https://platformio.org/lib/show/1655/TinyGPSPlus # Using fork of TinyGPSPlus patched to build on ESP-IDF diff --git a/esphome/components/gps/time/gps_time.cpp b/esphome/components/gps/time/gps_time.cpp index cff8c1fb07..fb662a3d60 100644 --- a/esphome/components/gps/time/gps_time.cpp +++ b/esphome/components/gps/time/gps_time.cpp @@ -16,10 +16,6 @@ void GPSTime::from_tiny_gps_(TinyGPSPlus &tiny_gps) { val.year = tiny_gps.date.year(); val.month = tiny_gps.date.month(); val.day_of_month = tiny_gps.date.day(); - // Set these to valid value for recalc_timestamp_utc - it's not used for calculation - val.day_of_week = 1; - val.day_of_year = 1; - val.hour = tiny_gps.time.hour(); val.minute = tiny_gps.time.minute(); val.second = tiny_gps.time.second(); diff --git a/esphome/components/graph/graph.cpp b/esphome/components/graph/graph.cpp index c43cd07fe0..801c97e3f5 100644 --- a/esphome/components/graph/graph.cpp +++ b/esphome/components/graph/graph.cpp @@ -171,7 +171,7 @@ void Graph::draw(Display *buff, uint16_t x_offset, uint16_t y_offset, Color colo bool prev_b = false; int16_t prev_y = 0; for (uint32_t i = 0; i < this->width_; i++) { - float v = (trace->get_tracedata()->get_value(i) - ymin) / yrange; + float v = yrange != 0 ? (trace->get_tracedata()->get_value(i) - ymin) / yrange : NAN; if (!std::isnan(v) && (thick > 0)) { int16_t x = this->width_ - 1 - i + x_offset; uint8_t bit = 1 << ((i % (thick * LineType::PATTERN_LENGTH)) / thick); diff --git a/esphome/components/graphical_display_menu/graphical_display_menu.cpp b/esphome/components/graphical_display_menu/graphical_display_menu.cpp index 2b120a746f..cf1672f217 100644 --- a/esphome/components/graphical_display_menu/graphical_display_menu.cpp +++ b/esphome/components/graphical_display_menu/graphical_display_menu.cpp @@ -38,13 +38,13 @@ void GraphicalDisplayMenu::setup() { void GraphicalDisplayMenu::dump_config() { ESP_LOGCONFIG(TAG, "Graphical Display Menu\n" - "Has Display: %s\n" - "Popup Mode: %s\n" - "Advanced Drawing Mode: %s\n" - "Has Font: %s\n" - "Mode: %s\n" - "Active: %s\n" - "Menu items:", + " Has Display: %s\n" + " Popup Mode: %s\n" + " Advanced Drawing Mode: %s\n" + " Has Font: %s\n" + " Mode: %s\n" + " Active: %s\n" + " Menu items:", YESNO(this->display_ != nullptr), YESNO(this->display_ != nullptr), YESNO(this->display_ == nullptr), YESNO(this->font_ != nullptr), this->mode_ == display_menu_base::MENU_MODE_ROTARY ? "Rotary" : "Joystick", YESNO(this->active_)); diff --git a/esphome/components/graphical_display_menu/graphical_display_menu.h b/esphome/components/graphical_display_menu/graphical_display_menu.h index 96f2bd79fd..ce1db18525 100644 --- a/esphome/components/graphical_display_menu/graphical_display_menu.h +++ b/esphome/components/graphical_display_menu/graphical_display_menu.h @@ -44,7 +44,7 @@ class GraphicalDisplayMenu : public display_menu_base::DisplayMenuComponent { void set_foreground_color(Color foreground_color); void set_background_color(Color background_color); - void add_on_redraw_callback(std::function &&cb) { this->on_redraw_callbacks_.add(std::move(cb)); } + template void add_on_redraw_callback(F &&cb) { this->on_redraw_callbacks_.add(std::forward(cb)); } void draw(display::Display *display, const display::Rect *bounds); @@ -75,9 +75,12 @@ class GraphicalDisplayMenu : public display_menu_base::DisplayMenuComponent { class GraphicalDisplayMenuOnRedrawTrigger : public Trigger { public: - explicit GraphicalDisplayMenuOnRedrawTrigger(GraphicalDisplayMenu *parent) { - parent->add_on_redraw_callback([this, parent]() { this->trigger(parent); }); + explicit GraphicalDisplayMenuOnRedrawTrigger(GraphicalDisplayMenu *parent) : parent_(parent) { + parent->add_on_redraw_callback([this]() { this->trigger(this->parent_); }); } + + protected: + GraphicalDisplayMenu *parent_; }; } // namespace graphical_display_menu diff --git a/esphome/components/gree/gree.cpp b/esphome/components/gree/gree.cpp index b8cf8a39a8..732ebd9632 100644 --- a/esphome/components/gree/gree.cpp +++ b/esphome/components/gree/gree.cpp @@ -87,19 +87,12 @@ void GreeClimate::transmit_state() { // Calculate the checksum if (this->model_ == GREE_YAN || this->model_ == GREE_YX1FF) { remote_state[7] = ((remote_state[0] << 4) + (remote_state[1] << 4) + 0xC0); - } else if (this->model_ == GREE_YAG) { + } else { remote_state[7] = ((((remote_state[0] & 0x0F) + (remote_state[1] & 0x0F) + (remote_state[2] & 0x0F) + (remote_state[3] & 0x0F) + ((remote_state[4] & 0xF0) >> 4) + ((remote_state[5] & 0xF0) >> 4) + ((remote_state[6] & 0xF0) >> 4) + 0x0A) & 0x0F) << 4); - } else { - remote_state[7] = - ((((remote_state[0] & 0x0F) + (remote_state[1] & 0x0F) + (remote_state[2] & 0x0F) + (remote_state[3] & 0x0F) + - ((remote_state[5] & 0xF0) >> 4) + ((remote_state[6] & 0xF0) >> 4) + ((remote_state[7] & 0xF0) >> 4) + 0x0A) & - 0x0F) - << 4) | - (remote_state[7] & 0x0F); } auto transmit = this->transmitter_->transmit(); @@ -180,7 +173,7 @@ uint8_t GreeClimate::operation_mode_() { uint8_t GreeClimate::fan_speed_() { // YX1FF has 4 fan speeds -- we treat low as quiet and turbo as high if (this->model_ == GREE_YX1FF) { - switch (this->fan_mode.value()) { + switch (this->fan_mode.value_or(climate::CLIMATE_FAN_ON)) { case climate::CLIMATE_FAN_QUIET: return GREE_FAN_1; case climate::CLIMATE_FAN_LOW: @@ -195,7 +188,7 @@ uint8_t GreeClimate::fan_speed_() { } } - switch (this->fan_mode.value()) { + switch (this->fan_mode.value_or(climate::CLIMATE_FAN_ON)) { case climate::CLIMATE_FAN_LOW: return GREE_FAN_1; case climate::CLIMATE_FAN_MEDIUM: @@ -235,7 +228,7 @@ uint8_t GreeClimate::temperature_() { uint8_t GreeClimate::preset_() { // YX1FF has sleep preset if (this->model_ == GREE_YX1FF) { - switch (this->preset.value()) { + switch (this->preset.value_or(climate::CLIMATE_PRESET_NONE)) { case climate::CLIMATE_PRESET_NONE: return GREE_PRESET_NONE; case climate::CLIMATE_PRESET_SLEEP: diff --git a/esphome/components/grove_tb6612fng/__init__.py b/esphome/components/grove_tb6612fng/__init__.py index 869c05387f..210e2f7bab 100644 --- a/esphome/components/grove_tb6612fng/__init__.py +++ b/esphome/components/grove_tb6612fng/__init__.py @@ -72,6 +72,7 @@ async def to_code(config): cv.Required(CONF_DIRECTION): cv.enum(DIRECTION_TYPE, upper=True), } ), + synchronous=True, ) async def grove_tb6612fng_run_to_code(config, action_id, template_arg, args): var = cg.new_Pvariable(action_id, template_arg) @@ -96,6 +97,7 @@ async def grove_tb6612fng_run_to_code(config, action_id, template_arg, args): cv.Required(CONF_CHANNEL): cv.templatable(cv.int_range(min=0, max=1)), } ), + synchronous=True, ) async def grove_tb6612fng_break_to_code(config, action_id, template_arg, args): var = cg.new_Pvariable(action_id, template_arg) @@ -115,6 +117,7 @@ async def grove_tb6612fng_break_to_code(config, action_id, template_arg, args): cv.Required(CONF_CHANNEL): cv.templatable(cv.int_range(min=0, max=1)), } ), + synchronous=True, ) async def grove_tb6612fng_stop_to_code(config, action_id, template_arg, args): var = cg.new_Pvariable(action_id, template_arg) @@ -133,6 +136,7 @@ async def grove_tb6612fng_stop_to_code(config, action_id, template_arg, args): cv.Required(CONF_ID): cv.use_id(GROVE_TB6612FNG), } ), + synchronous=True, ) async def grove_tb6612fng_standby_to_code(config, action_id, template_arg, args): var = cg.new_Pvariable(action_id, template_arg) @@ -149,6 +153,7 @@ async def grove_tb6612fng_standby_to_code(config, action_id, template_arg, args) cv.Required(CONF_ID): cv.use_id(GROVE_TB6612FNG), } ), + synchronous=True, ) async def grove_tb6612fng_no_standby_to_code(config, action_id, template_arg, args): var = cg.new_Pvariable(action_id, template_arg) @@ -166,6 +171,7 @@ async def grove_tb6612fng_no_standby_to_code(config, action_id, template_arg, ar cv.Required(CONF_ADDRESS): cv.i2c_address, } ), + synchronous=True, ) async def grove_tb6612fng_change_address_to_code(config, action_id, template_arg, args): var = cg.new_Pvariable(action_id, template_arg) diff --git a/esphome/components/grove_tb6612fng/grove_tb6612fng.cpp b/esphome/components/grove_tb6612fng/grove_tb6612fng.cpp index a249984647..c10fa4cf25 100644 --- a/esphome/components/grove_tb6612fng/grove_tb6612fng.cpp +++ b/esphome/components/grove_tb6612fng/grove_tb6612fng.cpp @@ -131,7 +131,7 @@ void GroveMotorDriveTB6612FNG::stepper_run(StepperModeTypeT mode, int16_t steps, buffer_[4] = ms_per_step; buffer_[5] = (ms_per_step >> 8); - if (this->write_register(GROVE_MOTOR_DRIVER_I2C_CMD_STEPPER_RUN, buffer_, 1) != i2c::ERROR_OK) { + if (this->write_register(GROVE_MOTOR_DRIVER_I2C_CMD_STEPPER_RUN, buffer_, 6) != i2c::ERROR_OK) { ESP_LOGW(TAG, "Run stepper failed!"); this->status_set_warning(); return; @@ -139,7 +139,8 @@ void GroveMotorDriveTB6612FNG::stepper_run(StepperModeTypeT mode, int16_t steps, } void GroveMotorDriveTB6612FNG::stepper_stop() { - if (this->write_register(GROVE_MOTOR_DRIVER_I2C_CMD_STEPPER_STOP, nullptr, 1) != i2c::ERROR_OK) { + uint8_t status = 0; + if (this->write_register(GROVE_MOTOR_DRIVER_I2C_CMD_STEPPER_STOP, &status, 1) != i2c::ERROR_OK) { ESP_LOGW(TAG, "Send stop stepper failed!"); this->status_set_warning(); return; diff --git a/esphome/components/growatt_solar/growatt_solar.cpp b/esphome/components/growatt_solar/growatt_solar.cpp index 686c1c232e..2997425872 100644 --- a/esphome/components/growatt_solar/growatt_solar.cpp +++ b/esphome/components/growatt_solar/growatt_solar.cpp @@ -26,7 +26,7 @@ void GrowattSolar::update() { } // The bus might be slow, or there might be other devices, or other components might be talking to our device. - if (this->waiting_for_response()) { + if (!this->ready_for_immediate_send()) { this->waiting_to_update_ = true; return; } diff --git a/esphome/components/growatt_solar/growatt_solar.h b/esphome/components/growatt_solar/growatt_solar.h index b0ddd4b99d..833d6a36dd 100644 --- a/esphome/components/growatt_solar/growatt_solar.h +++ b/esphome/components/growatt_solar/growatt_solar.h @@ -56,8 +56,8 @@ class GrowattSolar : public PollingComponent, public modbus::ModbusDevice { } protected: - bool waiting_to_update_; - uint32_t last_send_; + bool waiting_to_update_{false}; + uint32_t last_send_{0}; struct GrowattPhase { sensor::Sensor *voltage_sensor_{nullptr}; diff --git a/esphome/components/gt911/touchscreen/__init__.py b/esphome/components/gt911/touchscreen/__init__.py index 6c80ff280f..b850eeea8b 100644 --- a/esphome/components/gt911/touchscreen/__init__.py +++ b/esphome/components/gt911/touchscreen/__init__.py @@ -16,7 +16,7 @@ GT911Touchscreen = gt911_ns.class_( CONFIG_SCHEMA = touchscreen.TOUCHSCREEN_SCHEMA.extend( { cv.GenerateID(): cv.declare_id(GT911Touchscreen), - cv.Optional(CONF_INTERRUPT_PIN): pins.internal_gpio_input_pin_schema, + cv.Optional(CONF_INTERRUPT_PIN): pins.gpio_output_pin_schema, cv.Optional(CONF_RESET_PIN): pins.gpio_output_pin_schema, } ).extend(i2c.i2c_device_schema(0x5D)) diff --git a/esphome/components/gt911/touchscreen/gt911_touchscreen.cpp b/esphome/components/gt911/touchscreen/gt911_touchscreen.cpp index b11880a042..17bfa82cb4 100644 --- a/esphome/components/gt911/touchscreen/gt911_touchscreen.cpp +++ b/esphome/components/gt911/touchscreen/gt911_touchscreen.cpp @@ -2,6 +2,7 @@ #include "esphome/core/helpers.h" #include "esphome/core/log.h" +#include "esphome/core/gpio.h" namespace esphome { namespace gt911 { @@ -26,15 +27,17 @@ static const size_t MAX_BUTTONS = 4; // max number of buttons scanned void GT911Touchscreen::setup() { if (this->reset_pin_ != nullptr) { + // temporarily set the interrupt pin to output to control address selection this->reset_pin_->setup(); this->reset_pin_->digital_write(false); if (this->interrupt_pin_ != nullptr) { - // temporarily set the interrupt pin to output to control address selection + this->interrupt_pin_->setup(); this->interrupt_pin_->pin_mode(gpio::FLAG_OUTPUT); this->interrupt_pin_->digital_write(false); } delay(2); - this->reset_pin_->digital_write(true); // wait at least T3+T4 ms as per the datasheet + this->reset_pin_->digital_write(true); + // wait at least T3+T4 ms as per the datasheet this->set_timeout(5 + 50 + 1, [this] { this->setup_internal_(); }); return; } @@ -43,11 +46,10 @@ void GT911Touchscreen::setup() { void GT911Touchscreen::setup_internal_() { if (this->interrupt_pin_ != nullptr) { - // set pre-configured input mode - this->interrupt_pin_->setup(); + if (this->interrupt_pin_->is_internal()) + this->interrupt_pin_->pin_mode(gpio::FLAG_INPUT); } - // check the configuration of the int line. uint8_t data[4]; i2c::ErrorCode err = this->write(GET_SWITCHES, sizeof(GET_SWITCHES)); if (err != i2c::ERROR_OK && this->address_ == PRIMARY_ADDRESS) { @@ -58,12 +60,25 @@ void GT911Touchscreen::setup_internal_() { err = this->read(data, 1); if (err == i2c::ERROR_OK) { ESP_LOGD(TAG, "Switches ADDR: 0x%02X DATA: 0x%02X", this->address_, data[0]); + + // data[0] & 1 == 1 => controller uses falling edge => active-low + // data[0] & 1 == 0 => controller uses rising edge => active-high + bool active_high = !(data[0] & 1); + if (this->interrupt_pin_ != nullptr) { - this->attach_interrupt_(this->interrupt_pin_, - (data[0] & 1) ? gpio::INTERRUPT_FALLING_EDGE : gpio::INTERRUPT_RISING_EDGE); + if (this->interrupt_pin_->is_internal()) { + // Direct MCU pin: attach a hardware interrupt, no polling needed. + this->attach_interrupt_(static_cast(this->interrupt_pin_), + active_high ? gpio::INTERRUPT_RISING_EDGE : gpio::INTERRUPT_FALLING_EDGE); + ESP_LOGD(TAG, "Interrupt pin: hardware interrupt, active %s", active_high ? "HIGH" : "LOW"); + } else { + // IO expander pin: leave as output for configuration only. + ESP_LOGD(TAG, "Interrupt pin: IO expander polling mode, active %s", active_high ? "HIGH" : "LOW"); + } } } } + if (this->x_raw_max_ == 0 || this->y_raw_max_ == 0) { // no calibration? Attempt to read the max values from the touchscreen. if (err == i2c::ERROR_OK) { diff --git a/esphome/components/gt911/touchscreen/gt911_touchscreen.h b/esphome/components/gt911/touchscreen/gt911_touchscreen.h index 85025b5522..a6577b5879 100644 --- a/esphome/components/gt911/touchscreen/gt911_touchscreen.h +++ b/esphome/components/gt911/touchscreen/gt911_touchscreen.h @@ -30,7 +30,9 @@ class GT911Touchscreen : public touchscreen::Touchscreen, public i2c::I2CDevice void dump_config() override; bool can_proceed() override { return this->setup_done_; } - void set_interrupt_pin(InternalGPIOPin *pin) { this->interrupt_pin_ = pin; } + /// Set a interrupt pin (supports hardware interrupts or expander connected). + void set_interrupt_pin(GPIOPin *pin) { this->interrupt_pin_ = pin; } + void set_reset_pin(GPIOPin *pin) { this->reset_pin_ = pin; } void register_button_listener(GT911ButtonListener *listener) { this->button_listeners_.push_back(listener); } @@ -49,7 +51,7 @@ class GT911Touchscreen : public touchscreen::Touchscreen, public i2c::I2CDevice /// @brief True if the touchscreen setup has completed successfully. bool setup_done_{false}; - InternalGPIOPin *interrupt_pin_{nullptr}; + GPIOPin *interrupt_pin_{nullptr}; GPIOPin *reset_pin_{nullptr}; std::vector button_listeners_; uint8_t button_state_{0xFF}; // last button state. Initial FF guarantees first update. diff --git a/esphome/components/haier/climate.py b/esphome/components/haier/climate.py index 8c3649058f..9c2c999f25 100644 --- a/esphome/components/haier/climate.py +++ b/esphome/components/haier/climate.py @@ -3,15 +3,11 @@ import logging from esphome import automation import esphome.codegen as cg from esphome.components import climate, logger, uart -from esphome.components.climate import ( - CONF_CURRENT_TEMPERATURE, - ClimateMode, - ClimatePreset, - ClimateSwingMode, -) +from esphome.components.climate import ClimateMode, ClimatePreset, ClimateSwingMode import esphome.config_validation as cv from esphome.const import ( CONF_BEEPER, + CONF_CURRENT_TEMPERATURE, CONF_DISPLAY, CONF_ID, CONF_LEVEL, @@ -26,7 +22,6 @@ from esphome.const import ( CONF_SUPPORTED_SWING_MODES, CONF_TARGET_TEMPERATURE, CONF_TEMPERATURE_STEP, - CONF_TRIGGER_ID, CONF_VISUAL, CONF_WIFI, ) @@ -126,21 +121,6 @@ SUPPORTED_HON_CONTROL_METHODS = { "SET_SINGLE_PARAMETER": HonControlMethod.SET_SINGLE_PARAMETER, } -HaierAlarmStartTrigger = haier_ns.class_( - "HaierAlarmStartTrigger", - automation.Trigger.template(cg.uint8, cg.const_char_ptr), -) - -HaierAlarmEndTrigger = haier_ns.class_( - "HaierAlarmEndTrigger", - automation.Trigger.template(cg.uint8, cg.const_char_ptr), -) - -StatusMessageTrigger = haier_ns.class_( - "StatusMessageTrigger", - automation.Trigger.template(cg.const_char_ptr, cg.size_t), -) - def validate_visual(config): if CONF_VISUAL in config: @@ -207,13 +187,7 @@ def _base_config_schema(class_: MockObjClass) -> cv.Schema: cv.Optional( CONF_ANSWER_TIMEOUT, ): cv.positive_time_period_milliseconds, - cv.Optional(CONF_ON_STATUS_MESSAGE): automation.validate_automation( - { - cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id( - StatusMessageTrigger - ), - } - ), + cv.Optional(CONF_ON_STATUS_MESSAGE): automation.validate_automation({}), } ) .extend(uart.UART_DEVICE_SCHEMA) @@ -268,19 +242,9 @@ CONFIG_SCHEMA = cv.All( f"The {CONF_OUTDOOR_TEMPERATURE} option is deprecated, use a sensor for a haier platform instead" ), cv.Optional(CONF_ON_ALARM_START): automation.validate_automation( - { - cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id( - HaierAlarmStartTrigger - ), - } - ), - cv.Optional(CONF_ON_ALARM_END): automation.validate_automation( - { - cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id( - HaierAlarmEndTrigger - ), - } + {} ), + cv.Optional(CONF_ON_ALARM_END): automation.validate_automation({}), } ), }, @@ -323,10 +287,16 @@ HAIER_HON_BASE_ACTION_SCHEMA = automation.maybe_simple_id( @automation.register_action( - "climate.haier.display_on", DisplayOnAction, HAIER_BASE_ACTION_SCHEMA + "climate.haier.display_on", + DisplayOnAction, + HAIER_BASE_ACTION_SCHEMA, + synchronous=True, ) @automation.register_action( - "climate.haier.display_off", DisplayOffAction, HAIER_BASE_ACTION_SCHEMA + "climate.haier.display_off", + DisplayOffAction, + HAIER_BASE_ACTION_SCHEMA, + synchronous=True, ) async def display_action_to_code(config, action_id, template_arg, args): paren = await cg.get_variable(config[CONF_ID]) @@ -334,10 +304,16 @@ async def display_action_to_code(config, action_id, template_arg, args): @automation.register_action( - "climate.haier.beeper_on", BeeperOnAction, HAIER_HON_BASE_ACTION_SCHEMA + "climate.haier.beeper_on", + BeeperOnAction, + HAIER_HON_BASE_ACTION_SCHEMA, + synchronous=True, ) @automation.register_action( - "climate.haier.beeper_off", BeeperOffAction, HAIER_HON_BASE_ACTION_SCHEMA + "climate.haier.beeper_off", + BeeperOffAction, + HAIER_HON_BASE_ACTION_SCHEMA, + synchronous=True, ) async def beeper_action_to_code(config, action_id, template_arg, args): paren = await cg.get_variable(config[CONF_ID]) @@ -349,11 +325,13 @@ async def beeper_action_to_code(config, action_id, template_arg, args): "climate.haier.start_self_cleaning", StartSelfCleaningAction, HAIER_HON_BASE_ACTION_SCHEMA, + synchronous=True, ) @automation.register_action( "climate.haier.start_steri_cleaning", StartSteriCleaningAction, HAIER_HON_BASE_ACTION_SCHEMA, + synchronous=True, ) async def start_cleaning_to_code(config, action_id, template_arg, args): paren = await cg.get_variable(config[CONF_ID]) @@ -372,6 +350,7 @@ async def start_cleaning_to_code(config, action_id, template_arg, args): ), } ), + synchronous=True, ) async def haier_set_vertical_airflow_to_code(config, action_id, template_arg, args): paren = await cg.get_variable(config[CONF_ID]) @@ -395,6 +374,7 @@ async def haier_set_vertical_airflow_to_code(config, action_id, template_arg, ar ), } ), + synchronous=True, ) async def haier_set_horizontal_airflow_to_code(config, action_id, template_arg, args): paren = await cg.get_variable(config[CONF_ID]) @@ -407,10 +387,16 @@ async def haier_set_horizontal_airflow_to_code(config, action_id, template_arg, @automation.register_action( - "climate.haier.health_on", HealthOnAction, HAIER_BASE_ACTION_SCHEMA + "climate.haier.health_on", + HealthOnAction, + HAIER_BASE_ACTION_SCHEMA, + synchronous=True, ) @automation.register_action( - "climate.haier.health_off", HealthOffAction, HAIER_BASE_ACTION_SCHEMA + "climate.haier.health_off", + HealthOffAction, + HAIER_BASE_ACTION_SCHEMA, + synchronous=True, ) async def health_action_to_code(config, action_id, template_arg, args): paren = await cg.get_variable(config[CONF_ID]) @@ -418,13 +404,22 @@ async def health_action_to_code(config, action_id, template_arg, args): @automation.register_action( - "climate.haier.power_on", PowerOnAction, HAIER_BASE_ACTION_SCHEMA + "climate.haier.power_on", + PowerOnAction, + HAIER_BASE_ACTION_SCHEMA, + synchronous=True, ) @automation.register_action( - "climate.haier.power_off", PowerOffAction, HAIER_BASE_ACTION_SCHEMA + "climate.haier.power_off", + PowerOffAction, + HAIER_BASE_ACTION_SCHEMA, + synchronous=True, ) @automation.register_action( - "climate.haier.power_toggle", PowerToggleAction, HAIER_BASE_ACTION_SCHEMA + "climate.haier.power_toggle", + PowerToggleAction, + HAIER_BASE_ACTION_SCHEMA, + synchronous=True, ) async def power_action_to_code(config, action_id, template_arg, args): paren = await cg.get_variable(config[CONF_ID]) @@ -503,19 +498,25 @@ async def to_code(config): var.set_status_message_header_size(config[CONF_STATUS_MESSAGE_HEADER_SIZE]) ) for conf in config.get(CONF_ON_ALARM_START, []): - trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) - await automation.build_automation( - trigger, [(cg.uint8, "code"), (cg.const_char_ptr, "message")], conf + await automation.build_callback_automation( + var, + "add_alarm_start_callback", + [(cg.uint8, "code"), (cg.const_char_ptr, "message")], + conf, ) for conf in config.get(CONF_ON_ALARM_END, []): - trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) - await automation.build_automation( - trigger, [(cg.uint8, "code"), (cg.const_char_ptr, "message")], conf + await automation.build_callback_automation( + var, + "add_alarm_end_callback", + [(cg.uint8, "code"), (cg.const_char_ptr, "message")], + conf, ) for conf in config.get(CONF_ON_STATUS_MESSAGE, []): - trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) - await automation.build_automation( - trigger, [(cg.const_char_ptr, "data"), (cg.size_t, "data_size")], conf + await automation.build_callback_automation( + var, + "add_status_message_callback", + [(cg.const_char_ptr, "data"), (cg.size_t, "data_size")], + conf, ) # https://github.com/paveldn/HaierProtocol cg.add_library("pavlodn/HaierProtocol", "0.9.31") diff --git a/esphome/components/haier/haier_base.cpp b/esphome/components/haier/haier_base.cpp index 1882aa439e..4a06066d3c 100644 --- a/esphome/components/haier/haier_base.cpp +++ b/esphome/components/haier/haier_base.cpp @@ -197,10 +197,6 @@ void HaierClimateBase::send_custom_command(const haier_protocol::HaierMessage &m this->action_request_ = PendingAction({ActionRequest::SEND_CUSTOM_COMMAND, message}); } -void HaierClimateBase::add_status_message_callback(std::function &&callback) { - this->status_message_callback_.add(std::move(callback)); -} - haier_protocol::HandlerError HaierClimateBase::answer_preprocess_( haier_protocol::FrameType request_message_type, haier_protocol::FrameType expected_request_message_type, haier_protocol::FrameType answer_message_type, haier_protocol::FrameType expected_answer_message_type, @@ -246,7 +242,7 @@ void HaierClimateBase::setup() { this->last_request_timestamp_ = std::chrono::steady_clock::now(); this->set_phase(ProtocolPhases::SENDING_INIT_1); this->haier_protocol_.set_default_timeout_handler( - std::bind(&esphome::haier::HaierClimateBase::timeout_default_handler_, this, std::placeholders::_1)); + [this](haier_protocol::FrameType type) { return this->timeout_default_handler_(type); }); this->set_handlers(); this->initialization(); } diff --git a/esphome/components/haier/haier_base.h b/esphome/components/haier/haier_base.h index e24217bfd9..0c416623c0 100644 --- a/esphome/components/haier/haier_base.h +++ b/esphome/components/haier/haier_base.h @@ -74,7 +74,9 @@ class HaierClimateBase : public esphome::Component, void set_answer_timeout(uint32_t timeout); void set_send_wifi(bool send_wifi); void send_custom_command(const haier_protocol::HaierMessage &message); - void add_status_message_callback(std::function &&callback); + template void add_status_message_callback(F &&callback) { + this->status_message_callback_.add(std::forward(callback)); + } protected: enum class ProtocolPhases { @@ -175,12 +177,5 @@ class HaierClimateBase : public esphome::Component, ESPPreferenceObject base_rtc_; }; -class StatusMessageTrigger : public Trigger { - public: - explicit StatusMessageTrigger(HaierClimateBase *parent) { - parent->add_status_message_callback([this](const char *data, size_t data_size) { this->trigger(data, data_size); }); - } -}; - } // namespace haier } // namespace esphome diff --git a/esphome/components/haier/hon_climate.cpp b/esphome/components/haier/hon_climate.cpp index d98d273957..1e9cb42f38 100644 --- a/esphome/components/haier/hon_climate.cpp +++ b/esphome/components/haier/hon_climate.cpp @@ -114,14 +114,6 @@ void HonClimate::start_steri_cleaning() { } } -void HonClimate::add_alarm_start_callback(std::function &&callback) { - this->alarm_start_callback_.add(std::move(callback)); -} - -void HonClimate::add_alarm_end_callback(std::function &&callback) { - this->alarm_end_callback_.add(std::move(callback)); -} - haier_protocol::HandlerError HonClimate::get_device_version_answer_handler_(haier_protocol::FrameType request_type, haier_protocol::FrameType message_type, const uint8_t *data, size_t data_size) { @@ -309,32 +301,38 @@ void HonClimate::set_handlers() { // Set handlers this->haier_protocol_.set_answer_handler( haier_protocol::FrameType::GET_DEVICE_VERSION, - std::bind(&HonClimate::get_device_version_answer_handler_, this, std::placeholders::_1, std::placeholders::_2, - std::placeholders::_3, std::placeholders::_4)); + [this](haier_protocol::FrameType req, haier_protocol::FrameType msg, const uint8_t *data, size_t size) { + return this->get_device_version_answer_handler_(req, msg, data, size); + }); this->haier_protocol_.set_answer_handler( haier_protocol::FrameType::GET_DEVICE_ID, - std::bind(&HonClimate::get_device_id_answer_handler_, this, std::placeholders::_1, std::placeholders::_2, - std::placeholders::_3, std::placeholders::_4)); + [this](haier_protocol::FrameType req, haier_protocol::FrameType msg, const uint8_t *data, size_t size) { + return this->get_device_id_answer_handler_(req, msg, data, size); + }); this->haier_protocol_.set_answer_handler( haier_protocol::FrameType::CONTROL, - std::bind(&HonClimate::status_handler_, this, std::placeholders::_1, std::placeholders::_2, std::placeholders::_3, - std::placeholders::_4)); + [this](haier_protocol::FrameType req, haier_protocol::FrameType msg, const uint8_t *data, size_t size) { + return this->status_handler_(req, msg, data, size); + }); this->haier_protocol_.set_answer_handler( haier_protocol::FrameType::GET_MANAGEMENT_INFORMATION, - std::bind(&HonClimate::get_management_information_answer_handler_, this, std::placeholders::_1, - std::placeholders::_2, std::placeholders::_3, std::placeholders::_4)); + [this](haier_protocol::FrameType req, haier_protocol::FrameType msg, const uint8_t *data, size_t size) { + return this->get_management_information_answer_handler_(req, msg, data, size); + }); this->haier_protocol_.set_answer_handler( haier_protocol::FrameType::GET_ALARM_STATUS, - std::bind(&HonClimate::get_alarm_status_answer_handler_, this, std::placeholders::_1, std::placeholders::_2, - std::placeholders::_3, std::placeholders::_4)); + [this](haier_protocol::FrameType req, haier_protocol::FrameType msg, const uint8_t *data, size_t size) { + return this->get_alarm_status_answer_handler_(req, msg, data, size); + }); this->haier_protocol_.set_answer_handler( haier_protocol::FrameType::REPORT_NETWORK_STATUS, - std::bind(&HonClimate::report_network_status_answer_handler_, this, std::placeholders::_1, std::placeholders::_2, - std::placeholders::_3, std::placeholders::_4)); - this->haier_protocol_.set_message_handler( - haier_protocol::FrameType::ALARM_STATUS, - std::bind(&HonClimate::alarm_status_message_handler_, this, std::placeholders::_1, std::placeholders::_2, - std::placeholders::_3)); + [this](haier_protocol::FrameType req, haier_protocol::FrameType msg, const uint8_t *data, size_t size) { + return this->report_network_status_answer_handler_(req, msg, data, size); + }); + this->haier_protocol_.set_message_handler(haier_protocol::FrameType::ALARM_STATUS, + [this](haier_protocol::FrameType type, const uint8_t *data, size_t size) { + return this->alarm_status_message_handler_(type, data, size); + }); } void HonClimate::dump_config() { @@ -677,7 +675,6 @@ haier_protocol::HaierMessage HonClimate::get_control_message() { this->quiet_mode_state_ = (SwitchState) ((uint8_t) this->quiet_mode_state_ & 0b01); } out_data->beeper_status = ((!this->get_beeper_state()) || (!has_hvac_settings)) ? 1 : 0; - control_out_buffer[4] = 0; // This byte should be cleared before setting values out_data->display_status = this->get_display_state() ? 1 : 0; this->display_status_ = (SwitchState) ((uint8_t) this->display_status_ & 0b01); out_data->health_mode = this->get_health_mode() ? 1 : 0; @@ -750,7 +747,7 @@ void HonClimate::update_sub_sensor_(SubSensorType type, float value) { if (type < SubSensorType::SUB_SENSOR_TYPE_COUNT) { size_t index = (size_t) type; if ((this->sub_sensors_[index] != nullptr) && - ((!this->sub_sensors_[index]->has_state()) || (this->sub_sensors_[index]->raw_state != value))) + ((!this->sub_sensors_[index]->has_state()) || (this->sub_sensors_[index]->get_raw_state() != value))) this->sub_sensors_[index]->publish_state(value); } } @@ -893,7 +890,8 @@ haier_protocol::HandlerError HonClimate::process_status_message_(const uint8_t * } else { this->preset = CLIMATE_PRESET_NONE; } - should_publish = should_publish || (!old_preset.has_value()) || (old_preset.value() != this->preset.value()); + should_publish = should_publish || (!old_preset.has_value()) || + (old_preset.value_or(CLIMATE_PRESET_NONE) != this->preset.value_or(CLIMATE_PRESET_NONE)); } { // Target temperature @@ -936,7 +934,8 @@ haier_protocol::HandlerError HonClimate::process_status_message_(const uint8_t * this->fan_mode = CLIMATE_FAN_HIGH; break; } - should_publish = should_publish || (!old_fan_mode.has_value()) || (old_fan_mode.value() != fan_mode.value()); + should_publish = should_publish || (!old_fan_mode.has_value()) || + (old_fan_mode.value_or(CLIMATE_FAN_ON) != this->fan_mode.value_or(CLIMATE_FAN_ON)); } // Display status // should be before "Climate mode" because it is changing this->mode @@ -1301,7 +1300,8 @@ void HonClimate::clear_control_messages_queue_() { } bool HonClimate::prepare_pending_action() { - switch (this->action_request_.value().action) { + auto &action_request = this->action_request_.value(); // NOLINT(bugprone-unchecked-optional-access) + switch (action_request.action) { case ActionRequest::START_SELF_CLEAN: if (this->control_method_ == HonControlMethod::SET_GROUP_PARAMETERS) { uint8_t control_out_buffer[haier_protocol::MAX_FRAME_SIZE]; @@ -1315,12 +1315,12 @@ bool HonClimate::prepare_pending_action() { out_data->ac_power = 1; out_data->ac_mode = (uint8_t) hon_protocol::ConditioningMode::DRY; out_data->light_status = 0; - this->action_request_.value().message = haier_protocol::HaierMessage( + action_request.message = haier_protocol::HaierMessage( haier_protocol::FrameType::CONTROL, (uint16_t) hon_protocol::SubcommandsControl::SET_GROUP_PARAMETERS, control_out_buffer, this->real_control_packet_size_); return true; } else if (this->control_method_ == HonControlMethod::SET_SINGLE_PARAMETER) { - this->action_request_.value().message = + action_request.message = haier_protocol::HaierMessage(haier_protocol::FrameType::CONTROL, (uint16_t) hon_protocol::SubcommandsControl::SET_SINGLE_PARAMETER + (uint8_t) hon_protocol::DataParameters::SELF_CLEANING, @@ -1343,7 +1343,7 @@ bool HonClimate::prepare_pending_action() { out_data->ac_power = 1; out_data->ac_mode = (uint8_t) hon_protocol::ConditioningMode::DRY; out_data->light_status = 0; - this->action_request_.value().message = haier_protocol::HaierMessage( + action_request.message = haier_protocol::HaierMessage( haier_protocol::FrameType::CONTROL, (uint16_t) hon_protocol::SubcommandsControl::SET_GROUP_PARAMETERS, control_out_buffer, this->real_control_packet_size_); return true; @@ -1372,9 +1372,8 @@ void HonClimate::process_protocol_reset() { bool HonClimate::should_get_big_data_() { if (this->big_data_sensors_ > 0) { - static uint8_t counter = 0; - counter = (counter + 1) % 3; - return counter == 1; + this->big_data_counter_ = (this->big_data_counter_ + 1) % 3; + return this->big_data_counter_ == 1; } return false; } diff --git a/esphome/components/haier/hon_climate.h b/esphome/components/haier/hon_climate.h index a567ab1d89..7a87f27b66 100644 --- a/esphome/components/haier/hon_climate.h +++ b/esphome/components/haier/hon_climate.h @@ -1,6 +1,7 @@ #pragma once #include +#include #ifdef USE_SENSOR #include "esphome/components/sensor/sensor.h" #endif @@ -29,10 +30,10 @@ enum class CleaningState : uint8_t { enum class HonControlMethod { MONITOR_ONLY = 0, SET_GROUP_PARAMETERS, SET_SINGLE_PARAMETER }; struct HonSettings { - hon_protocol::VerticalSwingMode last_vertiacal_swing; - hon_protocol::HorizontalSwingMode last_horizontal_swing; - bool beeper_state; - bool quiet_mode_state; + hon_protocol::VerticalSwingMode last_vertiacal_swing{hon_protocol::VerticalSwingMode::CENTER}; + hon_protocol::HorizontalSwingMode last_horizontal_swing{hon_protocol::HorizontalSwingMode::CENTER}; + bool beeper_state{true}; + bool quiet_mode_state{false}; }; class HonClimate : public HaierClimateBase { @@ -123,8 +124,12 @@ class HonClimate : public HaierClimateBase { void set_extra_sensors_packet_bytes_size(size_t size) { this->extra_sensors_packet_bytes_ = size; }; void set_status_message_header_size(size_t size) { this->status_message_header_size_ = size; }; void set_control_method(HonControlMethod method) { this->control_method_ = method; }; - void add_alarm_start_callback(std::function &&callback); - void add_alarm_end_callback(std::function &&callback); + template void add_alarm_start_callback(F &&callback) { + this->alarm_start_callback_.add(std::forward(callback)); + } + template void add_alarm_end_callback(F &&callback) { + this->alarm_end_callback_.add(std::forward(callback)); + } float get_active_alarm_count() const { return this->active_alarm_count_; } protected: @@ -187,28 +192,13 @@ class HonClimate : public HaierClimateBase { float active_alarm_count_{NAN}; std::chrono::steady_clock::time_point last_alarm_request_; int big_data_sensors_{0}; + uint8_t big_data_counter_{0}; esphome::optional current_vertical_swing_{}; esphome::optional current_horizontal_swing_{}; - HonSettings settings_; + HonSettings settings_{}; ESPPreferenceObject hon_rtc_; SwitchState quiet_mode_state_{SwitchState::OFF}; }; -class HaierAlarmStartTrigger : public Trigger { - public: - explicit HaierAlarmStartTrigger(HonClimate *parent) { - parent->add_alarm_start_callback( - [this](uint8_t alarm_code, const char *alarm_message) { this->trigger(alarm_code, alarm_message); }); - } -}; - -class HaierAlarmEndTrigger : public Trigger { - public: - explicit HaierAlarmEndTrigger(HonClimate *parent) { - parent->add_alarm_end_callback( - [this](uint8_t alarm_code, const char *alarm_message) { this->trigger(alarm_code, alarm_message); }); - } -}; - } // namespace haier } // namespace esphome diff --git a/esphome/components/haier/smartair2_climate.cpp b/esphome/components/haier/smartair2_climate.cpp index 63c22821b3..2be5d13050 100644 --- a/esphome/components/haier/smartair2_climate.cpp +++ b/esphome/components/haier/smartair2_climate.cpp @@ -106,18 +106,21 @@ void Smartair2Climate::set_handlers() { // Set handlers this->haier_protocol_.set_answer_handler( haier_protocol::FrameType::GET_DEVICE_VERSION, - std::bind(&Smartair2Climate::get_device_version_answer_handler_, this, std::placeholders::_1, - std::placeholders::_2, std::placeholders::_3, std::placeholders::_4)); + [this](haier_protocol::FrameType req, haier_protocol::FrameType msg, const uint8_t *data, size_t size) { + return this->get_device_version_answer_handler_(req, msg, data, size); + }); this->haier_protocol_.set_answer_handler( haier_protocol::FrameType::CONTROL, - std::bind(&Smartair2Climate::status_handler_, this, std::placeholders::_1, std::placeholders::_2, - std::placeholders::_3, std::placeholders::_4)); + [this](haier_protocol::FrameType req, haier_protocol::FrameType msg, const uint8_t *data, size_t size) { + return this->status_handler_(req, msg, data, size); + }); this->haier_protocol_.set_answer_handler( haier_protocol::FrameType::REPORT_NETWORK_STATUS, - std::bind(&Smartair2Climate::report_network_status_answer_handler_, this, std::placeholders::_1, - std::placeholders::_2, std::placeholders::_3, std::placeholders::_4)); + [this](haier_protocol::FrameType req, haier_protocol::FrameType msg, const uint8_t *data, size_t size) { + return this->report_network_status_answer_handler_(req, msg, data, size); + }); this->haier_protocol_.set_default_timeout_handler( - std::bind(&Smartair2Climate::messages_timeout_handler_with_cycle_for_init_, this, std::placeholders::_1)); + [this](haier_protocol::FrameType type) { return this->messages_timeout_handler_with_cycle_for_init_(type); }); } void Smartair2Climate::dump_config() { @@ -385,7 +388,7 @@ haier_protocol::HaierMessage Smartair2Climate::get_control_message() { } haier_protocol::HandlerError Smartair2Climate::process_status_message_(const uint8_t *packet_buffer, uint8_t size) { - if (size < sizeof(smartair2_protocol::HaierStatus)) + if (size != sizeof(smartair2_protocol::HaierStatus)) return haier_protocol::HandlerError::WRONG_MESSAGE_STRUCTURE; smartair2_protocol::HaierStatus packet; memcpy(&packet, packet_buffer, size); @@ -402,7 +405,8 @@ haier_protocol::HandlerError Smartair2Climate::process_status_message_(const uin } else { this->preset = CLIMATE_PRESET_NONE; } - should_publish = should_publish || (!old_preset.has_value()) || (old_preset.value() != this->preset.value()); + should_publish = should_publish || (!old_preset.has_value()) || + (old_preset.value_or(CLIMATE_PRESET_NONE) != this->preset.value_or(CLIMATE_PRESET_NONE)); } { // Target temperature @@ -446,7 +450,8 @@ haier_protocol::HandlerError Smartair2Climate::process_status_message_(const uin this->fan_mode = CLIMATE_FAN_HIGH; break; } - should_publish = should_publish || (!old_fan_mode.has_value()) || (old_fan_mode.value() != fan_mode.value()); + should_publish = should_publish || (!old_fan_mode.has_value()) || + (old_fan_mode.value_or(CLIMATE_FAN_ON) != this->fan_mode.value_or(CLIMATE_FAN_ON)); } // Display status // should be before "Climate mode" because it is changing this->mode diff --git a/esphome/components/hbridge/fan/__init__.py b/esphome/components/hbridge/fan/__init__.py index 31a20a8981..8ea8677ba2 100644 --- a/esphome/components/hbridge/fan/__init__.py +++ b/esphome/components/hbridge/fan/__init__.py @@ -52,6 +52,7 @@ CONFIG_SCHEMA = ( "fan.hbridge.brake", BrakeAction, maybe_simple_id({cv.GenerateID(): cv.use_id(HBridgeFan)}), + synchronous=True, ) async def fan_hbridge_brake_to_code(config, action_id, template_arg, args): paren = await cg.get_variable(config[CONF_ID]) diff --git a/esphome/components/hbridge/fan/hbridge_fan.cpp b/esphome/components/hbridge/fan/hbridge_fan.cpp index 9bf58f9d1e..89c162eebf 100644 --- a/esphome/components/hbridge/fan/hbridge_fan.cpp +++ b/esphome/components/hbridge/fan/hbridge_fan.cpp @@ -28,15 +28,15 @@ fan::FanCall HBridgeFan::brake() { } void HBridgeFan::setup() { + // Construct traits before restore so preset modes can be looked up by index + this->traits_ = fan::FanTraits(this->oscillating_ != nullptr, true, true, this->speed_count_); + this->traits_.set_supported_preset_modes(this->preset_modes_); + auto restore = this->restore_state_(); if (restore.has_value()) { restore->apply(*this); this->write_state_(); } - - // Construct traits - this->traits_ = fan::FanTraits(this->oscillating_ != nullptr, true, true, this->speed_count_); - this->traits_.set_supported_preset_modes(this->preset_modes_); } void HBridgeFan::dump_config() { @@ -49,14 +49,18 @@ void HBridgeFan::dump_config() { } void HBridgeFan::control(const fan::FanCall &call) { - if (call.get_state().has_value()) - this->state = *call.get_state(); - if (call.get_speed().has_value()) - this->speed = *call.get_speed(); - if (call.get_oscillating().has_value()) - this->oscillating = *call.get_oscillating(); - if (call.get_direction().has_value()) - this->direction = *call.get_direction(); + auto call_state = call.get_state(); + if (call_state.has_value()) + this->state = *call_state; + auto call_speed = call.get_speed(); + if (call_speed.has_value()) + this->speed = *call_speed; + auto call_oscillating = call.get_oscillating(); + if (call_oscillating.has_value()) + this->oscillating = *call_oscillating; + auto call_direction = call.get_direction(); + if (call_direction.has_value()) + this->direction = *call_direction; this->apply_preset_mode_(call); this->write_state_(); diff --git a/esphome/components/hc8/hc8.cpp b/esphome/components/hc8/hc8.cpp index 4d0f77df1b..900acca691 100644 --- a/esphome/components/hc8/hc8.cpp +++ b/esphome/components/hc8/hc8.cpp @@ -24,12 +24,16 @@ void HC8Component::setup() { } void HC8Component::update() { - uint32_t now_ms = App.get_loop_component_start_time(); - uint32_t warmup_ms = this->warmup_seconds_ * 1000; - if (now_ms < warmup_ms) { - ESP_LOGW(TAG, "HC8 warming up, %" PRIu32 " s left", (warmup_ms - now_ms) / 1000); - this->status_set_warning(); - return; + if (!this->warmup_complete_) { + uint32_t now_ms = App.get_loop_component_start_time(); + uint32_t warmup_ms = this->warmup_seconds_ * 1000; + if (now_ms < warmup_ms) { + ESP_LOGW(TAG, "HC8 warming up, %" PRIu32 " s left", (warmup_ms - now_ms) / 1000); + this->status_set_warning(); + return; + } + this->warmup_complete_ = true; + this->status_clear_warning(); } while (this->available()) @@ -86,8 +90,6 @@ void HC8Component::calibrate(uint16_t baseline) { this->flush(); } -float HC8Component::get_setup_priority() const { return setup_priority::DATA; } - void HC8Component::dump_config() { ESP_LOGCONFIG(TAG, "HC8:\n" diff --git a/esphome/components/hc8/hc8.h b/esphome/components/hc8/hc8.h index 7711fb8c97..b060f38a80 100644 --- a/esphome/components/hc8/hc8.h +++ b/esphome/components/hc8/hc8.h @@ -11,8 +11,6 @@ namespace esphome::hc8 { class HC8Component : public PollingComponent, public uart::UARTDevice { public: - float get_setup_priority() const override; - void setup() override; void update() override; void dump_config() override; @@ -25,6 +23,7 @@ class HC8Component : public PollingComponent, public uart::UARTDevice { protected: sensor::Sensor *co2_sensor_{nullptr}; uint32_t warmup_seconds_{0}; + bool warmup_complete_{false}; }; template class HC8CalibrateAction : public Action, public Parented { diff --git a/esphome/components/hc8/sensor.py b/esphome/components/hc8/sensor.py index 2f39b47f3c..29b428e310 100644 --- a/esphome/components/hc8/sensor.py +++ b/esphome/components/hc8/sensor.py @@ -68,7 +68,10 @@ CALIBRATION_ACTION_SCHEMA = cv.Schema( @automation.register_action( - "hc8.calibrate", HC8CalibrateAction, CALIBRATION_ACTION_SCHEMA + "hc8.calibrate", + HC8CalibrateAction, + CALIBRATION_ACTION_SCHEMA, + synchronous=True, ) async def hc8_calibration_to_code(config, action_id, template_arg, args): var = cg.new_Pvariable(action_id, template_arg) diff --git a/esphome/components/hdc1080/hdc1080.h b/esphome/components/hdc1080/hdc1080.h index 7ad0764f1f..a5bece82c4 100644 --- a/esphome/components/hdc1080/hdc1080.h +++ b/esphome/components/hdc1080/hdc1080.h @@ -16,8 +16,6 @@ class HDC1080Component : public PollingComponent, public i2c::I2CDevice { void dump_config() override; void update() override; - float get_setup_priority() const override { return setup_priority::DATA; } - protected: sensor::Sensor *temperature_{nullptr}; sensor::Sensor *humidity_{nullptr}; diff --git a/esphome/components/hdc2010/hdc2010.cpp b/esphome/components/hdc2010/hdc2010.cpp index c53fdb3f5b..0334b30eec 100644 --- a/esphome/components/hdc2010/hdc2010.cpp +++ b/esphome/components/hdc2010/hdc2010.cpp @@ -7,50 +7,36 @@ namespace hdc2010 { static const char *const TAG = "hdc2010"; -static const uint8_t HDC2010_ADDRESS = 0x40; // 0b1000000 or 0b1000001 from datasheet -static const uint8_t HDC2010_CMD_CONFIGURATION_MEASUREMENT = 0x8F; -static const uint8_t HDC2010_CMD_START_MEASUREMENT = 0xF9; -static const uint8_t HDC2010_CMD_TEMPERATURE_LOW = 0x00; -static const uint8_t HDC2010_CMD_TEMPERATURE_HIGH = 0x01; -static const uint8_t HDC2010_CMD_HUMIDITY_LOW = 0x02; -static const uint8_t HDC2010_CMD_HUMIDITY_HIGH = 0x03; -static const uint8_t CONFIG = 0x0E; -static const uint8_t MEASUREMENT_CONFIG = 0x0F; +// Register addresses +static constexpr uint8_t REG_TEMPERATURE_LOW = 0x00; +static constexpr uint8_t REG_TEMPERATURE_HIGH = 0x01; +static constexpr uint8_t REG_HUMIDITY_LOW = 0x02; +static constexpr uint8_t REG_HUMIDITY_HIGH = 0x03; +static constexpr uint8_t REG_RESET_DRDY_INT_CONF = 0x0E; +static constexpr uint8_t REG_MEASUREMENT_CONF = 0x0F; + +// REG_MEASUREMENT_CONF (0x0F) bit masks +static constexpr uint8_t MEAS_TRIG = 0x01; // Bit 0: measurement trigger +static constexpr uint8_t MEAS_CONF_MASK = 0x06; // Bits 2:1: measurement mode +static constexpr uint8_t HRES_MASK = 0x30; // Bits 5:4: humidity resolution +static constexpr uint8_t TRES_MASK = 0xC0; // Bits 7:6: temperature resolution + +// REG_RESET_DRDY_INT_CONF (0x0E) bit masks +static constexpr uint8_t AMM_MASK = 0x70; // Bits 6:4: auto measurement mode void HDC2010Component::setup() { ESP_LOGCONFIG(TAG, "Running setup"); - const uint8_t data[2] = { - 0b00000000, // resolution 14bit for both humidity and temperature - 0b00000000 // reserved - }; - - if (!this->write_bytes(HDC2010_CMD_CONFIGURATION_MEASUREMENT, data, 2)) { - ESP_LOGW(TAG, "Initial config instruction error"); - this->status_set_warning(); - return; - } - - // Set measurement mode to temperature and humidity + // Set 14-bit resolution for both sensors and measurement mode to temp + humidity uint8_t config_contents; - this->read_register(MEASUREMENT_CONFIG, &config_contents, 1); - config_contents = (config_contents & 0xF9); // Always set to TEMP_AND_HUMID mode - this->write_bytes(MEASUREMENT_CONFIG, &config_contents, 1); + this->read_register(REG_MEASUREMENT_CONF, &config_contents, 1); + config_contents &= ~(TRES_MASK | HRES_MASK | MEAS_CONF_MASK); // 14-bit temp, 14-bit humidity, temp+humidity mode + this->write_bytes(REG_MEASUREMENT_CONF, &config_contents, 1); - // Set rate to manual - this->read_register(CONFIG, &config_contents, 1); - config_contents &= 0x8F; - this->write_bytes(CONFIG, &config_contents, 1); - - // Set temperature resolution to 14bit - this->read_register(CONFIG, &config_contents, 1); - config_contents &= 0x3F; - this->write_bytes(CONFIG, &config_contents, 1); - - // Set humidity resolution to 14bit - this->read_register(CONFIG, &config_contents, 1); - config_contents &= 0xCF; - this->write_bytes(CONFIG, &config_contents, 1); + // Set auto measurement rate to manual (on-demand via MEAS_TRIG) + this->read_register(REG_RESET_DRDY_INT_CONF, &config_contents, 1); + config_contents &= ~AMM_MASK; + this->write_bytes(REG_RESET_DRDY_INT_CONF, &config_contents, 1); } void HDC2010Component::dump_config() { @@ -67,9 +53,9 @@ void HDC2010Component::dump_config() { void HDC2010Component::update() { // Trigger measurement uint8_t config_contents; - this->read_register(CONFIG, &config_contents, 1); - config_contents |= 0x01; - this->write_bytes(MEASUREMENT_CONFIG, &config_contents, 1); + this->read_register(REG_MEASUREMENT_CONF, &config_contents, 1); + config_contents |= MEAS_TRIG; + this->write_bytes(REG_MEASUREMENT_CONF, &config_contents, 1); // 1ms delay after triggering the sample set_timeout(1, [this]() { @@ -90,8 +76,8 @@ void HDC2010Component::update() { float HDC2010Component::read_temp() { uint8_t byte[2]; - this->read_register(HDC2010_CMD_TEMPERATURE_LOW, &byte[0], 1); - this->read_register(HDC2010_CMD_TEMPERATURE_HIGH, &byte[1], 1); + this->read_register(REG_TEMPERATURE_LOW, &byte[0], 1); + this->read_register(REG_TEMPERATURE_HIGH, &byte[1], 1); uint16_t temp = encode_uint16(byte[1], byte[0]); return (float) temp * 0.0025177f - 40.0f; @@ -100,8 +86,8 @@ float HDC2010Component::read_temp() { float HDC2010Component::read_humidity() { uint8_t byte[2]; - this->read_register(HDC2010_CMD_HUMIDITY_LOW, &byte[0], 1); - this->read_register(HDC2010_CMD_HUMIDITY_HIGH, &byte[1], 1); + this->read_register(REG_HUMIDITY_LOW, &byte[0], 1); + this->read_register(REG_HUMIDITY_HIGH, &byte[1], 1); uint16_t humidity = encode_uint16(byte[1], byte[0]); return (float) humidity * 0.001525879f; diff --git a/esphome/components/hdc2080/__init__.py b/esphome/components/hdc2080/__init__.py new file mode 100644 index 0000000000..341ea61048 --- /dev/null +++ b/esphome/components/hdc2080/__init__.py @@ -0,0 +1 @@ +CODEOWNERS = ["@G-Pereira", "@jesserockz"] diff --git a/esphome/components/hdc2080/hdc2080.cpp b/esphome/components/hdc2080/hdc2080.cpp new file mode 100644 index 0000000000..dcb207e099 --- /dev/null +++ b/esphome/components/hdc2080/hdc2080.cpp @@ -0,0 +1,71 @@ +#include "hdc2080.h" +#include "esphome/core/hal.h" +#include "esphome/core/log.h" + +namespace esphome::hdc2080 { + +static const char *const TAG = "hdc2080"; + +// Register map (Table 8-6) +static constexpr uint8_t REG_TEMPERATURE_LOW = 0x00; // Temperature [7:0] +static constexpr uint8_t REG_TEMPERATURE_HIGH = 0x01; // Temperature [15:8] +static constexpr uint8_t REG_HUMIDITY_LOW = 0x02; // Humidity [7:0] +static constexpr uint8_t REG_HUMIDITY_HIGH = 0x03; // Humidity [15:8] +static constexpr uint8_t REG_RESET_DRDY_INT_CONF = 0x0E; // Soft Reset and Interrupt Configuration +static constexpr uint8_t REG_MEASUREMENT_CONFIGURATION = 0x0F; + +// Measurement register (0x0F) bit fields +static constexpr uint8_t MEAS_TRIG = 0x01; // Bit 0: start measurement +static constexpr uint8_t MEAS_CONF_TEMP = 0x02; // Bits 2:1 = 01: temperature only +static constexpr uint8_t MEAS_CONF_HUM = 0x04; // Bits 2:1 = 10: humidity only + +void HDC2080Component::setup() { + const uint8_t data = 0x00; // automatic measurement mode disabled, heater off + if (this->write_register(REG_RESET_DRDY_INT_CONF, &data, 1) != i2c::ERROR_OK) { + this->mark_failed(ESP_LOG_MSG_COMM_FAIL); + return; + } +} + +void HDC2080Component::dump_config() { + ESP_LOGCONFIG(TAG, "HDC2080:"); + LOG_I2C_DEVICE(this); + LOG_UPDATE_INTERVAL(this); + LOG_SENSOR(" ", "Temperature", this->temperature_sensor_); + LOG_SENSOR(" ", "Humidity", this->humidity_sensor_); + if (this->is_failed()) { + ESP_LOGE(TAG, ESP_LOG_MSG_COMM_FAIL); + } +} + +void HDC2080Component::update() { + uint8_t data = MEAS_TRIG; // 14-bit resolution, measure both, start + if (this->temperature_sensor_ != nullptr && this->humidity_sensor_ == nullptr) { + data = MEAS_TRIG | MEAS_CONF_TEMP; + } else if (this->temperature_sensor_ == nullptr && this->humidity_sensor_ != nullptr) { + data = MEAS_TRIG | MEAS_CONF_HUM; + } + if (this->write_register(REG_MEASUREMENT_CONFIGURATION, &data, 1) != i2c::ERROR_OK) { + this->status_set_warning(ESP_LOG_MSG_COMM_FAIL); + return; + } + // wait for conversion to complete 2ms should be enough, more is fine + this->set_timeout(5, [this]() { + uint8_t raw_data[4]; + if (this->read_register(REG_TEMPERATURE_LOW, raw_data, 4) != i2c::ERROR_OK) { + this->status_set_warning(ESP_LOG_MSG_COMM_FAIL); + return; + } + this->status_clear_warning(); + if (this->temperature_sensor_ != nullptr) { + float temp = encode_uint16(raw_data[1], raw_data[0]) * (165.0f / 65536.0f) - 40.5f; + this->temperature_sensor_->publish_state(temp); + } + if (this->humidity_sensor_ != nullptr) { + float humidity = encode_uint16(raw_data[3], raw_data[2]) * (100.0f / 65536.0f); + this->humidity_sensor_->publish_state(humidity); + } + }); +} + +} // namespace esphome::hdc2080 diff --git a/esphome/components/hdc2080/hdc2080.h b/esphome/components/hdc2080/hdc2080.h new file mode 100644 index 0000000000..daa10d371d --- /dev/null +++ b/esphome/components/hdc2080/hdc2080.h @@ -0,0 +1,24 @@ +#pragma once + +#include "esphome/components/i2c/i2c.h" +#include "esphome/components/sensor/sensor.h" +#include "esphome/core/component.h" + +namespace esphome::hdc2080 { + +class HDC2080Component : public PollingComponent, public i2c::I2CDevice { + public: + void set_temperature(sensor::Sensor *temperature) { this->temperature_sensor_ = temperature; } + void set_humidity(sensor::Sensor *humidity) { this->humidity_sensor_ = humidity; } + + /// Setup the sensor and check for connection. + void setup() override; + void dump_config() override; + void update() override; + + protected: + sensor::Sensor *temperature_sensor_{nullptr}; + sensor::Sensor *humidity_sensor_{nullptr}; +}; + +} // namespace esphome::hdc2080 diff --git a/esphome/components/hdc2080/sensor.py b/esphome/components/hdc2080/sensor.py new file mode 100644 index 0000000000..777fc51cba --- /dev/null +++ b/esphome/components/hdc2080/sensor.py @@ -0,0 +1,57 @@ +import esphome.codegen as cg +from esphome.components import i2c, sensor +import esphome.config_validation as cv +from esphome.const import ( + CONF_HUMIDITY, + CONF_ID, + CONF_TEMPERATURE, + DEVICE_CLASS_HUMIDITY, + DEVICE_CLASS_TEMPERATURE, + STATE_CLASS_MEASUREMENT, + UNIT_CELSIUS, + UNIT_PERCENT, +) + +DEPENDENCIES = ["i2c"] + +hdc2080_ns = cg.esphome_ns.namespace("hdc2080") +HDC2080Component = hdc2080_ns.class_( + "HDC2080Component", cg.PollingComponent, i2c.I2CDevice +) + +CONFIG_SCHEMA = ( + cv.Schema( + { + cv.GenerateID(): cv.declare_id(HDC2080Component), + cv.Optional(CONF_TEMPERATURE): sensor.sensor_schema( + unit_of_measurement=UNIT_CELSIUS, + accuracy_decimals=1, + device_class=DEVICE_CLASS_TEMPERATURE, + state_class=STATE_CLASS_MEASUREMENT, + ), + cv.Optional(CONF_HUMIDITY): sensor.sensor_schema( + unit_of_measurement=UNIT_PERCENT, + accuracy_decimals=0, + device_class=DEVICE_CLASS_HUMIDITY, + state_class=STATE_CLASS_MEASUREMENT, + ), + } + ) + .extend(cv.polling_component_schema("60s")) + .extend(i2c.i2c_device_schema(0x40)) + .add_extra(cv.has_at_least_one_key(CONF_TEMPERATURE, CONF_HUMIDITY)) +) + + +async def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + await cg.register_component(var, config) + await i2c.register_i2c_device(var, config) + + if temperature_config := config.get(CONF_TEMPERATURE): + sens = await sensor.new_sensor(temperature_config) + cg.add(var.set_temperature(sens)) + + if humidity_config := config.get(CONF_HUMIDITY): + sens = await sensor.new_sensor(humidity_config) + cg.add(var.set_humidity(sens)) diff --git a/esphome/components/hdc302x/__init__.py b/esphome/components/hdc302x/__init__.py new file mode 100644 index 0000000000..d8165281bf --- /dev/null +++ b/esphome/components/hdc302x/__init__.py @@ -0,0 +1 @@ +CODEOWNERS = ["@joshuasing"] diff --git a/esphome/components/hdc302x/hdc302x.cpp b/esphome/components/hdc302x/hdc302x.cpp new file mode 100644 index 0000000000..b50d34169a --- /dev/null +++ b/esphome/components/hdc302x/hdc302x.cpp @@ -0,0 +1,171 @@ +#include "hdc302x.h" + +#include "esphome/core/hal.h" +#include "esphome/core/helpers.h" +#include "esphome/core/log.h" + +namespace esphome::hdc302x { + +static const char *const TAG = "hdc302x.sensor"; + +// Commands (per datasheet Table 7-4) +static const uint8_t HDC302X_CMD_SOFT_RESET[2] = {0x30, 0xa2}; +static const uint8_t HDC302X_CMD_CLEAR_STATUS_REGISTER[2] = {0x30, 0x41}; + +static const uint8_t HDC302X_CMD_TRIGGER_MSB = 0x24; + +static const uint8_t HDC302X_CMD_HEATER_ENABLE[2] = {0x30, 0x6d}; +static const uint8_t HDC302X_CMD_HEATER_DISABLE[2] = {0x30, 0x66}; +static const uint8_t HDC302X_CMD_HEATER_CONFIGURE[2] = {0x30, 0x6e}; + +void HDC302XComponent::setup() { + // Soft reset the device + if (this->write(HDC302X_CMD_SOFT_RESET, 2) != i2c::ERROR_OK) { + this->mark_failed(LOG_STR("Soft reset failed")); + return; + } + // Delay SensorRR (reset ready), per datasheet, 6.5. + delay(3); + + // Clear status register + if (this->write(HDC302X_CMD_CLEAR_STATUS_REGISTER, 2) != i2c::ERROR_OK) { + this->mark_failed(LOG_STR("Clear status failed")); + return; + } +} + +void HDC302XComponent::dump_config() { + ESP_LOGCONFIG(TAG, + "HDC302x:\n" + " Heater: %s", + this->heater_active_ ? "active" : "inactive"); + LOG_I2C_DEVICE(this); + LOG_UPDATE_INTERVAL(this); + LOG_SENSOR(" ", "Temperature", this->temp_sensor_); + LOG_SENSOR(" ", "Humidity", this->humidity_sensor_); +} + +void HDC302XComponent::update() { + uint8_t cmd[] = { + HDC302X_CMD_TRIGGER_MSB, + this->power_mode_, + }; + if (this->write(cmd, 2) != i2c::ERROR_OK) { + this->status_set_warning(LOG_STR(ESP_LOG_MSG_COMM_FAIL)); + return; + } + + // Read data after ADC conversion has completed + this->set_timeout(this->conversion_delay_ms_(), [this]() { this->read_data_(); }); +} + +void HDC302XComponent::start_heater(uint16_t power, uint32_t duration_ms) { + if (!this->disable_heater_()) { + ESP_LOGD(TAG, "Heater disable before start failed"); + } + if (!this->configure_heater_(power) || !this->enable_heater_()) { + ESP_LOGW(TAG, "Heater start failed"); + return; + } + this->heater_active_ = true; + this->cancel_timeout("heater_off"); + if (duration_ms > 0) { + this->set_timeout("heater_off", duration_ms, [this]() { this->stop_heater(); }); + } +} + +void HDC302XComponent::stop_heater() { + this->cancel_timeout("heater_off"); + if (!this->disable_heater_()) { + ESP_LOGW(TAG, "Heater stop failed"); + } + this->heater_active_ = false; +} + +bool HDC302XComponent::enable_heater_() { + if (this->write(HDC302X_CMD_HEATER_ENABLE, 2) != i2c::ERROR_OK) { + ESP_LOGE(TAG, "Enable heater failed"); + return false; + } + return true; +} + +bool HDC302XComponent::configure_heater_(uint16_t power_level) { + if (power_level > 0x3fff) { + ESP_LOGW(TAG, "Heater power 0x%04x exceeds max 0x3fff", power_level); + return false; + } + + // Heater current level config. + uint8_t config[] = { + static_cast((power_level >> 8) & 0xff), // MSB + static_cast(power_level & 0xff) // LSB + }; + + // Configure level of heater current (per datasheet 7.5.7.8). + uint8_t cmd[] = { + HDC302X_CMD_HEATER_CONFIGURE[0], HDC302X_CMD_HEATER_CONFIGURE[1], config[0], config[1], + crc8(config, 2, 0xff, 0x31, true), + }; + if (this->write(cmd, sizeof(cmd)) != i2c::ERROR_OK) { + ESP_LOGE(TAG, "Configure heater failed"); + return false; + } + + return true; +} + +bool HDC302XComponent::disable_heater_() { + if (this->write(HDC302X_CMD_HEATER_DISABLE, 2) != i2c::ERROR_OK) { + ESP_LOGE(TAG, "Disable heater failed"); + return false; + } + return true; +} + +void HDC302XComponent::read_data_() { + uint8_t buf[6]; + if (this->read(buf, 6) != i2c::ERROR_OK) { + this->status_set_warning(LOG_STR(ESP_LOG_MSG_COMM_FAIL)); + return; + } + + // Check checksums + if (crc8(buf, 2, 0xff, 0x31, true) != buf[2] || crc8(buf + 3, 2, 0xff, 0x31, true) != buf[5]) { + this->status_set_warning(LOG_STR("Read data: invalid CRC")); + return; + } + + this->status_clear_warning(); + + if (this->temp_sensor_ != nullptr) { + uint16_t raw_t = encode_uint16(buf[0], buf[1]); + // Calculate temperature in Celsius per datasheet section 7.3.3. + float temp = -45 + 175 * (float(raw_t) / 65535.0f); + this->temp_sensor_->publish_state(temp); + } + + if (this->humidity_sensor_ != nullptr) { + uint16_t raw_rh = encode_uint16(buf[3], buf[4]); + // Calculate RH% per datasheet section 7.3.3. + float humidity = 100 * (float(raw_rh) / 65535.0f); + this->humidity_sensor_->publish_state(humidity); + } +} + +uint32_t HDC302XComponent::conversion_delay_ms_() { + // ADC conversion delay per datasheet, Table 7-5. - Trigger on Demand + switch (this->power_mode_) { + case HDC302XPowerMode::BALANCED: + return 8; + case HDC302XPowerMode::LOW_POWER: + return 5; + case HDC302XPowerMode::ULTRA_LOW_POWER: + return 4; + case HDC302XPowerMode::HIGH_ACCURACY: + default: + return 13; + } +} + +} // namespace esphome::hdc302x diff --git a/esphome/components/hdc302x/hdc302x.h b/esphome/components/hdc302x/hdc302x.h new file mode 100644 index 0000000000..6afea0a8c0 --- /dev/null +++ b/esphome/components/hdc302x/hdc302x.h @@ -0,0 +1,68 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/core/automation.h" +#include "esphome/components/sensor/sensor.h" +#include "esphome/components/i2c/i2c.h" + +namespace esphome::hdc302x { + +enum HDC302XPowerMode : uint8_t { + HIGH_ACCURACY = 0x00, + BALANCED = 0x0b, + LOW_POWER = 0x16, + ULTRA_LOW_POWER = 0xff, +}; + +/** + HDC302x Temperature and humidity sensor. + + Datasheet: + https://www.ti.com/lit/ds/symlink/hdc3020.pdf + */ +class HDC302XComponent : public PollingComponent, public i2c::I2CDevice { + public: + void setup() override; + void dump_config() override; + void update() override; + + void start_heater(uint16_t power, uint32_t duration_ms); + void stop_heater(); + + void set_temp_sensor(sensor::Sensor *temp_sensor) { this->temp_sensor_ = temp_sensor; } + void set_humidity_sensor(sensor::Sensor *humidity_sensor) { this->humidity_sensor_ = humidity_sensor; } + + void set_power_mode(HDC302XPowerMode power_mode) { this->power_mode_ = power_mode; } + + protected: + sensor::Sensor *temp_sensor_{nullptr}; + sensor::Sensor *humidity_sensor_{nullptr}; + + HDC302XPowerMode power_mode_{HDC302XPowerMode::HIGH_ACCURACY}; + bool heater_active_{false}; + + bool enable_heater_(); + bool configure_heater_(uint16_t power_level); + bool disable_heater_(); + void read_data_(); + uint32_t conversion_delay_ms_(); +}; + +template class HeaterOnAction : public Action, public Parented { + public: + TEMPLATABLE_VALUE(uint16_t, power) + TEMPLATABLE_VALUE(uint32_t, duration) + + void play(const Ts &...x) override { + auto power_val = this->power_.value(x...); + auto duration_val = this->duration_.value(x...); + this->parent_->start_heater(power_val, duration_val); + } +}; + +template class HeaterOffAction : public Action, public Parented { + public: + void play(const Ts &...x) override { this->parent_->stop_heater(); } +}; + +} // namespace esphome::hdc302x diff --git a/esphome/components/hdc302x/sensor.py b/esphome/components/hdc302x/sensor.py new file mode 100644 index 0000000000..a6265b9b98 --- /dev/null +++ b/esphome/components/hdc302x/sensor.py @@ -0,0 +1,141 @@ +from esphome import automation +from esphome.automation import maybe_simple_id +import esphome.codegen as cg +from esphome.components import i2c, sensor +import esphome.config_validation as cv +from esphome.const import ( + CONF_DURATION, + CONF_HUMIDITY, + CONF_ID, + CONF_POWER, + CONF_POWER_MODE, + CONF_TEMPERATURE, + DEVICE_CLASS_HUMIDITY, + DEVICE_CLASS_TEMPERATURE, + STATE_CLASS_MEASUREMENT, + UNIT_CELSIUS, + UNIT_PERCENT, +) + +DEPENDENCIES = ["i2c"] + +hdc302x_ns = cg.esphome_ns.namespace("hdc302x") +HDC302XComponent = hdc302x_ns.class_( + "HDC302XComponent", cg.PollingComponent, i2c.I2CDevice +) + +HDC302XPowerMode = hdc302x_ns.enum("HDC302XPowerMode") +POWER_MODE_OPTIONS = { + "HIGH_ACCURACY": HDC302XPowerMode.HIGH_ACCURACY, + "BALANCED": HDC302XPowerMode.BALANCED, + "LOW_POWER": HDC302XPowerMode.LOW_POWER, + "ULTRA_LOW_POWER": HDC302XPowerMode.ULTRA_LOW_POWER, +} + +# Actions +HeaterOnAction = hdc302x_ns.class_("HeaterOnAction", automation.Action) +HeaterOffAction = hdc302x_ns.class_("HeaterOffAction", automation.Action) + +CONFIG_SCHEMA = ( + cv.Schema( + { + cv.GenerateID(): cv.declare_id(HDC302XComponent), + cv.Optional(CONF_TEMPERATURE): sensor.sensor_schema( + unit_of_measurement=UNIT_CELSIUS, + accuracy_decimals=2, + device_class=DEVICE_CLASS_TEMPERATURE, + state_class=STATE_CLASS_MEASUREMENT, + ), + cv.Optional(CONF_HUMIDITY): sensor.sensor_schema( + unit_of_measurement=UNIT_PERCENT, + accuracy_decimals=2, + device_class=DEVICE_CLASS_HUMIDITY, + state_class=STATE_CLASS_MEASUREMENT, + ), + cv.Optional(CONF_POWER_MODE, default="HIGH_ACCURACY"): cv.enum( + POWER_MODE_OPTIONS, upper=True + ), + } + ) + .extend(cv.polling_component_schema("60s")) + .extend(i2c.i2c_device_schema(0x44)) # Default address per datasheet, Table 7-2. +) + + +async def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + await cg.register_component(var, config) + await i2c.register_i2c_device(var, config) + + if temp_config := config.get(CONF_TEMPERATURE): + sens = await sensor.new_sensor(temp_config) + cg.add(var.set_temp_sensor(sens)) + + if humidity_config := config.get(CONF_HUMIDITY): + sens = await sensor.new_sensor(humidity_config) + cg.add(var.set_humidity_sensor(sens)) + + cg.add(var.set_power_mode(config[CONF_POWER_MODE])) + + +# HDC302x heater power configs, per datasheet Table 7-15. +HDC302X_HEATER_POWER_MAP = { + "QUARTER": 0x009F, + "HALF": 0x03FF, + "FULL": 0x3FFF, +} + + +def heater_power_value(value): + """Accept enum names or raw uint16 values""" + if isinstance(value, cv.Lambda): + return value + if isinstance(value, str): + upper = value.upper() + if upper in HDC302X_HEATER_POWER_MAP: + return HDC302X_HEATER_POWER_MAP[upper] + raise cv.Invalid( + f"Unknown heater power preset: {value}. Use QUARTER, HALF, FULL, or a raw value 0-16383" + ) + return cv.int_range(min=0, max=0x3FFF)(value) + + +HDC302X_ACTION_SCHEMA = maybe_simple_id({cv.GenerateID(): cv.use_id(HDC302XComponent)}) + +HDC302X_HEATER_ON_ACTION_SCHEMA = maybe_simple_id( + { + cv.GenerateID(): cv.use_id(HDC302XComponent), + cv.Optional(CONF_POWER, default="QUARTER"): cv.templatable(heater_power_value), + cv.Optional(CONF_DURATION, default="5s"): cv.templatable( + cv.positive_time_period_milliseconds + ), + } +) + + +@automation.register_action( + "hdc302x.heater_on", + HeaterOnAction, + HDC302X_HEATER_ON_ACTION_SCHEMA, + synchronous=True, +) +async def hdc302x_heater_on_to_code(config, action_id, template_arg, args): + var = cg.new_Pvariable(action_id, template_arg) + await cg.register_parented(var, config[CONF_ID]) + template_ = await cg.templatable(config[CONF_POWER], args, cg.uint16) + cg.add(var.set_power(template_)) + template_ = await cg.templatable(config[CONF_DURATION], args, cg.uint32) + cg.add(var.set_duration(template_)) + return var + + +@automation.register_action( + "hdc302x.heater_off", + HeaterOffAction, + HDC302X_ACTION_SCHEMA, + synchronous=True, +) +async def hdc302x_heater_off_to_code(config, action_id, template_arg, args): + var = cg.new_Pvariable(action_id, template_arg) + await cg.register_parented(var, config[CONF_ID]) + return var diff --git a/esphome/components/he60r/he60r.cpp b/esphome/components/he60r/he60r.cpp index ca17930272..47440cc1f7 100644 --- a/esphome/components/he60r/he60r.cpp +++ b/esphome/components/he60r/he60r.cpp @@ -171,9 +171,12 @@ void HE60rCover::control(const CoverCall &call) { } else { this->toggles_needed_++; } - } else if (call.get_position().has_value()) { + } else { + auto pos_opt = call.get_position(); + if (!pos_opt.has_value()) + return; // go to position action - auto pos = *call.get_position(); + auto pos = *pos_opt; // are we at the target? if (pos == this->position) { this->start_direction_(COVER_OPERATION_IDLE); @@ -236,7 +239,7 @@ void HE60rCover::recompute_position_() { return; const uint32_t now = millis(); - if (now > this->last_recompute_time_) { + if (now != this->last_recompute_time_) { auto diff = (unsigned) (now - last_recompute_time_); float delta; switch (this->current_operation) { diff --git a/esphome/components/heatpumpir/climate.py b/esphome/components/heatpumpir/climate.py index 0d760938d0..7743da77ab 100644 --- a/esphome/components/heatpumpir/climate.py +++ b/esphome/components/heatpumpir/climate.py @@ -125,6 +125,7 @@ async def to_code(config): cg.add(var.set_vertical_default(config[CONF_VERTICAL_DEFAULT])) cg.add(var.set_max_temperature(config[CONF_MAX_TEMPERATURE])) cg.add(var.set_min_temperature(config[CONF_MIN_TEMPERATURE])) + cg.add_build_flag("-Wno-error=overloaded-virtual") cg.add_library("tonia/HeatpumpIR", "1.0.40") if CORE.is_libretiny or CORE.is_esp32: diff --git a/esphome/components/heatpumpir/heatpumpir.cpp b/esphome/components/heatpumpir/heatpumpir.cpp index 6b73a24dc4..11e7672dc1 100644 --- a/esphome/components/heatpumpir/heatpumpir.cpp +++ b/esphome/components/heatpumpir/heatpumpir.cpp @@ -114,7 +114,7 @@ void HeatpumpIRClimate::setup() { this->current_temperature = state; IRSenderESPHome esp_sender(this->transmitter_); - this->heatpump_ir_->send(esp_sender, uint8_t(lround(this->current_temperature + 0.5))); + this->heatpump_ir_->send(esp_sender, uint8_t(lround(this->current_temperature))); // current temperature changed, publish state this->publish_state(); diff --git a/esphome/components/hitachi_ac344/hitachi_ac344.cpp b/esphome/components/hitachi_ac344/hitachi_ac344.cpp index 2bcb205644..69469cab2e 100644 --- a/esphome/components/hitachi_ac344/hitachi_ac344.cpp +++ b/esphome/components/hitachi_ac344/hitachi_ac344.cpp @@ -175,7 +175,7 @@ void HitachiClimate::transmit_state() { set_temp_(static_cast(this->target_temperature)); - switch (this->fan_mode.value()) { + switch (this->fan_mode.value_or(climate::CLIMATE_FAN_ON)) { case climate::CLIMATE_FAN_LOW: set_fan_(HITACHI_AC344_FAN_LOW); break; diff --git a/esphome/components/hitachi_ac344/hitachi_ac344.h b/esphome/components/hitachi_ac344/hitachi_ac344.h index c34f033d92..0877b83261 100644 --- a/esphome/components/hitachi_ac344/hitachi_ac344.h +++ b/esphome/components/hitachi_ac344/hitachi_ac344.h @@ -96,7 +96,7 @@ class HitachiClimate : public climate_ir::ClimateIR { void set_power_(bool on); uint8_t get_mode_(); void set_mode_(uint8_t mode); - void set_temp_(uint8_t celsius, bool set_previous = false); + void set_temp_(uint8_t celsius, bool set_previous = true); uint8_t get_fan_(); void set_fan_(uint8_t speed); void set_swing_v_toggle_(bool on); diff --git a/esphome/components/hitachi_ac424/hitachi_ac424.cpp b/esphome/components/hitachi_ac424/hitachi_ac424.cpp index 64f23dfc17..0b3cc99a82 100644 --- a/esphome/components/hitachi_ac424/hitachi_ac424.cpp +++ b/esphome/components/hitachi_ac424/hitachi_ac424.cpp @@ -176,7 +176,7 @@ void HitachiClimate::transmit_state() { set_temp_(static_cast(this->target_temperature)); - switch (this->fan_mode.value()) { + switch (this->fan_mode.value_or(climate::CLIMATE_FAN_ON)) { case climate::CLIMATE_FAN_LOW: set_fan_(HITACHI_AC424_FAN_LOW); break; diff --git a/esphome/components/hlk_fm22x/__init__.py b/esphome/components/hlk_fm22x/__init__.py index efd64b6513..c0349319d1 100644 --- a/esphome/components/hlk_fm22x/__init__.py +++ b/esphome/components/hlk_fm22x/__init__.py @@ -8,7 +8,6 @@ from esphome.const import ( CONF_NAME, CONF_ON_ENROLLMENT_DONE, CONF_ON_ENROLLMENT_FAILED, - CONF_TRIGGER_ID, ) CODEOWNERS = ["@OnFreund"] @@ -28,33 +27,6 @@ HlkFm22xComponent = hlk_fm22x_ns.class_( "HlkFm22xComponent", cg.PollingComponent, uart.UARTDevice ) -FaceScanMatchedTrigger = hlk_fm22x_ns.class_( - "FaceScanMatchedTrigger", automation.Trigger.template(cg.int16, cg.std_string) -) - -FaceScanUnmatchedTrigger = hlk_fm22x_ns.class_( - "FaceScanUnmatchedTrigger", automation.Trigger.template() -) - -FaceScanInvalidTrigger = hlk_fm22x_ns.class_( - "FaceScanInvalidTrigger", automation.Trigger.template(cg.uint8) -) - -FaceInfoTrigger = hlk_fm22x_ns.class_( - "FaceInfoTrigger", - automation.Trigger.template( - cg.int16, cg.int16, cg.int16, cg.int16, cg.int16, cg.int16, cg.int16, cg.int16 - ), -) - -EnrollmentDoneTrigger = hlk_fm22x_ns.class_( - "EnrollmentDoneTrigger", automation.Trigger.template(cg.int16, cg.uint8) -) - -EnrollmentFailedTrigger = hlk_fm22x_ns.class_( - "EnrollmentFailedTrigger", automation.Trigger.template(cg.uint8) -) - EnrollmentAction = hlk_fm22x_ns.class_("EnrollmentAction", automation.Action) DeleteAction = hlk_fm22x_ns.class_("DeleteAction", automation.Action) DeleteAllAction = hlk_fm22x_ns.class_("DeleteAllAction", automation.Action) @@ -65,46 +37,14 @@ CONFIG_SCHEMA = cv.All( cv.Schema( { cv.GenerateID(): cv.declare_id(HlkFm22xComponent), - cv.Optional(CONF_ON_FACE_SCAN_MATCHED): automation.validate_automation( - { - cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id( - FaceScanMatchedTrigger - ), - } - ), + cv.Optional(CONF_ON_FACE_SCAN_MATCHED): automation.validate_automation({}), cv.Optional(CONF_ON_FACE_SCAN_UNMATCHED): automation.validate_automation( - { - cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id( - FaceScanUnmatchedTrigger - ), - } - ), - cv.Optional(CONF_ON_FACE_SCAN_INVALID): automation.validate_automation( - { - cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id( - FaceScanInvalidTrigger - ), - } - ), - cv.Optional(CONF_ON_FACE_INFO): automation.validate_automation( - { - cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(FaceInfoTrigger), - } - ), - cv.Optional(CONF_ON_ENROLLMENT_DONE): automation.validate_automation( - { - cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id( - EnrollmentDoneTrigger - ), - } - ), - cv.Optional(CONF_ON_ENROLLMENT_FAILED): automation.validate_automation( - { - cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id( - EnrollmentFailedTrigger - ), - } + {} ), + cv.Optional(CONF_ON_FACE_SCAN_INVALID): automation.validate_automation({}), + cv.Optional(CONF_ON_FACE_INFO): automation.validate_automation({}), + cv.Optional(CONF_ON_ENROLLMENT_DONE): automation.validate_automation({}), + cv.Optional(CONF_ON_ENROLLMENT_FAILED): automation.validate_automation({}), } ) .extend(cv.polling_component_schema("50ms")) @@ -118,23 +58,27 @@ async def to_code(config): await uart.register_uart_device(var, config) for conf in config.get(CONF_ON_FACE_SCAN_MATCHED, []): - trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) - await automation.build_automation( - trigger, [(cg.int16, "face_id"), (cg.std_string, "name")], conf + await automation.build_callback_automation( + var, + "add_on_face_scan_matched_callback", + [(cg.int16, "face_id"), (cg.std_string, "name")], + conf, ) for conf in config.get(CONF_ON_FACE_SCAN_UNMATCHED, []): - trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) - await automation.build_automation(trigger, [], conf) + await automation.build_callback_automation( + var, "add_on_face_scan_unmatched_callback", [], conf + ) for conf in config.get(CONF_ON_FACE_SCAN_INVALID, []): - trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) - await automation.build_automation(trigger, [(cg.uint8, "error")], conf) + await automation.build_callback_automation( + var, "add_on_face_scan_invalid_callback", [(cg.uint8, "error")], conf + ) for conf in config.get(CONF_ON_FACE_INFO, []): - trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) - await automation.build_automation( - trigger, + await automation.build_callback_automation( + var, + "add_on_face_info_callback", [ (cg.int16, "status"), (cg.int16, "left"), @@ -149,14 +93,17 @@ async def to_code(config): ) for conf in config.get(CONF_ON_ENROLLMENT_DONE, []): - trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) - await automation.build_automation( - trigger, [(cg.int16, "face_id"), (cg.uint8, "direction")], conf + await automation.build_callback_automation( + var, + "add_on_enrollment_done_callback", + [(cg.int16, "face_id"), (cg.uint8, "direction")], + conf, ) for conf in config.get(CONF_ON_ENROLLMENT_FAILED, []): - trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) - await automation.build_automation(trigger, [(cg.uint8, "error")], conf) + await automation.build_callback_automation( + var, "add_on_enrollment_failed_callback", [(cg.uint8, "error")], conf + ) @automation.register_action( @@ -170,6 +117,7 @@ async def to_code(config): }, key=CONF_NAME, ), + synchronous=True, ) async def hlk_fm22x_enroll_to_code(config, action_id, template_arg, args): var = cg.new_Pvariable(action_id, template_arg) @@ -192,6 +140,7 @@ async def hlk_fm22x_enroll_to_code(config, action_id, template_arg, args): }, key=CONF_FACE_ID, ), + synchronous=True, ) async def hlk_fm22x_delete_to_code(config, action_id, template_arg, args): var = cg.new_Pvariable(action_id, template_arg) @@ -210,6 +159,7 @@ async def hlk_fm22x_delete_to_code(config, action_id, template_arg, args): cv.GenerateID(): cv.use_id(HlkFm22xComponent), } ), + synchronous=True, ) async def hlk_fm22x_delete_all_to_code(config, action_id, template_arg, args): var = cg.new_Pvariable(action_id, template_arg) @@ -225,6 +175,7 @@ async def hlk_fm22x_delete_all_to_code(config, action_id, template_arg, args): cv.GenerateID(): cv.use_id(HlkFm22xComponent), } ), + synchronous=True, ) async def hlk_fm22x_scan_to_code(config, action_id, template_arg, args): var = cg.new_Pvariable(action_id, template_arg) @@ -240,6 +191,7 @@ async def hlk_fm22x_scan_to_code(config, action_id, template_arg, args): cv.GenerateID(): cv.use_id(HlkFm22xComponent), } ), + synchronous=True, ) async def hlk_fm22x_reset_to_code(config, action_id, template_arg, args): var = cg.new_Pvariable(action_id, template_arg) diff --git a/esphome/components/hlk_fm22x/hlk_fm22x.cpp b/esphome/components/hlk_fm22x/hlk_fm22x.cpp index c0f14c7105..7a0dc0690c 100644 --- a/esphome/components/hlk_fm22x/hlk_fm22x.cpp +++ b/esphome/components/hlk_fm22x/hlk_fm22x.cpp @@ -1,20 +1,17 @@ #include "hlk_fm22x.h" #include "esphome/core/log.h" #include "esphome/core/helpers.h" -#include #include namespace esphome::hlk_fm22x { static const char *const TAG = "hlk_fm22x"; - -// Maximum response size is 36 bytes (VERIFY reply: face_id + 32-byte name) -static constexpr size_t HLK_FM22X_MAX_RESPONSE_SIZE = 36; +static constexpr uint32_t PAYLOAD_TIMEOUT_MS = 20; void HlkFm22xComponent::setup() { ESP_LOGCONFIG(TAG, "Setting up HLK-FM22X..."); this->set_enrolling_(false); - while (this->available()) { + while (this->available() > 0) { this->read(); } this->defer([this]() { this->send_command_(HlkFm22xCommand::GET_STATUS); }); @@ -35,7 +32,7 @@ void HlkFm22xComponent::update() { } void HlkFm22xComponent::enroll_face(const std::string &name, HlkFm22xFaceDirection direction) { - if (name.length() > 31) { + if (name.length() > HLK_FM22X_NAME_SIZE - 1) { ESP_LOGE(TAG, "enroll_face(): name too long '%s'", name.c_str()); return; } @@ -88,7 +85,7 @@ void HlkFm22xComponent::send_command_(HlkFm22xCommand command, const uint8_t *da } this->wait_cycles_ = 0; this->active_command_ = command; - while (this->available()) + while (this->available() > 0) this->read(); this->write((uint8_t) (START_CODE >> 8)); this->write((uint8_t) (START_CODE & 0xFF)); @@ -137,17 +134,37 @@ void HlkFm22xComponent::recv_command_() { checksum ^= byte; length |= byte; - std::vector data; - data.reserve(length); + // Wait for remaining data (payload + checksum) to arrive. + // Header bytes are already consumed, so we must finish reading this message. + uint32_t start = millis(); + while (this->available() < length + 1) { + if (millis() - start > PAYLOAD_TIMEOUT_MS) { + ESP_LOGE(TAG, "Timeout waiting for payload (%u bytes)", length); + // Drain any partial payload bytes to resync the parser + while (this->available() > 0) { + this->read(); + } + return; + } + delay(1); + } + + // Read up to buffer size; discard excess bytes while still computing checksum + // GET_ALL_FACE_IDS can return all enrolled face data (hundreds of bytes) + // but handlers only need the first few bytes + size_t to_store = std::min(static_cast(length), HLK_FM22X_MAX_RESPONSE_SIZE); for (uint16_t idx = 0; idx < length; ++idx) { byte = this->read(); checksum ^= byte; - data.push_back(byte); + if (idx < to_store) { + this->recv_buf_[idx] = byte; + } } #if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE char hex_buf[format_hex_pretty_size(HLK_FM22X_MAX_RESPONSE_SIZE)]; - ESP_LOGV(TAG, "Recv type: 0x%.2X, data: %s", response_type, format_hex_pretty_to(hex_buf, data.data(), data.size())); + ESP_LOGV(TAG, "Recv type: 0x%.2X, data: %s", response_type, + format_hex_pretty_to(hex_buf, this->recv_buf_.data(), to_store)); #endif byte = this->read(); @@ -157,10 +174,10 @@ void HlkFm22xComponent::recv_command_() { } switch (response_type) { case HlkFm22xResponseType::NOTE: - this->handle_note_(data); + this->handle_note_(this->recv_buf_.data(), to_store); break; case HlkFm22xResponseType::REPLY: - this->handle_reply_(data); + this->handle_reply_(this->recv_buf_.data(), to_store); break; default: ESP_LOGW(TAG, "Unexpected response type: 0x%.2X", response_type); @@ -168,11 +185,15 @@ void HlkFm22xComponent::recv_command_() { } } -void HlkFm22xComponent::handle_note_(const std::vector &data) { +void HlkFm22xComponent::handle_note_(const uint8_t *data, size_t length) { + if (length < 1) { + ESP_LOGE(TAG, "Empty note data"); + return; + } switch (data[0]) { case HlkFm22xNoteType::FACE_STATE: - if (data.size() < 17) { - ESP_LOGE(TAG, "Invalid face note data size: %u", data.size()); + if (length < 17) { + ESP_LOGE(TAG, "Invalid face note data size: %zu", length); break; } { @@ -209,9 +230,13 @@ void HlkFm22xComponent::handle_note_(const std::vector &data) { } } -void HlkFm22xComponent::handle_reply_(const std::vector &data) { +void HlkFm22xComponent::handle_reply_(const uint8_t *data, size_t length) { auto expected = this->active_command_; this->active_command_ = HlkFm22xCommand::NONE; + if (length < 2) { + ESP_LOGE(TAG, "Reply too short: %zu bytes", length); + return; + } if (data[0] != (uint8_t) expected) { ESP_LOGE(TAG, "Unexpected response command. Expected: 0x%.2X, Received: 0x%.2X", expected, data[0]); return; @@ -238,16 +263,20 @@ void HlkFm22xComponent::handle_reply_(const std::vector &data) { } switch (expected) { case HlkFm22xCommand::VERIFY: { + if (length < 4 + HLK_FM22X_NAME_SIZE) { + ESP_LOGE(TAG, "VERIFY response too short: %zu bytes", length); + break; + } int16_t face_id = ((int16_t) data[2] << 8) | data[3]; - std::string name(data.begin() + 4, data.begin() + 36); - ESP_LOGD(TAG, "Face verified. ID: %d, name: %s", face_id, name.c_str()); + const char *name_ptr = reinterpret_cast(data + 4); + ESP_LOGD(TAG, "Face verified. ID: %d, name: %.*s", face_id, (int) HLK_FM22X_NAME_SIZE, name_ptr); if (this->last_face_id_sensor_ != nullptr) { this->last_face_id_sensor_->publish_state(face_id); } if (this->last_face_name_text_sensor_ != nullptr) { - this->last_face_name_text_sensor_->publish_state(name); + this->last_face_name_text_sensor_->publish_state(name_ptr, HLK_FM22X_NAME_SIZE); } - this->face_scan_matched_callback_.call(face_id, name); + this->face_scan_matched_callback_.call(face_id, std::string(name_ptr, HLK_FM22X_NAME_SIZE)); break; } case HlkFm22xCommand::ENROLL: { @@ -266,9 +295,8 @@ void HlkFm22xComponent::handle_reply_(const std::vector &data) { this->defer([this]() { this->send_command_(HlkFm22xCommand::GET_VERSION); }); break; case HlkFm22xCommand::GET_VERSION: - if (this->version_text_sensor_ != nullptr) { - std::string version(data.begin() + 2, data.end()); - this->version_text_sensor_->publish_state(version); + if (this->version_text_sensor_ != nullptr && length > 2) { + this->version_text_sensor_->publish_state(reinterpret_cast(data + 2), length - 2); } this->defer([this]() { this->get_face_count_(); }); break; diff --git a/esphome/components/hlk_fm22x/hlk_fm22x.h b/esphome/components/hlk_fm22x/hlk_fm22x.h index 9c981d3c44..fd8257b435 100644 --- a/esphome/components/hlk_fm22x/hlk_fm22x.h +++ b/esphome/components/hlk_fm22x/hlk_fm22x.h @@ -7,12 +7,15 @@ #include "esphome/components/text_sensor/text_sensor.h" #include "esphome/components/uart/uart.h" +#include #include -#include namespace esphome::hlk_fm22x { static const uint16_t START_CODE = 0xEFAA; +static constexpr size_t HLK_FM22X_NAME_SIZE = 32; +// Maximum response payload: command(1) + result(1) + face_id(2) + name(32) = 36 +static constexpr size_t HLK_FM22X_MAX_RESPONSE_SIZE = 36; enum HlkFm22xCommand { NONE = 0x00, RESET = 0x10, @@ -88,24 +91,23 @@ class HlkFm22xComponent : public PollingComponent, public uart::UARTDevice { void set_version_text_sensor(text_sensor::TextSensor *version_text_sensor) { this->version_text_sensor_ = version_text_sensor; } - void add_on_face_scan_matched_callback(std::function callback) { - this->face_scan_matched_callback_.add(std::move(callback)); + template void add_on_face_scan_matched_callback(F &&callback) { + this->face_scan_matched_callback_.add(std::forward(callback)); } - void add_on_face_scan_unmatched_callback(std::function callback) { - this->face_scan_unmatched_callback_.add(std::move(callback)); + template void add_on_face_scan_unmatched_callback(F &&callback) { + this->face_scan_unmatched_callback_.add(std::forward(callback)); } - void add_on_face_scan_invalid_callback(std::function callback) { - this->face_scan_invalid_callback_.add(std::move(callback)); + template void add_on_face_scan_invalid_callback(F &&callback) { + this->face_scan_invalid_callback_.add(std::forward(callback)); } - void add_on_face_info_callback( - std::function callback) { - this->face_info_callback_.add(std::move(callback)); + template void add_on_face_info_callback(F &&callback) { + this->face_info_callback_.add(std::forward(callback)); } - void add_on_enrollment_done_callback(std::function callback) { - this->enrollment_done_callback_.add(std::move(callback)); + template void add_on_enrollment_done_callback(F &&callback) { + this->enrollment_done_callback_.add(std::forward(callback)); } - void add_on_enrollment_failed_callback(std::function callback) { - this->enrollment_failed_callback_.add(std::move(callback)); + template void add_on_enrollment_failed_callback(F &&callback) { + this->enrollment_failed_callback_.add(std::forward(callback)); } void enroll_face(const std::string &name, HlkFm22xFaceDirection direction); @@ -118,10 +120,11 @@ class HlkFm22xComponent : public PollingComponent, public uart::UARTDevice { void get_face_count_(); void send_command_(HlkFm22xCommand command, const uint8_t *data = nullptr, size_t size = 0); void recv_command_(); - void handle_note_(const std::vector &data); - void handle_reply_(const std::vector &data); + void handle_note_(const uint8_t *data, size_t length); + void handle_reply_(const uint8_t *data, size_t length); void set_enrolling_(bool enrolling); + std::array recv_buf_; HlkFm22xCommand active_command_ = HlkFm22xCommand::NONE; uint16_t wait_cycles_ = 0; sensor::Sensor *face_count_sensor_{nullptr}; @@ -138,52 +141,6 @@ class HlkFm22xComponent : public PollingComponent, public uart::UARTDevice { CallbackManager enrollment_failed_callback_; }; -class FaceScanMatchedTrigger : public Trigger { - public: - explicit FaceScanMatchedTrigger(HlkFm22xComponent *parent) { - parent->add_on_face_scan_matched_callback( - [this](int16_t face_id, const std::string &name) { this->trigger(face_id, name); }); - } -}; - -class FaceScanUnmatchedTrigger : public Trigger<> { - public: - explicit FaceScanUnmatchedTrigger(HlkFm22xComponent *parent) { - parent->add_on_face_scan_unmatched_callback([this]() { this->trigger(); }); - } -}; - -class FaceScanInvalidTrigger : public Trigger { - public: - explicit FaceScanInvalidTrigger(HlkFm22xComponent *parent) { - parent->add_on_face_scan_invalid_callback([this](uint8_t error) { this->trigger(error); }); - } -}; - -class FaceInfoTrigger : public Trigger { - public: - explicit FaceInfoTrigger(HlkFm22xComponent *parent) { - parent->add_on_face_info_callback( - [this](int16_t status, int16_t left, int16_t top, int16_t right, int16_t bottom, int16_t yaw, int16_t pitch, - int16_t roll) { this->trigger(status, left, top, right, bottom, yaw, pitch, roll); }); - } -}; - -class EnrollmentDoneTrigger : public Trigger { - public: - explicit EnrollmentDoneTrigger(HlkFm22xComponent *parent) { - parent->add_on_enrollment_done_callback( - [this](int16_t face_id, uint8_t direction) { this->trigger(face_id, direction); }); - } -}; - -class EnrollmentFailedTrigger : public Trigger { - public: - explicit EnrollmentFailedTrigger(HlkFm22xComponent *parent) { - parent->add_on_enrollment_failed_callback([this](uint8_t error) { this->trigger(error); }); - } -}; - template class EnrollmentAction : public Action, public Parented { public: TEMPLATABLE_VALUE(std::string, name) diff --git a/esphome/components/hlw8012/hlw8012.cpp b/esphome/components/hlw8012/hlw8012.cpp index f037ee9d8b..22f292e47e 100644 --- a/esphome/components/hlw8012/hlw8012.cpp +++ b/esphome/components/hlw8012/hlw8012.cpp @@ -48,7 +48,6 @@ void HLW8012Component::dump_config() { LOG_SENSOR(" ", "Power", this->power_sensor_); LOG_SENSOR(" ", "Energy", this->energy_sensor_); } -float HLW8012Component::get_setup_priority() const { return setup_priority::DATA; } void HLW8012Component::update() { // HLW8012 has 50% duty cycle pulse_counter::pulse_counter_t raw_cf = this->cf_store_.read_raw_value(); @@ -74,13 +73,13 @@ void HLW8012Component::update() { // Only read cf1 after one cycle. Apparently it's quite unstable after being changed. if (this->current_mode_) { float current = cf1_hz * this->current_multiplier_; - ESP_LOGD(TAG, "Got power=%.1fW, current=%.1fA", power, current); + ESP_LOGV(TAG, "Got power=%.1fW, current=%.1fA", power, current); if (this->current_sensor_ != nullptr) { this->current_sensor_->publish_state(current); } } else { float voltage = cf1_hz * this->voltage_multiplier_; - ESP_LOGD(TAG, "Got power=%.1fW, voltage=%.1fV", power, voltage); + ESP_LOGV(TAG, "Got power=%.1fW, voltage=%.1fV", power, voltage); if (this->voltage_sensor_ != nullptr) { this->voltage_sensor_->publish_state(voltage); } diff --git a/esphome/components/hlw8012/hlw8012.h b/esphome/components/hlw8012/hlw8012.h index 312391f533..8a13ec07d8 100644 --- a/esphome/components/hlw8012/hlw8012.h +++ b/esphome/components/hlw8012/hlw8012.h @@ -31,7 +31,6 @@ class HLW8012Component : public PollingComponent { void setup() override; void dump_config() override; - float get_setup_priority() const override; void update() override; void set_initial_mode(HLW8012InitialMode initial_mode) { diff --git a/esphome/components/hlw8012/sensor.py b/esphome/components/hlw8012/sensor.py index 201ea4451f..1d793ac6b1 100644 --- a/esphome/components/hlw8012/sensor.py +++ b/esphome/components/hlw8012/sensor.py @@ -1,6 +1,7 @@ from esphome import pins import esphome.codegen as cg from esphome.components import sensor +from esphome.components.esp32 import include_builtin_idf_component import esphome.config_validation as cv from esphome.const import ( CONF_CHANGE_MODE_EVERY, @@ -25,6 +26,7 @@ from esphome.const import ( UNIT_WATT, UNIT_WATT_HOURS, ) +from esphome.core import CORE AUTO_LOAD = ["pulse_counter"] @@ -91,6 +93,9 @@ CONFIG_SCHEMA = cv.Schema( async def to_code(config): + if CORE.is_esp32: + include_builtin_idf_component("esp_driver_pcnt") + var = cg.new_Pvariable(config[CONF_ID]) await cg.register_component(var, config) diff --git a/esphome/components/hm3301/hm3301.cpp b/esphome/components/hm3301/hm3301.cpp index 00fb85397c..9343b47823 100644 --- a/esphome/components/hm3301/hm3301.cpp +++ b/esphome/components/hm3301/hm3301.cpp @@ -31,8 +31,6 @@ void HM3301Component::dump_config() { LOG_SENSOR(" ", "AQI", this->aqi_sensor_); } -float HM3301Component::get_setup_priority() const { return setup_priority::DATA; } - void HM3301Component::update() { if (this->read(data_buffer_, 29) != i2c::ERROR_OK) { ESP_LOGW(TAG, "Read result failed"); diff --git a/esphome/components/hm3301/hm3301.h b/esphome/components/hm3301/hm3301.h index e155ed6b4b..6b10a5e237 100644 --- a/esphome/components/hm3301/hm3301.h +++ b/esphome/components/hm3301/hm3301.h @@ -23,7 +23,6 @@ class HM3301Component : public PollingComponent, public i2c::I2CDevice { void setup() override; void dump_config() override; - float get_setup_priority() const override; void update() override; protected: diff --git a/esphome/components/hmac_sha256/hmac_sha256.cpp b/esphome/components/hmac_sha256/hmac_sha256.cpp index 2146e961bc..c113cb48a6 100644 --- a/esphome/components/hmac_sha256/hmac_sha256.cpp +++ b/esphome/components/hmac_sha256/hmac_sha256.cpp @@ -7,7 +7,55 @@ namespace esphome::hmac_sha256 { constexpr size_t SHA256_DIGEST_SIZE = 32; -#if defined(USE_ESP32) || defined(USE_LIBRETINY) +#if defined(USE_HMAC_SHA256_PSA) + +// ESP-IDF 6.0 ships mbedtls 4.0 which removed the legacy mbedtls_md HMAC API. +// Use the PSA Crypto MAC API instead. + +HmacSHA256::~HmacSHA256() { + psa_mac_abort(&this->op_); + psa_destroy_key(this->key_id_); +} + +void HmacSHA256::init(const uint8_t *key, size_t len) { + psa_mac_abort(&this->op_); + psa_destroy_key(this->key_id_); + + psa_key_attributes_t attributes = PSA_KEY_ATTRIBUTES_INIT; + psa_set_key_type(&attributes, PSA_KEY_TYPE_HMAC); + psa_set_key_usage_flags(&attributes, PSA_KEY_USAGE_SIGN_MESSAGE); + psa_set_key_algorithm(&attributes, PSA_ALG_HMAC(PSA_ALG_SHA_256)); + psa_import_key(&attributes, key, len, &this->key_id_); + + this->op_ = PSA_MAC_OPERATION_INIT; + psa_mac_sign_setup(&this->op_, this->key_id_, PSA_ALG_HMAC(PSA_ALG_SHA_256)); +} + +void HmacSHA256::add(const uint8_t *data, size_t len) { psa_mac_update(&this->op_, data, len); } + +void HmacSHA256::calculate() { + size_t mac_length; + psa_mac_sign_finish(&this->op_, this->digest_, sizeof(this->digest_), &mac_length); +} + +void HmacSHA256::get_bytes(uint8_t *output) { memcpy(output, this->digest_, SHA256_DIGEST_SIZE); } + +void HmacSHA256::get_hex(char *output) { + format_hex_to(output, SHA256_DIGEST_SIZE * 2 + 1, this->digest_, SHA256_DIGEST_SIZE); +} + +bool HmacSHA256::equals_bytes(const uint8_t *expected) { + return memcmp(this->digest_, expected, SHA256_DIGEST_SIZE) == 0; +} + +bool HmacSHA256::equals_hex(const char *expected) { + char hex_output[SHA256_DIGEST_SIZE * 2 + 1]; + this->get_hex(hex_output); + hex_output[SHA256_DIGEST_SIZE * 2] = '\0'; + return strncmp(hex_output, expected, SHA256_DIGEST_SIZE * 2) == 0; +} + +#elif defined(USE_HMAC_SHA256_MBEDTLS) HmacSHA256::~HmacSHA256() { mbedtls_md_free(&this->ctx_); } @@ -93,7 +141,7 @@ bool HmacSHA256::equals_bytes(const uint8_t *expected) { return this->ohash_.equ bool HmacSHA256::equals_hex(const char *expected) { return this->ohash_.equals_hex(expected); } -#endif // USE_ESP32 || USE_LIBRETINY +#endif // USE_HMAC_SHA256_PSA / USE_HMAC_SHA256_MBEDTLS } // namespace esphome::hmac_sha256 #endif diff --git a/esphome/components/hmac_sha256/hmac_sha256.h b/esphome/components/hmac_sha256/hmac_sha256.h index 85622cac46..22129b1182 100644 --- a/esphome/components/hmac_sha256/hmac_sha256.h +++ b/esphome/components/hmac_sha256/hmac_sha256.h @@ -5,7 +5,19 @@ #include -#if defined(USE_ESP32) || defined(USE_LIBRETINY) +#if defined(USE_ESP32) +#include +#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(6, 0, 0) +// mbedtls 4.0 (IDF 6.0) removed the legacy mbedtls_md HMAC API. +// Use the PSA Crypto MAC API instead. +#define USE_HMAC_SHA256_PSA +#include +#else +#define USE_HMAC_SHA256_MBEDTLS +#include "mbedtls/md.h" +#endif +#elif defined(USE_LIBRETINY) +#define USE_HMAC_SHA256_MBEDTLS #include "mbedtls/md.h" #else #include "esphome/components/sha256/sha256.h" @@ -45,7 +57,12 @@ class HmacSHA256 { bool equals_hex(const char *expected); protected: -#if defined(USE_ESP32) || defined(USE_LIBRETINY) +#if defined(USE_HMAC_SHA256_PSA) + static constexpr size_t SHA256_DIGEST_SIZE = 32; + psa_mac_operation_t op_ = PSA_MAC_OPERATION_INIT; + mbedtls_svc_key_id_t key_id_ = MBEDTLS_SVC_KEY_ID_INIT; + uint8_t digest_[SHA256_DIGEST_SIZE]{}; +#elif defined(USE_HMAC_SHA256_MBEDTLS) static constexpr size_t SHA256_DIGEST_SIZE = 32; mbedtls_md_context_t ctx_{}; uint8_t digest_[SHA256_DIGEST_SIZE]{}; diff --git a/esphome/components/hmc5883l/hmc5883l.cpp b/esphome/components/hmc5883l/hmc5883l.cpp index 101493ad91..bee5282125 100644 --- a/esphome/components/hmc5883l/hmc5883l.cpp +++ b/esphome/components/hmc5883l/hmc5883l.cpp @@ -83,7 +83,6 @@ void HMC5883LComponent::dump_config() { LOG_SENSOR(" ", "Z Axis", this->z_sensor_); LOG_SENSOR(" ", "Heading", this->heading_sensor_); } -float HMC5883LComponent::get_setup_priority() const { return setup_priority::DATA; } void HMC5883LComponent::update() { uint16_t raw_x, raw_y, raw_z; if (!this->read_byte_16(HMC5883L_REGISTER_DATA_X_MSB, &raw_x) || @@ -96,7 +95,7 @@ void HMC5883LComponent::update() { float mg_per_bit; switch (this->range_) { case HMC5883L_RANGE_88_UT: - mg_per_bit = 0.073f; + mg_per_bit = 0.73f; break; case HMC5883L_RANGE_130_UT: mg_per_bit = 0.92f; diff --git a/esphome/components/hmc5883l/hmc5883l.h b/esphome/components/hmc5883l/hmc5883l.h index 06fba2af9d..b5cf93e62b 100644 --- a/esphome/components/hmc5883l/hmc5883l.h +++ b/esphome/components/hmc5883l/hmc5883l.h @@ -39,7 +39,6 @@ class HMC5883LComponent : public PollingComponent, public i2c::I2CDevice { public: void setup() override; void dump_config() override; - float get_setup_priority() const override; void update() override; void set_oversampling(HMC5883LOversampling oversampling) { oversampling_ = oversampling; } @@ -62,7 +61,7 @@ class HMC5883LComponent : public PollingComponent, public i2c::I2CDevice { NONE = 0, COMMUNICATION_FAILED, ID_REGISTERS, - } error_code_; + } error_code_{NONE}; HighFrequencyLoopRequester high_freq_; }; diff --git a/esphome/components/homeassistant/number/homeassistant_number.cpp b/esphome/components/homeassistant/number/homeassistant_number.cpp index 00ea88ff16..da802b7fe9 100644 --- a/esphome/components/homeassistant/number/homeassistant_number.cpp +++ b/esphome/components/homeassistant/number/homeassistant_number.cpp @@ -55,15 +55,15 @@ void HomeassistantNumber::step_retrieved_(StringRef step) { } void HomeassistantNumber::setup() { - api::global_api_server->subscribe_home_assistant_state( - this->entity_id_, nullptr, std::bind(&HomeassistantNumber::state_changed_, this, std::placeholders::_1)); + api::global_api_server->subscribe_home_assistant_state(this->entity_id_, nullptr, + [this](StringRef state) { this->state_changed_(state); }); - api::global_api_server->get_home_assistant_state( - this->entity_id_, "min", std::bind(&HomeassistantNumber::min_retrieved_, this, std::placeholders::_1)); - api::global_api_server->get_home_assistant_state( - this->entity_id_, "max", std::bind(&HomeassistantNumber::max_retrieved_, this, std::placeholders::_1)); - api::global_api_server->get_home_assistant_state( - this->entity_id_, "step", std::bind(&HomeassistantNumber::step_retrieved_, this, std::placeholders::_1)); + api::global_api_server->get_home_assistant_state(this->entity_id_, "min", + [this](StringRef min) { this->min_retrieved_(min); }); + api::global_api_server->get_home_assistant_state(this->entity_id_, "max", + [this](StringRef max) { this->max_retrieved_(max); }); + api::global_api_server->get_home_assistant_state(this->entity_id_, "step", + [this](StringRef step) { this->step_retrieved_(step); }); } void HomeassistantNumber::dump_config() { diff --git a/esphome/components/homeassistant/time/homeassistant_time.cpp b/esphome/components/homeassistant/time/homeassistant_time.cpp index e72c5a21f5..d039892073 100644 --- a/esphome/components/homeassistant/time/homeassistant_time.cpp +++ b/esphome/components/homeassistant/time/homeassistant_time.cpp @@ -11,8 +11,6 @@ void HomeassistantTime::dump_config() { RealTimeClock::dump_config(); } -float HomeassistantTime::get_setup_priority() const { return setup_priority::DATA; } - void HomeassistantTime::setup() { global_homeassistant_time = this; } void HomeassistantTime::update() { api::global_api_server->request_time(); } diff --git a/esphome/components/homeassistant/time/homeassistant_time.h b/esphome/components/homeassistant/time/homeassistant_time.h index 36e28ea16b..455ded2022 100644 --- a/esphome/components/homeassistant/time/homeassistant_time.h +++ b/esphome/components/homeassistant/time/homeassistant_time.h @@ -7,13 +7,12 @@ namespace esphome { namespace homeassistant { -class HomeassistantTime : public time::RealTimeClock { +class HomeassistantTime final : public time::RealTimeClock { public: void setup() override; void update() override; void dump_config() override; void set_epoch_time(uint32_t epoch) { this->synchronize_epoch_(epoch); } - float get_setup_priority() const override; }; extern HomeassistantTime *global_homeassistant_time; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) diff --git a/esphome/components/honeywell_hih_i2c/honeywell_hih.cpp b/esphome/components/honeywell_hih_i2c/honeywell_hih.cpp index 5f2b009972..904672d136 100644 --- a/esphome/components/honeywell_hih_i2c/honeywell_hih.cpp +++ b/esphome/components/honeywell_hih_i2c/honeywell_hih.cpp @@ -91,7 +91,5 @@ void HoneywellHIComponent::dump_config() { LOG_UPDATE_INTERVAL(this); } -float HoneywellHIComponent::get_setup_priority() const { return setup_priority::DATA; } - } // namespace honeywell_hih_i2c } // namespace esphome diff --git a/esphome/components/honeywell_hih_i2c/honeywell_hih.h b/esphome/components/honeywell_hih_i2c/honeywell_hih.h index 4457eab1da..79140f7399 100644 --- a/esphome/components/honeywell_hih_i2c/honeywell_hih.h +++ b/esphome/components/honeywell_hih_i2c/honeywell_hih.h @@ -11,7 +11,6 @@ namespace honeywell_hih_i2c { class HoneywellHIComponent : public PollingComponent, public i2c::I2CDevice { public: void dump_config() override; - float get_setup_priority() const override; void loop() override; void update() override; diff --git a/esphome/components/honeywellabp/honeywellabp.cpp b/esphome/components/honeywellabp/honeywellabp.cpp index c204325dfc..58c5df230f 100644 --- a/esphome/components/honeywellabp/honeywellabp.cpp +++ b/esphome/components/honeywellabp/honeywellabp.cpp @@ -35,10 +35,7 @@ uint8_t HONEYWELLABPSensor::readsensor_() { pressure_count_ = ((uint16_t) (buf_[0]) << 8 & 0x3F00) | ((uint16_t) (buf_[1]) & 0xFF); // 11 - bit temperature is all of byte 2 (lowest 8 bits) and the first three bits of byte 3 temperature_count_ = (((uint16_t) (buf_[2]) << 3) & 0x7F8) | (((uint16_t) (buf_[3]) >> 5) & 0x7); - ESP_LOGV(TAG, - "Sensor pressure_count_ %d\n" - "Sensor temperature_count_ %d", - pressure_count_, temperature_count_); + ESP_LOGV(TAG, "Sensor pressure_count_ %d, temperature_count_ %d", pressure_count_, temperature_count_); } return status_; } diff --git a/esphome/components/honeywellabp2_i2c/honeywellabp2.h b/esphome/components/honeywellabp2_i2c/honeywellabp2.h index 274de847ac..d29ebb855d 100644 --- a/esphome/components/honeywellabp2_i2c/honeywellabp2.h +++ b/esphome/components/honeywellabp2_i2c/honeywellabp2.h @@ -45,8 +45,8 @@ class HONEYWELLABP2Sensor : public PollingComponent, public i2c::I2CDevice { const float max_count_b_ = 11744051.2; // (70% of 2^24 counts or 0xB33333) const float min_count_b_ = 5033164.8; // (30% of 2^24 counts or 0x4CCCCC) - float max_count_; - float min_count_; + float max_count_{max_count_a_}; + float min_count_{min_count_a_}; bool measurement_running_ = false; uint8_t raw_data_[7]; // holds output data diff --git a/esphome/components/host/__init__.py b/esphome/components/host/__init__.py index ba05e497c8..8adbfb02ec 100644 --- a/esphome/components/host/__init__.py +++ b/esphome/components/host/__init__.py @@ -41,6 +41,7 @@ CONFIG_SCHEMA = cv.All( async def to_code(config): cg.add_build_flag("-DUSE_HOST") + cg.add_define("USE_NATIVE_64BIT_TIME") cg.add_define("USE_ESPHOME_HOST_MAC_ADDRESS", config[CONF_MAC_ADDRESS].parts) cg.add_build_flag("-std=gnu++20") cg.add_define("ESPHOME_BOARD", "host") diff --git a/esphome/components/host/core.cpp b/esphome/components/host/core.cpp index 164d622dd4..0ade4274fe 100644 --- a/esphome/components/host/core.cpp +++ b/esphome/components/host/core.cpp @@ -1,17 +1,24 @@ #ifdef USE_HOST +#include "esphome/core/application.h" #include "esphome/core/hal.h" #include "esphome/core/helpers.h" #include "preferences.h" +#include #include #include #include #include +namespace { +volatile sig_atomic_t s_signal_received = 0; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) +void signal_handler(int signal) { s_signal_received = signal; } +} // namespace + namespace esphome { -void IRAM_ATTR HOT yield() { ::sched_yield(); } +void HOT yield() { ::sched_yield(); } uint32_t IRAM_ATTR HOT millis() { struct timespec spec; clock_gettime(CLOCK_MONOTONIC, &spec); @@ -19,7 +26,12 @@ uint32_t IRAM_ATTR HOT millis() { uint32_t ms = round(spec.tv_nsec / 1e6); return ((uint32_t) seconds) * 1000U + ms; } -void IRAM_ATTR HOT delay(uint32_t ms) { +uint64_t millis_64() { + struct timespec spec; + clock_gettime(CLOCK_MONOTONIC, &spec); + return static_cast(spec.tv_sec) * 1000ULL + static_cast(spec.tv_nsec) / 1000000ULL; +} +void HOT delay(uint32_t ms) { struct timespec ts; ts.tv_sec = ms / 1000; ts.tv_nsec = (ms % 1000) * 1000000; @@ -48,11 +60,10 @@ void arch_restart() { exit(0); } void arch_init() { // pass } -void IRAM_ATTR HOT arch_feed_wdt() { +void HOT arch_feed_wdt() { // pass } -uint8_t progmem_read_byte(const uint8_t *addr) { return *addr; } uint32_t arch_get_cpu_cycle_count() { struct timespec spec; clock_gettime(CLOCK_MONOTONIC, &spec); @@ -67,11 +78,17 @@ uint32_t arch_get_cpu_freq_hz() { return 1000000000U; } void setup(); void loop(); int main() { + // Install signal handlers for graceful shutdown (flushes preferences to disk) + std::signal(SIGINT, signal_handler); + std::signal(SIGTERM, signal_handler); + esphome::host::setup_preferences(); setup(); - while (true) { + while (s_signal_received == 0) { loop(); } + esphome::App.run_safe_shutdown_hooks(); + return 0; } #endif // USE_HOST diff --git a/esphome/components/host/helpers.cpp b/esphome/components/host/helpers.cpp index fdad4f5cb6..7e8849b3e1 100644 --- a/esphome/components/host/helpers.cpp +++ b/esphome/components/host/helpers.cpp @@ -8,8 +8,6 @@ #include #endif #include -#include -#include #include "esphome/core/defines.h" #include "esphome/core/log.h" @@ -18,13 +16,6 @@ namespace esphome { static const char *const TAG = "helpers.host"; -uint32_t random_uint32() { - std::random_device dev; - std::mt19937 rng(dev()); - std::uniform_int_distribution dist(0, std::numeric_limits::max()); - return dist(rng); -} - bool random_bytes(uint8_t *data, size_t len) { FILE *fp = fopen("/dev/urandom", "r"); if (fp == nullptr) { diff --git a/esphome/components/host/preference_backend.h b/esphome/components/host/preference_backend.h new file mode 100644 index 0000000000..68537cad28 --- /dev/null +++ b/esphome/components/host/preference_backend.h @@ -0,0 +1,29 @@ +#pragma once +#ifdef USE_HOST + +#include +#include + +namespace esphome::host { + +class HostPreferenceBackend final { + public: + explicit HostPreferenceBackend(uint32_t key) : key_(key) {} + + bool save(const uint8_t *data, size_t len); + bool load(uint8_t *data, size_t len); + + protected: + uint32_t key_{}; +}; + +class HostPreferences; +HostPreferences *get_preferences(); + +} // namespace esphome::host + +namespace esphome { +using PreferenceBackend = host::HostPreferenceBackend; +} // namespace esphome + +#endif // USE_HOST diff --git a/esphome/components/host/preferences.cpp b/esphome/components/host/preferences.cpp index 7b939cdebb..c0be270062 100644 --- a/esphome/components/host/preferences.cpp +++ b/esphome/components/host/preferences.cpp @@ -4,17 +4,22 @@ #include #include "preferences.h" #include "esphome/core/application.h" +#include "esphome/core/log.h" -namespace esphome { -namespace host { +namespace esphome::host { namespace fs = std::filesystem; -static const char *const TAG = "host.preferences"; +static const char *const TAG = "preferences"; void HostPreferences::setup_() { if (this->setup_complete_) return; - this->filename_.append(getenv("HOME")); + const char *home = getenv("HOME"); + if (home == nullptr) { + ESP_LOGE(TAG, "HOME environment variable is not set"); + abort(); + } + this->filename_.append(home); this->filename_.append("/.esphome"); this->filename_.append("/prefs"); fs::create_directories(this->filename_); @@ -44,9 +49,12 @@ void HostPreferences::setup_() { bool HostPreferences::sync() { this->setup_(); FILE *fp = fopen(this->filename_.c_str(), "wb"); - std::map>::iterator it; + if (fp == nullptr) { + ESP_LOGE(TAG, "Failed to open preferences file for writing: %s", this->filename_.c_str()); + return false; + } - for (it = this->data.begin(); it != this->data.end(); ++it) { + for (auto it = this->data.begin(); it != this->data.end(); ++it) { fwrite(&it->first, sizeof(uint32_t), 1, fp); uint8_t len = it->second.size(); fwrite(&len, sizeof(len), 1, fp); @@ -66,10 +74,13 @@ ESPPreferenceObject HostPreferences::make_preference(size_t length, uint32_t typ return ESPPreferenceObject(backend); }; +static HostPreferences s_preferences; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) + +HostPreferences *get_preferences() { return &s_preferences; } + void setup_preferences() { - auto *pref = new HostPreferences(); // NOLINT(cppcoreguidelines-owning-memory) - host_preferences = pref; - global_preferences = pref; + host_preferences = &s_preferences; + global_preferences = &s_preferences; } bool HostPreferenceBackend::save(const uint8_t *data, size_t len) { @@ -78,9 +89,11 @@ bool HostPreferenceBackend::save(const uint8_t *data, size_t len) { bool HostPreferenceBackend::load(uint8_t *data, size_t len) { return host_preferences->load(this->key_, data, len); } -HostPreferences *host_preferences; -} // namespace host +HostPreferences *host_preferences; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) +} // namespace esphome::host + +namespace esphome { ESPPreferences *global_preferences; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) } // namespace esphome diff --git a/esphome/components/host/preferences.h b/esphome/components/host/preferences.h index 6b2e7eb8f9..25858799ff 100644 --- a/esphome/components/host/preferences.h +++ b/esphome/components/host/preferences.h @@ -1,33 +1,22 @@ #pragma once - #ifdef USE_HOST -#include "esphome/core/preferences.h" +#include "esphome/core/preference_backend.h" +#include #include +#include +#include -namespace esphome { -namespace host { +namespace esphome::host { -class HostPreferenceBackend : public ESPPreferenceBackend { +class HostPreferences final : public PreferencesMixin { public: - explicit HostPreferenceBackend(uint32_t key) { this->key_ = key; } + using PreferencesMixin::make_preference; + bool sync(); + bool reset(); - bool save(const uint8_t *data, size_t len) override; - bool load(uint8_t *data, size_t len) override; - - protected: - uint32_t key_{}; -}; - -class HostPreferences : public ESPPreferences { - public: - bool sync() override; - bool reset() override; - - ESPPreferenceObject make_preference(size_t length, uint32_t type, bool in_flash) override; - ESPPreferenceObject make_preference(size_t length, uint32_t type) override { - return make_preference(length, type, false); - } + ESPPreferenceObject make_preference(size_t length, uint32_t type, bool in_flash); + ESPPreferenceObject make_preference(size_t length, uint32_t type) { return make_preference(length, type, false); } bool save(uint32_t key, const uint8_t *data, size_t len) { if (len > 255) @@ -58,10 +47,12 @@ class HostPreferences : public ESPPreferences { std::string filename_{}; std::map> data{}; }; + void setup_preferences(); extern HostPreferences *host_preferences; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) -} // namespace host -} // namespace esphome +} // namespace esphome::host + +DECLARE_PREFERENCE_ALIASES(esphome::host::HostPreferences) #endif // USE_HOST diff --git a/esphome/components/hte501/hte501.cpp b/esphome/components/hte501/hte501.cpp index cde6886109..ef9ef1fabf 100644 --- a/esphome/components/hte501/hte501.cpp +++ b/esphome/components/hte501/hte501.cpp @@ -43,14 +43,13 @@ void HTE501Component::dump_config() { LOG_SENSOR(" ", "Humidity", this->humidity_sensor_); } -float HTE501Component::get_setup_priority() const { return setup_priority::DATA; } void HTE501Component::update() { uint8_t address_1[] = {0x2C, 0x1B}; this->write(address_1, 2); this->set_timeout(50, [this]() { uint8_t i2c_response[6]; this->read(i2c_response, 6); - if (i2c_response[2] != crc8(i2c_response, 2, 0xFF, 0x31, true) && + if (i2c_response[2] != crc8(i2c_response, 2, 0xFF, 0x31, true) || i2c_response[5] != crc8(i2c_response + 3, 2, 0xFF, 0x31, true)) { this->error_code_ = CRC_CHECK_FAILED; this->status_set_warning(); diff --git a/esphome/components/hte501/hte501.h b/esphome/components/hte501/hte501.h index a7072d5bdb..7f29885f49 100644 --- a/esphome/components/hte501/hte501.h +++ b/esphome/components/hte501/hte501.h @@ -13,14 +13,13 @@ class HTE501Component : public PollingComponent, public i2c::I2CDevice { void set_temperature_sensor(sensor::Sensor *temperature_sensor) { temperature_sensor_ = temperature_sensor; } void set_humidity_sensor(sensor::Sensor *humidity_sensor) { humidity_sensor_ = humidity_sensor; } - float get_setup_priority() const override; void setup() override; void dump_config() override; void update() override; protected: - sensor::Sensor *temperature_sensor_; - sensor::Sensor *humidity_sensor_; + sensor::Sensor *temperature_sensor_{nullptr}; + sensor::Sensor *humidity_sensor_{nullptr}; enum ErrorCode { NONE = 0, COMMUNICATION_FAILED, CRC_CHECK_FAILED } error_code_{NONE}; }; diff --git a/esphome/components/http_request/__init__.py b/esphome/components/http_request/__init__.py index 07bc758037..416432cfc4 100644 --- a/esphome/components/http_request/__init__.py +++ b/esphome/components/http_request/__init__.py @@ -50,6 +50,8 @@ CONF_FOLLOW_REDIRECTS = "follow_redirects" CONF_REDIRECT_LIMIT = "redirect_limit" CONF_BUFFER_SIZE_RX = "buffer_size_rx" CONF_BUFFER_SIZE_TX = "buffer_size_tx" +CONF_TLS_BUFFER_SIZE_RX = "tls_buffer_size_rx" +CONF_TLS_BUFFER_SIZE_TX = "tls_buffer_size_tx" CONF_CA_CERTIFICATE_PATH = "ca_certificate_path" CONF_MAX_RESPONSE_BUFFER_SIZE = "max_response_buffer_size" @@ -124,6 +126,12 @@ CONFIG_SCHEMA = cv.All( cv.SplitDefault(CONF_BUFFER_SIZE_TX, esp32=512): cv.All( cv.uint16_t, cv.only_on_esp32 ), + cv.SplitDefault(CONF_TLS_BUFFER_SIZE_RX, esp8266=512): cv.All( + cv.uint16_t, cv.only_on_esp8266 + ), + cv.SplitDefault(CONF_TLS_BUFFER_SIZE_TX, esp8266=512): cv.All( + cv.uint16_t, cv.only_on_esp8266 + ), cv.Optional(CONF_CA_CERTIFICATE_PATH): cv.All( cv.file_, cv.Any(cv.only_on(PLATFORM_HOST), cv.only_on_esp32), @@ -150,11 +158,16 @@ async def to_code(config): if CORE.is_esp8266 and not config[CONF_ESP8266_DISABLE_SSL_SUPPORT]: cg.add_define("USE_HTTP_REQUEST_ESP8266_HTTPS") + cg.add(var.set_tls_buffer_size_rx(config[CONF_TLS_BUFFER_SIZE_RX])) + cg.add(var.set_tls_buffer_size_tx(config[CONF_TLS_BUFFER_SIZE_TX])) if timeout_ms := config.get(CONF_WATCHDOG_TIMEOUT): cg.add(var.set_watchdog_timeout(timeout_ms)) 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_tx(config[CONF_BUFFER_SIZE_TX])) cg.add(var.set_verify_ssl(config[CONF_VERIFY_SSL])) @@ -266,13 +279,22 @@ HTTP_REQUEST_SEND_ACTION_SCHEMA = HTTP_REQUEST_ACTION_SCHEMA.extend( @automation.register_action( - "http_request.get", HttpRequestSendAction, HTTP_REQUEST_GET_ACTION_SCHEMA + "http_request.get", + HttpRequestSendAction, + HTTP_REQUEST_GET_ACTION_SCHEMA, + synchronous=True, ) @automation.register_action( - "http_request.post", HttpRequestSendAction, HTTP_REQUEST_POST_ACTION_SCHEMA + "http_request.post", + HttpRequestSendAction, + HTTP_REQUEST_POST_ACTION_SCHEMA, + synchronous=True, ) @automation.register_action( - "http_request.send", HttpRequestSendAction, HTTP_REQUEST_SEND_ACTION_SCHEMA + "http_request.send", + HttpRequestSendAction, + HTTP_REQUEST_SEND_ACTION_SCHEMA, + synchronous=True, ) async def http_request_action_to_code(config, action_id, template_arg, args): paren = await cg.get_variable(config[CONF_ID]) @@ -299,15 +321,19 @@ async def http_request_action_to_code(config, action_id, template_arg, args): lambda_ = await cg.process_lambda(json_, args_, return_type=cg.void) cg.add(var.set_json(lambda_)) else: + cg.add(var.init_json(len(json_))) for key in json_: template_ = await cg.templatable(json_[key], args, cg.std_string) cg.add(var.add_json(key, template_)) - for key, value in config.get(CONF_REQUEST_HEADERS, {}).items(): + request_headers = config.get(CONF_REQUEST_HEADERS, {}) + if request_headers: + cg.add(var.init_request_headers(len(request_headers))) + for key, value in request_headers.items(): template_ = await cg.templatable(value, args, cg.const_char_ptr) cg.add(var.add_request_header(key, template_)) for value in config.get(CONF_COLLECT_HEADERS, []): - cg.add(var.add_collect_header(value)) + cg.add(var.add_collect_header(value.lower())) if response_conf := config.get(CONF_ON_RESPONSE): if capture_response: diff --git a/esphome/components/http_request/http_request.cpp b/esphome/components/http_request/http_request.cpp index 11dde4715a..2c74638f12 100644 --- a/esphome/components/http_request/http_request.cpp +++ b/esphome/components/http_request/http_request.cpp @@ -11,7 +11,7 @@ static const char *const TAG = "http_request"; void HttpRequestComponent::dump_config() { ESP_LOGCONFIG(TAG, "HTTP Request:\n" - " Timeout: %ums\n" + " Timeout: %" PRIu32 "ms\n" " User-Agent: %s\n" " Follow redirects: %s\n" " Redirect limit: %d", @@ -22,23 +22,15 @@ void HttpRequestComponent::dump_config() { } std::string HttpContainer::get_response_header(const std::string &header_name) { - auto response_headers = this->get_response_headers(); - auto header_name_lower_case = str_lower_case(header_name); - if (response_headers.count(header_name_lower_case) == 0) { - ESP_LOGW(TAG, "No header with name %s found", header_name_lower_case.c_str()); - return ""; - } else { - auto values = response_headers[header_name_lower_case]; - if (values.empty()) { - ESP_LOGE(TAG, "header with name %s returned an empty list, this shouldn't happen", - header_name_lower_case.c_str()); - return ""; - } else { - auto header_value = values.front(); - ESP_LOGD(TAG, "Header with name %s found with value %s", header_name_lower_case.c_str(), header_value.c_str()); - return header_value; + auto lower = str_lower_case(header_name); + for (const auto &entry : this->response_headers_) { + if (entry.name == lower) { + ESP_LOGD(TAG, "Header with name %s found with value %s", lower.c_str(), entry.value.c_str()); + return entry.value; } } + ESP_LOGW(TAG, "No header with name %s found", lower.c_str()); + return ""; } } // namespace esphome::http_request diff --git a/esphome/components/http_request/http_request.h b/esphome/components/http_request/http_request.h index fb39ca504c..73dbda8694 100644 --- a/esphome/components/http_request/http_request.h +++ b/esphome/components/http_request/http_request.h @@ -1,7 +1,6 @@ #pragma once #include -#include #include #include #include @@ -26,6 +25,7 @@ struct Header { enum HttpStatus { HTTP_STATUS_OK = 200, HTTP_STATUS_NO_CONTENT = 204, + HTTP_STATUS_RESET_CONTENT = 205, HTTP_STATUS_PARTIAL_CONTENT = 206, /* 3xx - Redirection */ @@ -79,6 +79,16 @@ inline bool is_redirect(int const status) { */ inline bool is_success(int const status) { return status >= HTTP_STATUS_OK && status < HTTP_STATUS_MULTIPLE_CHOICES; } +/// Check if a header name should be collected (linear scan, fine for small lists) +inline bool should_collect_header(const std::vector &lower_case_collect_headers, + const std::string &lower_header_name) { + for (const auto &h : lower_case_collect_headers) { + if (h == lower_header_name) + return true; + } + return false; +} + /* * HTTP Container Read Semantics * ============================= @@ -102,6 +112,42 @@ inline bool is_success(int const status) { return status >= HTTP_STATUS_OK && st * - ESP-IDF: blocking reads, 0 only returned when all content read * - Arduino: non-blocking, 0 means "no data yet" or "all content read" * + * Chunked responses that complete in a reasonable time work correctly on both + * platforms. The limitation below applies only to *streaming* chunked + * responses where data arrives slowly over a long period. + * + * Streaming chunked responses are NOT supported (all platforms): + * The read helpers (http_read_loop_result, http_read_fully) block the main + * event loop until all response data is received. For streaming responses + * where data trickles in slowly (e.g., TTS streaming via ffmpeg proxy), + * this starves the event loop on both ESP-IDF and Arduino. If data arrives + * just often enough to avoid the caller's timeout, the loop runs + * indefinitely. If data stops entirely, ESP-IDF fails with + * -ESP_ERR_HTTP_EAGAIN (transport timeout) while Arduino spins with + * delay(1) until the caller's timeout fires. Supporting streaming requires + * a non-blocking incremental read pattern that yields back to the event + * loop between chunks. Components that need streaming should use + * esp_http_client directly on a separate FreeRTOS task with + * esp_http_client_is_complete_data_received() for completion detection + * (see audio_reader.cpp for an example). + * + * Chunked transfer encoding - platform differences: + * - ESP-IDF HttpContainer: + * HttpContainerIDF overrides is_read_complete() to call + * esp_http_client_is_complete_data_received(), which is the + * authoritative completion check for both chunked and non-chunked + * transfers. When esp_http_client_read() returns 0 for a completed + * chunked response, read() returns 0 and is_read_complete() returns + * true, so callers get COMPLETE from http_read_loop_result(). + * + * - Arduino HttpContainer: + * Chunked responses are decoded internally (see + * HttpContainerArduino::read_chunked_()). When the final chunk arrives, + * is_chunked_ is cleared and content_length is set to bytes_read_. + * Completion is then detected via is_read_complete(), and a subsequent + * read() returns 0 to indicate "all content read" (not + * HTTP_ERROR_CONNECTION_CLOSED). + * * Use the helper functions below instead of checking return values directly: * - http_read_loop_result(): for manual loops with per-chunk processing * - http_read_fully(): for simple "read N bytes into buffer" operations @@ -126,19 +172,21 @@ struct HttpReadResult { /// Result of processing a non-blocking read with timeout (for manual loops) enum class HttpReadLoopResult : uint8_t { - DATA, ///< Data was read, process it - RETRY, ///< No data yet, already delayed, caller should continue loop - ERROR, ///< Read error, caller should exit loop - TIMEOUT, ///< Timeout waiting for data, caller should exit loop + DATA, ///< Data was read, process it + COMPLETE, ///< All content has been read, caller should exit loop + RETRY, ///< No data yet, already delayed, caller should continue loop + ERROR, ///< Read error, caller should exit loop + TIMEOUT, ///< Timeout waiting for data, caller should exit loop }; /// Process a read result with timeout tracking and delay handling /// @param bytes_read_or_error Return value from read() - positive for bytes read, negative for error /// @param last_data_time Time of last successful read, updated when data received /// @param timeout_ms Maximum time to wait for data -/// @return DATA if data received, RETRY if should continue loop, ERROR/TIMEOUT if should exit -inline HttpReadLoopResult http_read_loop_result(int bytes_read_or_error, uint32_t &last_data_time, - uint32_t timeout_ms) { +/// @param is_read_complete Whether all expected content has been read (from HttpContainer::is_read_complete()) +/// @return How the caller should proceed - see HttpReadLoopResult enum +inline HttpReadLoopResult http_read_loop_result(int bytes_read_or_error, uint32_t &last_data_time, uint32_t timeout_ms, + bool is_read_complete) { if (bytes_read_or_error > 0) { last_data_time = millis(); return HttpReadLoopResult::DATA; @@ -146,7 +194,10 @@ inline HttpReadLoopResult http_read_loop_result(int bytes_read_or_error, uint32_ if (bytes_read_or_error < 0) { return HttpReadLoopResult::ERROR; } - // bytes_read_or_error == 0: no data available yet + // bytes_read_or_error == 0: either "no data yet" or "all content read" + if (is_read_complete) { + return HttpReadLoopResult::COMPLETE; + } if (millis() - last_data_time >= timeout_ms) { return HttpReadLoopResult::TIMEOUT; } @@ -159,9 +210,9 @@ class HttpRequestComponent; class HttpContainer : public Parented { public: virtual ~HttpContainer() = default; - size_t content_length; - int status_code; - uint32_t duration_ms; + size_t content_length{0}; + int status_code{-1}; ///< -1 indicates no response received yet + uint32_t duration_ms{0}; /** * @brief Read data from the HTTP response body. @@ -194,22 +245,35 @@ class HttpContainer : public Parented { virtual void end() = 0; void set_secure(bool secure) { this->secure_ = secure; } + void set_chunked(bool chunked) { this->is_chunked_ = chunked; } size_t get_bytes_read() const { return this->bytes_read_; } - /** - * @brief Get response headers. - * - * @return The key is the lower case response header name, the value is the header value. - */ - std::map> get_response_headers() { return this->response_headers_; } + /// Check if all expected content has been read. + /// Base implementation handles non-chunked responses and status-code-based no-body checks. + /// Platform implementations may override for chunked completion detection: + /// - ESP-IDF: overrides to call esp_http_client_is_complete_data_received() for chunked. + /// - Arduino: read_chunked_() clears is_chunked_ and sets content_length on the final + /// chunk, after which the base implementation detects completion. + virtual bool is_read_complete() const { + // Per RFC 9112, these responses have no body: + // - 1xx (Informational), 204 No Content, 205 Reset Content, 304 Not Modified + if ((this->status_code >= 100 && this->status_code < 200) || this->status_code == HTTP_STATUS_NO_CONTENT || + this->status_code == HTTP_STATUS_RESET_CONTENT || this->status_code == HTTP_STATUS_NOT_MODIFIED) { + return true; + } + // For non-chunked responses, complete when bytes_read >= content_length + // This handles both Content-Length: 0 and Content-Length: N cases + return !this->is_chunked_ && this->bytes_read_ >= this->content_length; + } std::string get_response_header(const std::string &header_name); protected: size_t bytes_read_{0}; bool secure_{false}; - std::map> response_headers_{}; + bool is_chunked_{false}; ///< True if response uses chunked transfer encoding + std::vector
response_headers_{}; }; /// Read data from HTTP container into buffer with timeout handling @@ -219,7 +283,7 @@ class HttpContainer : public Parented { /// @param total_size Total bytes to read /// @param chunk_size Maximum bytes per read call /// @param timeout_ms Read timeout in milliseconds -/// @return HttpReadResult with status and error_code on failure +/// @return HttpReadResult with status and error_code on failure; use container->get_bytes_read() for total bytes read inline HttpReadResult http_read_fully(HttpContainer *container, uint8_t *buffer, size_t total_size, size_t chunk_size, uint32_t timeout_ms) { size_t read_index = 0; @@ -231,9 +295,11 @@ inline HttpReadResult http_read_fully(HttpContainer *container, uint8_t *buffer, App.feed_wdt(); yield(); - auto result = http_read_loop_result(read_bytes_or_error, last_data_time, timeout_ms); + auto result = http_read_loop_result(read_bytes_or_error, last_data_time, timeout_ms, container->is_read_complete()); if (result == HttpReadLoopResult::RETRY) continue; + if (result == HttpReadLoopResult::COMPLETE) + break; // Server sent less data than requested, but transfer is complete if (result == HttpReadLoopResult::ERROR) return {HttpReadStatus::ERROR, read_bytes_or_error}; if (result == HttpReadLoopResult::TIMEOUT) @@ -264,46 +330,115 @@ class HttpRequestComponent : public Component { void set_follow_redirects(bool follow_redirects) { this->follow_redirects_ = follow_redirects; } void set_redirect_limit(uint16_t limit) { this->redirect_limit_ = limit; } - std::shared_ptr get(const std::string &url) { return this->start(url, "GET", "", {}); } - std::shared_ptr get(const std::string &url, const std::list
&request_headers) { + std::shared_ptr get(const std::string &url) { + return this->start(url, "GET", "", std::vector
{}); + } + std::shared_ptr get(const std::string &url, const std::vector
&request_headers) { return this->start(url, "GET", "", request_headers); } - std::shared_ptr get(const std::string &url, const std::list
&request_headers, - const std::set &collect_headers) { - return this->start(url, "GET", "", request_headers, collect_headers); + std::shared_ptr get(const std::string &url, const std::vector
&request_headers, + const std::vector &lower_case_collect_headers) { + return this->start(url, "GET", "", request_headers, lower_case_collect_headers); } std::shared_ptr post(const std::string &url, const std::string &body) { - return this->start(url, "POST", body, {}); + return this->start(url, "POST", body, std::vector
{}); } std::shared_ptr post(const std::string &url, const std::string &body, - const std::list
&request_headers) { + const std::vector
&request_headers) { return this->start(url, "POST", body, request_headers); } + std::shared_ptr post(const std::string &url, const std::string &body, + const std::vector
&request_headers, + const std::vector &lower_case_collect_headers) { + return this->start(url, "POST", body, request_headers, lower_case_collect_headers); + } + + // Remove before 2027.1.0 + ESPDEPRECATED("Pass request_headers as std::vector
instead of std::list. Removed in 2027.1.0.", "2026.7.0") + std::shared_ptr get(const std::string &url, const std::list
&request_headers) { + return this->get(url, std::vector
(request_headers.begin(), request_headers.end())); + } + // Remove before 2027.1.0 + ESPDEPRECATED("Pass request_headers as std::vector
instead of std::list. Removed in 2027.1.0.", "2026.7.0") + std::shared_ptr get(const std::string &url, const std::list
&request_headers, + const std::vector &collect_headers) { + return this->get(url, std::vector
(request_headers.begin(), request_headers.end()), collect_headers); + } + // Remove before 2027.1.0 + ESPDEPRECATED("Pass request_headers as std::vector
instead of std::list. Removed in 2027.1.0.", "2026.7.0") + std::shared_ptr post(const std::string &url, const std::string &body, + const std::list
&request_headers) { + return this->post(url, body, std::vector
(request_headers.begin(), request_headers.end())); + } + // Remove before 2027.1.0 + ESPDEPRECATED("Pass request_headers as std::vector
instead of std::list. Removed in 2027.1.0.", "2026.7.0") std::shared_ptr post(const std::string &url, const std::string &body, const std::list
&request_headers, - const std::set &collect_headers) { - return this->start(url, "POST", body, request_headers, collect_headers); + const std::vector &collect_headers) { + return this->post(url, body, std::vector
(request_headers.begin(), request_headers.end()), collect_headers); } std::shared_ptr start(const std::string &url, const std::string &method, const std::string &body, - const std::list
&request_headers) { - return this->start(url, method, body, request_headers, {}); + const std::vector
&request_headers) { + // Call perform() directly to avoid ambiguity with the deprecated overloads + return this->perform(url, method, body, request_headers, {}); } + // Remove before 2027.1.0 + ESPDEPRECATED("Pass request_headers as std::vector
instead of std::list. Removed in 2027.1.0.", "2026.7.0") + std::shared_ptr start(const std::string &url, const std::string &method, const std::string &body, + const std::list
&request_headers) { + return this->start(url, method, body, std::vector
(request_headers.begin(), request_headers.end())); + } + + // Remove before 2027.1.0 + ESPDEPRECATED("Pass collect_headers as std::vector instead of std::set. Removed in 2027.1.0.", + "2026.7.0") + std::shared_ptr start(const std::string &url, const std::string &method, const std::string &body, + const std::vector
&request_headers, + const std::set &collect_headers) { + std::vector lower; + lower.reserve(collect_headers.size()); + for (const auto &h : collect_headers) { + lower.push_back(str_lower_case(h)); + } + return this->perform(url, method, body, request_headers, lower); + } + + // Remove before 2027.1.0 + ESPDEPRECATED("Pass request_headers as std::vector
instead of std::list, and collect_headers as " + "std::vector instead of std::set. Removed in 2027.1.0.", + "2026.7.0") std::shared_ptr start(const std::string &url, const std::string &method, const std::string &body, const std::list
&request_headers, const std::set &collect_headers) { - std::set lower_case_collect_headers; - for (const std::string &collect_header : collect_headers) { - lower_case_collect_headers.insert(str_lower_case(collect_header)); + std::vector lower; + lower.reserve(collect_headers.size()); + for (const auto &h : collect_headers) { + lower.push_back(str_lower_case(h)); } + return this->perform(url, method, body, std::vector
(request_headers.begin(), request_headers.end()), lower); + } + + // Remove before 2027.1.0 + ESPDEPRECATED("Pass request_headers as std::vector
instead of std::list. Removed in 2027.1.0.", "2026.7.0") + std::shared_ptr start(const std::string &url, const std::string &method, const std::string &body, + const std::list
&request_headers, + const std::vector &lower_case_collect_headers) { + return this->perform(url, method, body, std::vector
(request_headers.begin(), request_headers.end()), + lower_case_collect_headers); + } + + std::shared_ptr start(const std::string &url, const std::string &method, const std::string &body, + const std::vector
&request_headers, + const std::vector &lower_case_collect_headers) { return this->perform(url, method, body, request_headers, lower_case_collect_headers); } protected: virtual std::shared_ptr perform(const std::string &url, const std::string &method, - const std::string &body, const std::list
&request_headers, - const std::set &collect_headers) = 0; + const std::string &body, const std::vector
&request_headers, + const std::vector &lower_case_collect_headers) = 0; const char *useragent_{nullptr}; bool follow_redirects_{}; uint16_t redirect_limit_{}; @@ -321,24 +456,26 @@ template class HttpRequestSendAction : public Action { TEMPLATABLE_VALUE(bool, capture_response) #endif + void init_request_headers(size_t count) { this->request_headers_.init(count); } void add_request_header(const char *key, TemplatableValue value) { - this->request_headers_.insert({key, value}); + this->request_headers_.push_back({key, value}); } - void add_collect_header(const char *value) { this->collect_headers_.insert(value); } + void add_collect_header(const char *value) { this->lower_case_collect_headers_.push_back(value); } - void add_json(const char *key, TemplatableValue value) { this->json_.insert({key, value}); } + void init_json(size_t count) { this->json_.init(count); } + void add_json(const char *key, TemplatableValue value) { this->json_.push_back({key, value}); } void set_json(std::function json_func) { this->json_func_ = json_func; } #ifdef USE_HTTP_REQUEST_RESPONSE - Trigger, std::string &, Ts...> *get_success_trigger_with_response() const { - return this->success_trigger_with_response_; + Trigger, std::string &, Ts...> *get_success_trigger_with_response() { + return &this->success_trigger_with_response_; } #endif - Trigger, Ts...> *get_success_trigger() const { return this->success_trigger_; } + Trigger, Ts...> *get_success_trigger() { return &this->success_trigger_; } - Trigger *get_error_trigger() const { return this->error_trigger_; } + Trigger *get_error_trigger() { return &this->error_trigger_; } void set_max_response_buffer_size(size_t max_response_buffer_size) { this->max_response_buffer_size_ = max_response_buffer_size; @@ -350,29 +487,24 @@ template class HttpRequestSendAction : public Action { body = this->body_.value(x...); } if (!this->json_.empty()) { - auto f = std::bind(&HttpRequestSendAction::encode_json_, this, x..., std::placeholders::_1); - body = json::build_json(f); + body = json::build_json([this, x...](JsonObject root) { this->encode_json_(x..., root); }); } if (this->json_func_ != nullptr) { - auto f = std::bind(&HttpRequestSendAction::encode_json_func_, this, x..., std::placeholders::_1); - body = json::build_json(f); + body = json::build_json([this, x...](JsonObject root) { this->json_func_(x..., root); }); } - std::list
request_headers; - for (const auto &item : this->request_headers_) { - auto val = item.second; - Header header; - header.name = item.first; - header.value = val.value(x...); - request_headers.push_back(header); + std::vector
request_headers; + request_headers.reserve(this->request_headers_.size()); + for (const auto &[key, val] : this->request_headers_) { + request_headers.push_back({key, val.value(x...)}); } auto container = this->parent_->start(this->url_.value(x...), this->method_.value(x...), body, request_headers, - this->collect_headers_); + this->lower_case_collect_headers_); auto captured_args = std::make_tuple(x...); if (container == nullptr) { - std::apply([this](Ts... captured_args_inner) { this->error_trigger_->trigger(captured_args_inner...); }, + std::apply([this](Ts... captured_args_inner) { this->error_trigger_.trigger(captured_args_inner...); }, captured_args); return; } @@ -393,11 +525,12 @@ template class HttpRequestSendAction : public Action { int read_or_error = container->read(buf + read_index, std::min(max_length - read_index, 512)); App.feed_wdt(); yield(); - auto result = http_read_loop_result(read_or_error, last_data_time, read_timeout); + auto result = + http_read_loop_result(read_or_error, last_data_time, read_timeout, container->is_read_complete()); if (result == HttpReadLoopResult::RETRY) continue; if (result != HttpReadLoopResult::DATA) - break; // ERROR or TIMEOUT + break; // COMPLETE, ERROR, or TIMEOUT read_index += read_or_error; } response_body.reserve(read_index); @@ -406,14 +539,14 @@ template class HttpRequestSendAction : public Action { } std::apply( [this, &container, &response_body](Ts... captured_args_inner) { - this->success_trigger_with_response_->trigger(container, response_body, captured_args_inner...); + this->success_trigger_with_response_.trigger(container, response_body, captured_args_inner...); }, captured_args); } else #endif { std::apply([this, &container]( - Ts... captured_args_inner) { this->success_trigger_->trigger(container, captured_args_inner...); }, + Ts... captured_args_inner) { this->success_trigger_.trigger(container, captured_args_inner...); }, captured_args); } container->end(); @@ -426,19 +559,16 @@ template class HttpRequestSendAction : public Action { root[item.first] = val.value(x...); } } - void encode_json_func_(Ts... x, JsonObject root) { this->json_func_(x..., root); } HttpRequestComponent *parent_; - std::map> request_headers_{}; - std::set collect_headers_{"content-type", "content-length"}; - std::map> json_{}; + FixedVector>> request_headers_{}; + std::vector lower_case_collect_headers_{"content-type", "content-length"}; + FixedVector>> json_{}; std::function json_func_{nullptr}; #ifdef USE_HTTP_REQUEST_RESPONSE - Trigger, std::string &, Ts...> *success_trigger_with_response_ = - new Trigger, std::string &, Ts...>(); + Trigger, std::string &, Ts...> success_trigger_with_response_; #endif - Trigger, Ts...> *success_trigger_ = - new Trigger, Ts...>(); - Trigger *error_trigger_ = new Trigger(); + Trigger, Ts...> success_trigger_; + Trigger error_trigger_; size_t max_response_buffer_size_{SIZE_MAX}; }; diff --git a/esphome/components/http_request/http_request_arduino.cpp b/esphome/components/http_request/http_request_arduino.cpp index 82538b2cb3..f0dd649285 100644 --- a/esphome/components/http_request/http_request_arduino.cpp +++ b/esphome/components/http_request/http_request_arduino.cpp @@ -9,14 +9,23 @@ #include "esphome/core/defines.h" #include "esphome/core/log.h" +// Include BearSSL error constants for TLS failure diagnostics +#ifdef USE_ESP8266 +#include +#endif + namespace esphome::http_request { static const char *const TAG = "http_request.arduino"; +#ifdef USE_ESP8266 +// ESP8266 Arduino core (WiFiClientSecureBearSSL.cpp) returns -1000 on OOM +static constexpr int ESP8266_SSL_ERR_OOM = -1000; +#endif std::shared_ptr HttpRequestArduino::perform(const std::string &url, const std::string &method, const std::string &body, - const std::list
&request_headers, - const std::set &collect_headers) { + const std::vector
&request_headers, + const std::vector &lower_case_collect_headers) { if (!network::is_connected()) { this->status_momentary_error("failed", 1000); ESP_LOGW(TAG, "HTTP Request failed; Not connected to network"); @@ -47,7 +56,7 @@ std::shared_ptr HttpRequestArduino::perform(const std::string &ur ESP_LOGV(TAG, "ESP8266 HTTPS connection with WiFiClientSecure"); stream_ptr = std::make_unique(); WiFiClientSecure *secure_client = static_cast(stream_ptr.get()); - secure_client->setBufferSizes(512, 512); + secure_client->setBufferSizes(this->tls_buffer_size_rx_, this->tls_buffer_size_tx_); secure_client->setInsecure(); } else { stream_ptr = std::make_unique(); @@ -96,9 +105,9 @@ std::shared_ptr HttpRequestArduino::perform(const std::string &ur } // returned needed headers must be collected before the requests - const char *header_keys[collect_headers.size()]; + const char *header_keys[lower_case_collect_headers.size()]; int index = 0; - for (auto const &header_name : collect_headers) { + for (auto const &header_name : lower_case_collect_headers) { header_keys[index++] = header_name.c_str(); } container->client_.collectHeaders(header_keys, index); @@ -107,37 +116,70 @@ std::shared_ptr HttpRequestArduino::perform(const std::string &ur container->status_code = container->client_.sendRequest(method.c_str(), body.c_str()); App.feed_wdt(); if (container->status_code < 0) { +#if defined(USE_ESP8266) && defined(USE_HTTP_REQUEST_ESP8266_HTTPS) + if (secure) { + WiFiClientSecure *secure_client = static_cast(stream_ptr.get()); + int last_error = secure_client->getLastSSLError(); + + if (last_error != 0) { + const LogString *error_msg; + switch (last_error) { + case ESP8266_SSL_ERR_OOM: + error_msg = LOG_STR("Unable to allocate buffer memory"); + break; + case BR_ERR_TOO_LARGE: + error_msg = LOG_STR("Incoming TLS record does not fit in receive buffer (BR_ERR_TOO_LARGE)"); + break; + default: + error_msg = LOG_STR("Unknown SSL error"); + break; + } + ESP_LOGW(TAG, "SSL failure: %s (Code: %d)", LOG_STR_ARG(error_msg), last_error); + if (last_error == ESP8266_SSL_ERR_OOM) { + ESP_LOGW(TAG, "Configured TLS buffer sizes: %u/%u bytes, check max free heap block using the debug component", + (unsigned int) this->tls_buffer_size_rx_, (unsigned int) this->tls_buffer_size_tx_); + } + } else { + ESP_LOGW(TAG, "Connection failure with no error code"); + } + } +#endif + ESP_LOGW(TAG, "HTTP Request failed; URL: %s; Error: %s", url.c_str(), HTTPClient::errorToString(container->status_code).c_str()); + this->status_momentary_error("failed", 1000); container->end(); return nullptr; } - if (!is_success(container->status_code)) { ESP_LOGE(TAG, "HTTP Request failed; URL: %s; Code: %d", url.c_str(), container->status_code); this->status_momentary_error("failed", 1000); // Still return the container, so it can be used to get the status code and error message } - container->response_headers_ = {}; + container->response_headers_.clear(); auto header_count = container->client_.headers(); for (int i = 0; i < header_count; i++) { const std::string header_name = str_lower_case(container->client_.headerName(i).c_str()); - if (collect_headers.count(header_name) > 0) { + if (should_collect_header(lower_case_collect_headers, header_name)) { std::string header_value = container->client_.header(i).c_str(); ESP_LOGD(TAG, "Received response header, name: %s, value: %s", header_name.c_str(), header_value.c_str()); - container->response_headers_[header_name].push_back(header_value); + container->response_headers_.push_back({header_name, header_value}); } } // HTTPClient::getSize() returns -1 for chunked transfer encoding (no Content-Length). // When cast to size_t, -1 becomes SIZE_MAX (4294967295 on 32-bit). - // The read() method handles this: bytes_read_ can never reach SIZE_MAX, so the - // early return check (bytes_read_ >= content_length) will never trigger. + // The read() method uses a chunked transfer encoding decoder (read_chunked_) to strip + // chunk framing and deliver only decoded content. When the final 0-size chunk is received, + // is_chunked_ is cleared and content_length is set to the actual decoded size, so + // is_read_complete() returns true and callers exit their read loops correctly. int content_length = container->client_.getSize(); ESP_LOGD(TAG, "Content-Length: %d", content_length); container->content_length = (size_t) content_length; + // -1 (SIZE_MAX when cast to size_t) means chunked transfer encoding + container->set_chunked(content_length == -1); container->duration_ms = millis() - start; return container; @@ -160,6 +202,10 @@ std::shared_ptr HttpRequestArduino::perform(const std::string &ur // > 0: bytes read // 0: no data yet, retry <-- NOTE: 0 means retry, NOT EOF! // < 0: error/connection closed <-- connection closed returns -1, not 0 +// +// For chunked transfer encoding, read_chunked_() decodes chunk framing and delivers +// only the payload data. When the final 0-size chunk is received, it clears is_chunked_ +// and sets content_length = bytes_read_ so is_read_complete() returns true. int HttpContainerArduino::read(uint8_t *buf, size_t max_len) { const uint32_t start = millis(); watchdog::WatchdogManager wdm(this->parent_->get_watchdog_timeout()); @@ -170,24 +216,42 @@ int HttpContainerArduino::read(uint8_t *buf, size_t max_len) { return HTTP_ERROR_CONNECTION_CLOSED; } + if (this->is_chunked_) { + int result = this->read_chunked_(buf, max_len, stream_ptr); + this->duration_ms += (millis() - start); + if (result > 0) { + return result; + } + // result <= 0: check for completion or errors + if (this->is_read_complete()) { + return 0; // Chunked transfer complete (final 0-size chunk received) + } + if (result < 0) { + return result; // Stream error during chunk decoding + } + // read_chunked_ returned 0: no data was available (available() was 0). + // This happens when the TCP buffer is empty - either more data is in flight, + // or the connection dropped. Arduino's connected() returns false only when + // both the remote has closed AND the receive buffer is empty, so any buffered + // data is fully drained before we report the drop. + if (!stream_ptr->connected()) { + return HTTP_ERROR_CONNECTION_CLOSED; + } + return 0; // No data yet, caller should retry + } + + // Non-chunked path int available_data = stream_ptr->available(); - // For chunked transfer encoding, HTTPClient::getSize() returns -1, which becomes SIZE_MAX when - // cast to size_t. SIZE_MAX - bytes_read_ is still huge, so it won't limit the read. size_t remaining = (this->content_length > 0) ? (this->content_length - this->bytes_read_) : max_len; int bufsize = std::min(max_len, std::min(remaining, (size_t) available_data)); if (bufsize == 0) { this->duration_ms += (millis() - start); - // Check if we've read all expected content (only valid when content_length is known and not SIZE_MAX) - // For chunked encoding (content_length == SIZE_MAX), we can't use this check - if (this->content_length > 0 && this->bytes_read_ >= this->content_length) { + if (this->is_read_complete()) { return 0; // All content read successfully } - // No data available - check if connection is still open - // For chunked encoding, !connected() after reading means EOF (all chunks received) - // For known content_length with bytes_read_ < content_length, it means connection dropped if (!stream_ptr->connected()) { - return HTTP_ERROR_CONNECTION_CLOSED; // Connection closed or EOF for chunked + return HTTP_ERROR_CONNECTION_CLOSED; } return 0; // No data yet, caller should retry } @@ -201,6 +265,143 @@ int HttpContainerArduino::read(uint8_t *buf, size_t max_len) { return read_len; } +void HttpContainerArduino::chunk_header_complete_() { + if (this->chunk_remaining_ == 0) { + this->chunk_state_ = ChunkedState::CHUNK_TRAILER; + this->chunk_remaining_ = 1; // repurpose as at-start-of-line flag + } else { + this->chunk_state_ = ChunkedState::CHUNK_DATA; + } +} + +// Chunked transfer encoding decoder +// +// On Arduino, getStreamPtr() returns raw TCP data. For chunked responses, this includes +// chunk framing (size headers, CRLF delimiters) mixed with payload data. This decoder +// strips the framing and delivers only decoded content to the caller. +// +// Chunk format (RFC 9112 Section 7.1): +// [;extension]\r\n +// \r\n +// ... +// 0\r\n +// [trailer-field\r\n]* +// \r\n +// +// Non-blocking: only processes bytes already in the TCP receive buffer. +// State (chunk_state_, chunk_remaining_) is preserved between calls, so partial +// chunk headers or split \r\n sequences resume correctly on the next call. +// Framing bytes (hex sizes, \r\n) may be consumed without producing output; +// the caller sees 0 and retries via the normal read timeout logic. +// +// WiFiClient::read() returns -1 on error despite available() > 0 (connection reset +// between check and read). On any stream error (c < 0 or readBytes <= 0), we return +// already-decoded data if any; otherwise HTTP_ERROR_CONNECTION_CLOSED. The error +// will surface again on the next call since the stream stays broken. +// +// Returns: > 0 decoded bytes, 0 no data available, < 0 error +int HttpContainerArduino::read_chunked_(uint8_t *buf, size_t max_len, WiFiClient *stream) { + int total_decoded = 0; + + while (total_decoded < (int) max_len && this->chunk_state_ != ChunkedState::COMPLETE) { + // Non-blocking: only process what's already buffered + if (stream->available() == 0) + break; + + // CHUNK_DATA reads multiple bytes; handle before the single-byte switch + if (this->chunk_state_ == ChunkedState::CHUNK_DATA) { + // Only read what's available, what fits in buf, and what remains in this chunk + size_t to_read = + std::min({max_len - (size_t) total_decoded, this->chunk_remaining_, (size_t) stream->available()}); + if (to_read == 0) + break; + App.feed_wdt(); + int read_len = stream->readBytes(buf + total_decoded, to_read); + if (read_len <= 0) + return total_decoded > 0 ? total_decoded : HTTP_ERROR_CONNECTION_CLOSED; + total_decoded += read_len; + this->chunk_remaining_ -= read_len; + this->bytes_read_ += read_len; + if (this->chunk_remaining_ == 0) + this->chunk_state_ = ChunkedState::CHUNK_DATA_TRAIL; + continue; + } + + // All other states consume a single byte + int c = stream->read(); + if (c < 0) + return total_decoded > 0 ? total_decoded : HTTP_ERROR_CONNECTION_CLOSED; + + switch (this->chunk_state_) { + // Parse hex chunk size, one byte at a time: "[;ext]\r\n" + // Note: if no hex digits are parsed (e.g., bare \r\n), chunk_remaining_ stays 0 + // and is treated as the final chunk. This is intentionally lenient — on embedded + // devices, rejecting malformed framing is less useful than terminating cleanly. + // Overflow of chunk_remaining_ from extremely long hex strings (>8 digits on + // 32-bit) is not checked; >4GB chunks are unrealistic on embedded targets and + // would simply cause fewer bytes to be read from that chunk. + case ChunkedState::CHUNK_HEADER: + if (c == '\n') { + // \n terminates the size line; chunk_remaining_ == 0 means last chunk + this->chunk_header_complete_(); + } else { + uint8_t hex = parse_hex_char(c); + if (hex != INVALID_HEX_CHAR) { + this->chunk_remaining_ = (this->chunk_remaining_ << 4) | hex; + } else if (c != '\r') { + this->chunk_state_ = ChunkedState::CHUNK_HEADER_EXT; // ';' starts extension, skip to \n + } + } + break; + + // Skip chunk extension bytes until \n (e.g., ";name=value\r\n") + case ChunkedState::CHUNK_HEADER_EXT: + if (c == '\n') { + this->chunk_header_complete_(); + } + break; + + // Consume \r\n trailing each chunk's data + case ChunkedState::CHUNK_DATA_TRAIL: + if (c == '\n') { + this->chunk_state_ = ChunkedState::CHUNK_HEADER; + this->chunk_remaining_ = 0; // reset for next chunk's hex accumulation + } + // else: \r is consumed silently, next iteration gets \n + break; + + // Consume optional trailer headers and terminating empty line after final chunk. + // Per RFC 9112 Section 7.1: "0\r\n" is followed by optional "field\r\n" lines + // and a final "\r\n". chunk_remaining_ is repurposed as a flag: 1 = at start + // of line (may be the empty terminator), 0 = mid-line (reading a trailer field). + case ChunkedState::CHUNK_TRAILER: + if (c == '\n') { + if (this->chunk_remaining_ != 0) { + this->chunk_state_ = ChunkedState::COMPLETE; // Empty line terminates trailers + } else { + this->chunk_remaining_ = 1; // End of trailer field, at start of next line + } + } else if (c != '\r') { + this->chunk_remaining_ = 0; // Non-CRLF char: reading a trailer field + } + // \r doesn't change the flag — it's part of \r\n line endings + break; + + default: + break; + } + + if (this->chunk_state_ == ChunkedState::COMPLETE) { + // Clear chunked flag and set content_length to actual decoded size so + // is_read_complete() returns true and callers exit their read loops + this->is_chunked_ = false; + this->content_length = this->bytes_read_; + } + } + + return total_decoded; +} + void HttpContainerArduino::end() { watchdog::WatchdogManager wdm(this->parent_->get_watchdog_timeout()); this->client_.end(); diff --git a/esphome/components/http_request/http_request_arduino.h b/esphome/components/http_request/http_request_arduino.h index d9b5af9d81..b009d45b1c 100644 --- a/esphome/components/http_request/http_request_arduino.h +++ b/esphome/components/http_request/http_request_arduino.h @@ -18,6 +18,17 @@ namespace esphome::http_request { class HttpRequestArduino; + +/// State machine for decoding chunked transfer encoding on Arduino +enum class ChunkedState : uint8_t { + CHUNK_HEADER, ///< Reading hex digits of chunk size + CHUNK_HEADER_EXT, ///< Skipping chunk extensions until \n + CHUNK_DATA, ///< Reading chunk data bytes + CHUNK_DATA_TRAIL, ///< Skipping \r\n after chunk data + CHUNK_TRAILER, ///< Consuming trailer headers after final 0-size chunk + COMPLETE, ///< Finished: final chunk and trailers consumed +}; + class HttpContainerArduino : public HttpContainer { public: int read(uint8_t *buf, size_t max_len) override; @@ -26,13 +37,30 @@ class HttpContainerArduino : public HttpContainer { protected: friend class HttpRequestArduino; HTTPClient client_{}; + + /// Decode chunked transfer encoding from the raw stream + int read_chunked_(uint8_t *buf, size_t max_len, WiFiClient *stream); + /// Transition from chunk header to data or trailer based on parsed size + void chunk_header_complete_(); + ChunkedState chunk_state_{ChunkedState::CHUNK_HEADER}; + size_t chunk_remaining_{0}; ///< Bytes remaining in current chunk }; class HttpRequestArduino : public HttpRequestComponent { + public: +#ifdef USE_ESP8266 + void set_tls_buffer_size_rx(uint16_t size) { this->tls_buffer_size_rx_ = size; } + void set_tls_buffer_size_tx(uint16_t size) { this->tls_buffer_size_tx_ = size; } +#endif + protected: std::shared_ptr perform(const std::string &url, const std::string &method, const std::string &body, - const std::list
&request_headers, - const std::set &collect_headers) override; + const std::vector
&request_headers, + const std::vector &lower_case_collect_headers) override; +#ifdef USE_ESP8266 + uint16_t tls_buffer_size_rx_{512}; + uint16_t tls_buffer_size_tx_{512}; +#endif }; } // namespace esphome::http_request diff --git a/esphome/components/http_request/http_request_host.cpp b/esphome/components/http_request/http_request_host.cpp index b94570be12..60ab4d68a0 100644 --- a/esphome/components/http_request/http_request_host.cpp +++ b/esphome/components/http_request/http_request_host.cpp @@ -18,8 +18,8 @@ static const char *const TAG = "http_request.host"; std::shared_ptr HttpRequestHost::perform(const std::string &url, const std::string &method, const std::string &body, - const std::list
&request_headers, - const std::set &response_headers) { + const std::vector
&request_headers, + const std::vector &lower_case_collect_headers) { if (!network::is_connected()) { this->status_momentary_error("failed", 1000); ESP_LOGW(TAG, "HTTP Request failed; Not connected to network"); @@ -116,8 +116,8 @@ std::shared_ptr HttpRequestHost::perform(const std::string &url, for (auto header : response.headers) { ESP_LOGD(TAG, "Header: %s: %s", header.first.c_str(), header.second.c_str()); auto lower_name = str_lower_case(header.first); - if (response_headers.find(lower_name) != response_headers.end()) { - container->response_headers_[lower_name].emplace_back(header.second); + if (should_collect_header(lower_case_collect_headers, lower_name)) { + container->response_headers_.push_back({lower_name, header.second}); } } container->duration_ms = millis() - start; diff --git a/esphome/components/http_request/http_request_host.h b/esphome/components/http_request/http_request_host.h index 32e149e6a3..52be0e8a16 100644 --- a/esphome/components/http_request/http_request_host.h +++ b/esphome/components/http_request/http_request_host.h @@ -19,8 +19,8 @@ class HttpContainerHost : public HttpContainer { class HttpRequestHost : public HttpRequestComponent { public: std::shared_ptr perform(const std::string &url, const std::string &method, const std::string &body, - const std::list
&request_headers, - const std::set &response_headers) override; + const std::vector
&request_headers, + const std::vector &lower_case_collect_headers) override; void set_ca_path(const char *ca_path) { this->ca_path_ = ca_path; } protected: diff --git a/esphome/components/http_request/http_request_idf.cpp b/esphome/components/http_request/http_request_idf.cpp index 2b4dee953a..30f53eecdc 100644 --- a/esphome/components/http_request/http_request_idf.cpp +++ b/esphome/components/http_request/http_request_idf.cpp @@ -17,10 +17,11 @@ namespace esphome::http_request { static const char *const TAG = "http_request.idf"; +static constexpr uint32_t ERROR_DURATION_MS = 1000; struct UserData { - const std::set &collect_headers; - std::map> response_headers; + const std::vector &lower_case_collect_headers; + std::vector
&response_headers; }; void HttpRequestIDF::dump_config() { @@ -38,10 +39,10 @@ esp_err_t HttpRequestIDF::http_event_handler(esp_http_client_event_t *evt) { switch (evt->event_id) { case HTTP_EVENT_ON_HEADER: { const std::string header_name = str_lower_case(evt->header_key); - if (user_data->collect_headers.count(header_name)) { + if (should_collect_header(user_data->lower_case_collect_headers, header_name)) { const std::string header_value = evt->header_value; ESP_LOGD(TAG, "Received response header, name: %s, value: %s", header_name.c_str(), header_value.c_str()); - user_data->response_headers[header_name].push_back(header_value); + user_data->response_headers.push_back({header_name, header_value}); } break; } @@ -54,10 +55,10 @@ esp_err_t HttpRequestIDF::http_event_handler(esp_http_client_event_t *evt) { std::shared_ptr HttpRequestIDF::perform(const std::string &url, const std::string &method, const std::string &body, - const std::list
&request_headers, - const std::set &collect_headers) { + const std::vector
&request_headers, + const std::vector &lower_case_collect_headers) { if (!network::is_connected()) { - this->status_momentary_error("failed", 1000); + this->status_momentary_error("failed", ERROR_DURATION_MS); ESP_LOGE(TAG, "HTTP Request failed; Not connected to network"); return nullptr; } @@ -74,7 +75,7 @@ std::shared_ptr HttpRequestIDF::perform(const std::string &url, c } else if (method == "PATCH") { method_idf = HTTP_METHOD_PATCH; } else { - this->status_momentary_error("failed", 1000); + this->status_momentary_error("failed", ERROR_DURATION_MS); ESP_LOGE(TAG, "HTTP Request failed; Unsupported method"); return nullptr; } @@ -110,16 +111,22 @@ std::shared_ptr HttpRequestIDF::perform(const std::string &url, c watchdog::WatchdogManager wdm(this->get_watchdog_timeout()); config.event_handler = http_event_handler; - auto user_data = UserData{collect_headers, {}}; - config.user_data = static_cast(&user_data); esp_http_client_handle_t client = esp_http_client_init(&config); + if (client == nullptr) { + this->status_momentary_error("failed", ERROR_DURATION_MS); + ESP_LOGE(TAG, "HTTP Request failed; client could not be initialized"); + return nullptr; + } std::shared_ptr container = std::make_shared(client); container->set_parent(this); container->set_secure(secure); + auto user_data = UserData{lower_case_collect_headers, container->response_headers_}; + esp_http_client_set_user_data(client, static_cast(&user_data)); + for (const auto &header : request_headers) { esp_http_client_set_header(client, header.name.c_str(), header.value.c_str()); } @@ -128,7 +135,7 @@ std::shared_ptr HttpRequestIDF::perform(const std::string &url, c esp_err_t err = esp_http_client_open(client, body_len); if (err != ESP_OK) { - this->status_momentary_error("failed", 1000); + this->status_momentary_error("failed", ERROR_DURATION_MS); ESP_LOGE(TAG, "HTTP Request failed: %s", esp_err_to_name(err)); esp_http_client_cleanup(client); return nullptr; @@ -150,7 +157,7 @@ std::shared_ptr HttpRequestIDF::perform(const std::string &url, c } if (err != ESP_OK) { - this->status_momentary_error("failed", 1000); + this->status_momentary_error("failed", ERROR_DURATION_MS); ESP_LOGE(TAG, "HTTP Request failed: %s", esp_err_to_name(err)); esp_http_client_cleanup(client); return nullptr; @@ -160,10 +167,10 @@ std::shared_ptr HttpRequestIDF::perform(const std::string &url, c // esp_http_client_fetch_headers() returns 0 for chunked transfer encoding (no Content-Length header). // The read() method handles content_length == 0 specially to support chunked responses. container->content_length = esp_http_client_fetch_headers(client); + container->set_chunked(esp_http_client_is_chunked_response(client)); container->feed_wdt(); container->status_code = esp_http_client_get_status_code(client); container->feed_wdt(); - container->set_response_headers(user_data.response_headers); container->duration_ms = millis() - start; if (is_success(container->status_code)) { return container; @@ -175,7 +182,7 @@ std::shared_ptr HttpRequestIDF::perform(const std::string &url, c err = esp_http_client_set_redirection(client); if (err != ESP_OK) { ESP_LOGE(TAG, "esp_http_client_set_redirection failed: %s", esp_err_to_name(err)); - this->status_momentary_error("failed", 1000); + this->status_momentary_error("failed", ERROR_DURATION_MS); esp_http_client_cleanup(client); return nullptr; } @@ -188,13 +195,14 @@ std::shared_ptr HttpRequestIDF::perform(const std::string &url, c err = esp_http_client_open(client, 0); if (err != ESP_OK) { ESP_LOGE(TAG, "esp_http_client_open failed: %s", esp_err_to_name(err)); - this->status_momentary_error("failed", 1000); + this->status_momentary_error("failed", ERROR_DURATION_MS); esp_http_client_cleanup(client); return nullptr; } container->feed_wdt(); container->content_length = esp_http_client_fetch_headers(client); + container->set_chunked(esp_http_client_is_chunked_response(client)); container->feed_wdt(); container->status_code = esp_http_client_get_status_code(client); container->feed_wdt(); @@ -212,37 +220,54 @@ std::shared_ptr HttpRequestIDF::perform(const std::string &url, c } ESP_LOGE(TAG, "HTTP Request failed; URL: %s; Code: %d", url.c_str(), container->status_code); - this->status_momentary_error("failed", 1000); + this->status_momentary_error("failed", ERROR_DURATION_MS); return container; } +bool HttpContainerIDF::is_read_complete() const { + // Base class handles no-body status codes and non-chunked content_length completion + if (HttpContainer::is_read_complete()) { + return true; + } + // For chunked responses, use the authoritative ESP-IDF completion check + return this->is_chunked_ && esp_http_client_is_complete_data_received(this->client_); +} + // ESP-IDF HTTP read implementation (blocking mode) // // WARNING: Return values differ from BSD sockets! See http_request.h for full documentation. // // esp_http_client_read() in blocking mode returns: // > 0: bytes read -// 0: connection closed (end of stream) +// 0: all chunked data received (is_chunk_complete true) or connection closed +// -ESP_ERR_HTTP_EAGAIN: transport timeout, no data available yet // < 0: error // // We normalize to HttpContainer::read() contract: // > 0: bytes read -// 0: all content read (only returned when content_length is known and fully read) +// 0: all content read (for both content_length-based and chunked completion) // < 0: error/connection closed // // Note on chunked transfer encoding: // esp_http_client_fetch_headers() returns 0 for chunked responses (no Content-Length header). -// We handle this by skipping the content_length check when content_length is 0, -// allowing esp_http_client_read() to handle chunked decoding internally and signal EOF -// by returning 0. +// When esp_http_client_read() returns 0 for a chunked response, is_read_complete() calls +// esp_http_client_is_complete_data_received() to distinguish successful completion from +// connection errors. Callers use http_read_loop_result() which checks is_read_complete() +// to return COMPLETE for successful chunked EOF. +// +// Streaming chunked responses are not supported (see http_request.h for details). +// When data stops arriving, esp_http_client_read() returns -ESP_ERR_HTTP_EAGAIN +// after its internal transport timeout (configured via timeout_ms) expires. +// This is passed through as a negative return value, which callers treat as an error. int HttpContainerIDF::read(uint8_t *buf, size_t max_len) { const uint32_t start = millis(); watchdog::WatchdogManager wdm(this->parent_->get_watchdog_timeout()); - // Check if we've already read all expected content - // Skip this check when content_length is 0 (chunked transfer encoding or unknown length) - // For chunked responses, esp_http_client_read() will return 0 when all data is received - if (this->content_length > 0 && this->bytes_read_ >= this->content_length) { + // Check if we've already read all expected content (non-chunked and no-body only). + // Use the base class check here, NOT the override: esp_http_client_is_complete_data_received() + // returns true as soon as all data arrives from the network, but data may still be in + // the client's internal buffer waiting to be consumed by esp_http_client_read(). + if (HttpContainer::is_read_complete()) { return 0; // All content read successfully } @@ -257,15 +282,18 @@ int HttpContainerIDF::read(uint8_t *buf, size_t max_len) { return read_len_or_error; } - // esp_http_client_read() returns 0 in two cases: - // 1. Known content_length: connection closed before all data received (error) - // 2. Chunked encoding (content_length == 0): end of stream reached (EOF) - // For case 1, returning HTTP_ERROR_CONNECTION_CLOSED is correct. - // For case 2, 0 indicates that all chunked data has already been delivered - // in previous successful read() calls, so treating this as a closed - // connection does not cause any loss of response data. + // esp_http_client_read() returns 0 when: + // - Known content_length: connection closed before all data received (error) + // - Chunked encoding: all chunks received (is_chunk_complete true, genuine EOF) + // + // Return 0 in both cases. Callers use http_read_loop_result() which calls + // is_read_complete() to distinguish these: + // - Chunked complete: is_read_complete() returns true (via + // esp_http_client_is_complete_data_received()), caller gets COMPLETE + // - Non-chunked incomplete: is_read_complete() returns false, caller + // eventually gets TIMEOUT (since no more data arrives) if (read_len_or_error == 0) { - return HTTP_ERROR_CONNECTION_CLOSED; + return 0; } // Negative value - error, return the actual error code for debugging diff --git a/esphome/components/http_request/http_request_idf.h b/esphome/components/http_request/http_request_idf.h index ad11811a8f..9ed1a97b1a 100644 --- a/esphome/components/http_request/http_request_idf.h +++ b/esphome/components/http_request/http_request_idf.h @@ -16,15 +16,13 @@ class HttpContainerIDF : public HttpContainer { HttpContainerIDF(esp_http_client_handle_t client) : client_(client) {} int read(uint8_t *buf, size_t max_len) override; void end() override; + bool is_read_complete() const override; /// @brief Feeds the watchdog timer if the executing task has one attached void feed_wdt(); - void set_response_headers(std::map> &response_headers) { - this->response_headers_ = std::move(response_headers); - } - protected: + friend class HttpRequestIDF; esp_http_client_handle_t client_; }; @@ -39,8 +37,8 @@ class HttpRequestIDF : public HttpRequestComponent { protected: std::shared_ptr perform(const std::string &url, const std::string &method, const std::string &body, - const std::list
&request_headers, - const std::set &collect_headers) override; + const std::vector
&request_headers, + const std::vector &lower_case_collect_headers) override; // if zero ESP-IDF will use DEFAULT_HTTP_BUF_SIZE uint16_t buffer_size_rx_{}; uint16_t buffer_size_tx_{}; diff --git a/esphome/components/http_request/ota/__init__.py b/esphome/components/http_request/ota/__init__.py index d2c574d8c6..fb59e51943 100644 --- a/esphome/components/http_request/ota/__init__.py +++ b/esphome/components/http_request/ota/__init__.py @@ -70,6 +70,7 @@ OTA_HTTP_REQUEST_FLASH_ACTION_SCHEMA = cv.All( "ota.http_request.flash", OtaHttpRequestComponentFlashAction, OTA_HTTP_REQUEST_FLASH_ACTION_SCHEMA, + synchronous=True, ) async def ota_http_request_action_to_code(config, action_id, template_arg, args): paren = await cg.get_variable(config[CONF_ID]) diff --git a/esphome/components/http_request/ota/ota_http_request.cpp b/esphome/components/http_request/ota/ota_http_request.cpp index 8a4b3684cf..5dd21c314c 100644 --- a/esphome/components/http_request/ota/ota_http_request.cpp +++ b/esphome/components/http_request/ota/ota_http_request.cpp @@ -1,15 +1,13 @@ #include "ota_http_request.h" +#include + #include "esphome/core/application.h" #include "esphome/core/defines.h" #include "esphome/core/log.h" #include "esphome/components/md5/md5.h" #include "esphome/components/watchdog/watchdog.h" -#include "esphome/components/ota/ota_backend.h" -#include "esphome/components/ota/ota_backend_esp8266.h" -#include "esphome/components/ota/ota_backend_arduino_rp2040.h" -#include "esphome/components/ota/ota_backend_esp_idf.h" namespace esphome { namespace http_request { @@ -67,8 +65,7 @@ void OtaHttpRequestComponent::flash() { } } -void OtaHttpRequestComponent::cleanup_(std::unique_ptr backend, - const std::shared_ptr &container) { +void OtaHttpRequestComponent::cleanup_(ota::OTABackendPtr backend, const std::shared_ptr &container) { if (this->update_started_) { ESP_LOGV(TAG, "Aborting OTA backend"); backend->abort(); @@ -105,8 +102,7 @@ uint8_t OtaHttpRequestComponent::do_ota_() { // we will compute MD5 on the fly for verification -- Arduino OTA seems to ignore it md5_receive.init(); - ESP_LOGV(TAG, "MD5Digest initialized\n" - "OTA backend begin"); + ESP_LOGV(TAG, "MD5Digest initialized, OTA backend begin"); auto backend = ota::make_ota_backend(); auto error_code = backend->begin(container->content_length); if (error_code != ota::OTA_RESPONSE_OK) { @@ -130,9 +126,15 @@ uint8_t OtaHttpRequestComponent::do_ota_() { App.feed_wdt(); yield(); - auto result = http_read_loop_result(bufsize_or_error, last_data_time, read_timeout); + auto result = http_read_loop_result(bufsize_or_error, last_data_time, read_timeout, container->is_read_complete()); if (result == HttpReadLoopResult::RETRY) continue; + // For non-chunked responses, COMPLETE is unreachable (loop condition checks bytes_read < content_length). + // For chunked responses, the decoder sets content_length = bytes_read when the final chunk arrives, + // which causes the loop condition to terminate. But COMPLETE can still be returned if the decoder + // finishes mid-read, so this is needed for correctness. + if (result == HttpReadLoopResult::COMPLETE) + break; if (result != HttpReadLoopResult::DATA) { if (result == HttpReadLoopResult::TIMEOUT) { ESP_LOGE(TAG, "Timeout reading data"); @@ -204,6 +206,26 @@ uint8_t OtaHttpRequestComponent::do_ota_() { return ota::OTA_RESPONSE_OK; } +// URL-encode characters that are not unreserved per RFC 3986 section 2.3. +// This is needed for embedding userinfo (username/password) in URLs safely. +static std::string url_encode(const std::string &str) { + std::string result; + result.reserve(str.size()); + for (char c : str) { + if (std::isalnum(static_cast(c)) || c == '-' || c == '_' || c == '.' || c == '~') { + result += c; + } else { + result += '%'; + result += format_hex_pretty_char((static_cast(c) >> 4) & 0x0F); + result += format_hex_pretty_char(static_cast(c) & 0x0F); + } + } + return result; +} + +void OtaHttpRequestComponent::set_password(const std::string &password) { this->password_ = url_encode(password); } +void OtaHttpRequestComponent::set_username(const std::string &username) { this->username_ = url_encode(username); } + std::string OtaHttpRequestComponent::get_url_with_auth_(const std::string &url) { if (this->username_.empty() || this->password_.empty()) { return url; diff --git a/esphome/components/http_request/ota/ota_http_request.h b/esphome/components/http_request/ota/ota_http_request.h index 8735189e99..70e4559fa7 100644 --- a/esphome/components/http_request/ota/ota_http_request.h +++ b/esphome/components/http_request/ota/ota_http_request.h @@ -1,6 +1,6 @@ #pragma once -#include "esphome/components/ota/ota_backend.h" +#include "esphome/components/ota/ota_backend_factory.h" #include "esphome/core/component.h" #include "esphome/core/defines.h" #include "esphome/core/helpers.h" @@ -22,16 +22,16 @@ enum OtaHttpRequestError : uint8_t { OTA_CONNECTION_ERROR = 0x12, }; -class OtaHttpRequestComponent : public ota::OTAComponent, public Parented { +class OtaHttpRequestComponent final : public ota::OTAComponent, public Parented { public: void dump_config() override; float get_setup_priority() const override { return setup_priority::AFTER_WIFI; } void set_md5_url(const std::string &md5_url); void set_md5(const std::string &md5) { this->md5_expected_ = md5; } - void set_password(const std::string &password) { this->password_ = password; } + void set_password(const std::string &password); void set_url(const std::string &url); - void set_username(const std::string &username) { this->username_ = username; } + void set_username(const std::string &username); std::string md5_computed() { return this->md5_computed_; } std::string md5_expected() { return this->md5_expected_; } @@ -39,7 +39,7 @@ class OtaHttpRequestComponent : public ota::OTAComponent, public Parented backend, const std::shared_ptr &container); + void cleanup_(ota::OTABackendPtr backend, const std::shared_ptr &container); uint8_t do_ota_(); std::string get_url_with_auth_(const std::string &url); bool http_get_md5_(); diff --git a/esphome/components/http_request/update/http_request_update.cpp b/esphome/components/http_request/update/http_request_update.cpp index c63e55d159..1c52a28105 100644 --- a/esphome/components/http_request/update/http_request_update.cpp +++ b/esphome/components/http_request/update/http_request_update.cpp @@ -23,9 +23,36 @@ namespace http_request { static const char *const TAG = "http_request.update"; -static const size_t MAX_READ_SIZE = 256; +// Wraps UpdateInfo + error for the task→main-loop handoff. +struct TaskResult { + update::UpdateInfo info; + const LogString *error_str{nullptr}; +}; -void HttpRequestUpdate::setup() { this->ota_parent_->add_state_listener(this); } +static const size_t MAX_READ_SIZE = 256; +static constexpr uint32_t INITIAL_CHECK_INTERVAL_ID = 0; +static constexpr uint32_t INITIAL_CHECK_INTERVAL_MS = 10000; +static constexpr uint8_t INITIAL_CHECK_MAX_ATTEMPTS = 6; + +void HttpRequestUpdate::setup() { + this->ota_parent_->add_state_listener(this); + + // Check periodically until network is ready + // Only if update interval is > total retry window to avoid redundant checks + if (this->get_update_interval() != SCHEDULER_DONT_RUN && + this->get_update_interval() > INITIAL_CHECK_INTERVAL_MS * INITIAL_CHECK_MAX_ATTEMPTS) { + this->initial_check_remaining_ = INITIAL_CHECK_MAX_ATTEMPTS; + this->set_interval(INITIAL_CHECK_INTERVAL_ID, INITIAL_CHECK_INTERVAL_MS, [this]() { + bool connected = network::is_connected(); + if (--this->initial_check_remaining_ == 0 || connected) { + this->cancel_interval(INITIAL_CHECK_INTERVAL_ID); + if (connected) { + this->update(); + } + } + }); + } +} void HttpRequestUpdate::on_ota_state(ota::OTAState state, float progress, uint8_t error) { if (state == ota::OTAState::OTA_IN_PROGRESS) { @@ -45,7 +72,12 @@ void HttpRequestUpdate::update() { ESP_LOGD(TAG, "Network not connected, skipping update check"); return; } + this->cancel_interval(INITIAL_CHECK_INTERVAL_ID); #ifdef USE_ESP32 + if (this->update_task_handle_ != nullptr) { + ESP_LOGW(TAG, "Update check already in progress"); + return; + } xTaskCreate(HttpRequestUpdate::update_task, "update_task", 8192, (void *) this, 1, &this->update_task_handle_); #else this->update_task(this); @@ -55,139 +87,151 @@ void HttpRequestUpdate::update() { void HttpRequestUpdate::update_task(void *params) { HttpRequestUpdate *this_update = (HttpRequestUpdate *) params; + // Allocate once — every path below returns via the single defer at the end. + // On failure, error_str is set; on success it is nullptr. + auto *result = new TaskResult(); + auto *info = &result->info; + auto container = this_update->request_parent_->get(this_update->source_url_); if (container == nullptr || container->status_code != HTTP_STATUS_OK) { ESP_LOGE(TAG, "Failed to fetch manifest from %s", this_update->source_url_.c_str()); - // Defer to main loop to avoid race condition on component_state_ read-modify-write - this_update->defer([this_update]() { this_update->status_set_error(LOG_STR("Failed to fetch manifest")); }); - UPDATE_RETURN; + if (container != nullptr) + container->end(); + result->error_str = LOG_STR("Failed to fetch manifest"); + goto defer; // NOLINT(cppcoreguidelines-avoid-goto) } - RAMAllocator allocator; - uint8_t *data = allocator.allocate(container->content_length); - if (data == nullptr) { - ESP_LOGE(TAG, "Failed to allocate %zu bytes for manifest", container->content_length); - // Defer to main loop to avoid race condition on component_state_ read-modify-write - this_update->defer( - [this_update]() { this_update->status_set_error(LOG_STR("Failed to allocate memory for manifest")); }); - container->end(); - UPDATE_RETURN; - } - - auto read_result = http_read_fully(container.get(), data, container->content_length, MAX_READ_SIZE, - this_update->request_parent_->get_timeout()); - if (read_result.status != HttpReadStatus::OK) { - if (read_result.status == HttpReadStatus::TIMEOUT) { - ESP_LOGE(TAG, "Timeout reading manifest"); - } else { - ESP_LOGE(TAG, "Error reading manifest: %d", read_result.error_code); + { + RAMAllocator allocator; + uint8_t *data = allocator.allocate(container->content_length); + if (data == nullptr) { + ESP_LOGE(TAG, "Failed to allocate %zu bytes for manifest", container->content_length); + container->end(); + result->error_str = LOG_STR("Failed to allocate memory for manifest"); + goto defer; // NOLINT(cppcoreguidelines-avoid-goto) } - // Defer to main loop to avoid race condition on component_state_ read-modify-write - this_update->defer([this_update]() { this_update->status_set_error(LOG_STR("Failed to read manifest")); }); - allocator.deallocate(data, container->content_length); - container->end(); - UPDATE_RETURN; - } - size_t read_index = container->get_bytes_read(); - bool valid = false; - { // Ensures the response string falls out of scope and deallocates before the task ends - std::string response((char *) data, read_index); - allocator.deallocate(data, container->content_length); + auto read_result = http_read_fully(container.get(), data, container->content_length, MAX_READ_SIZE, + this_update->request_parent_->get_timeout()); + if (read_result.status != HttpReadStatus::OK) { + if (read_result.status == HttpReadStatus::TIMEOUT) { + ESP_LOGE(TAG, "Timeout reading manifest"); + } else { + ESP_LOGE(TAG, "Error reading manifest: %d", read_result.error_code); + } + allocator.deallocate(data, container->content_length); + container->end(); + result->error_str = LOG_STR("Failed to read manifest"); + goto defer; // NOLINT(cppcoreguidelines-avoid-goto) + } + size_t read_index = container->get_bytes_read(); + size_t content_length = container->content_length; container->end(); container.reset(); // Release ownership of the container's shared_ptr - valid = json::parse_json(response, [this_update](JsonObject root) -> bool { - if (!root[ESPHOME_F("name")].is() || !root[ESPHOME_F("version")].is() || - !root[ESPHOME_F("builds")].is()) { - ESP_LOGE(TAG, "Manifest does not contain required fields"); - return false; - } - this_update->update_info_.title = root[ESPHOME_F("name")].as(); - this_update->update_info_.latest_version = root[ESPHOME_F("version")].as(); - - for (auto build : root[ESPHOME_F("builds")].as()) { - if (!build[ESPHOME_F("chipFamily")].is()) { + bool valid = false; + { // Scope to ensure JsonDocument is destroyed before deallocating buffer + valid = json::parse_json(data, read_index, [info](JsonObject root) -> bool { + if (!root[ESPHOME_F("name")].is() || !root[ESPHOME_F("version")].is() || + !root[ESPHOME_F("builds")].is()) { ESP_LOGE(TAG, "Manifest does not contain required fields"); return false; } - if (build[ESPHOME_F("chipFamily")] == ESPHOME_VARIANT) { - if (!build[ESPHOME_F("ota")].is()) { + info->title = root[ESPHOME_F("name")].as(); + info->latest_version = root[ESPHOME_F("version")].as(); + + auto builds_array = root[ESPHOME_F("builds")].as(); + for (auto build : builds_array) { + if (!build[ESPHOME_F("chipFamily")].is()) { ESP_LOGE(TAG, "Manifest does not contain required fields"); return false; } - JsonObject ota = build[ESPHOME_F("ota")].as(); - if (!ota[ESPHOME_F("path")].is() || !ota[ESPHOME_F("md5")].is()) { - ESP_LOGE(TAG, "Manifest does not contain required fields"); - return false; + if (build[ESPHOME_F("chipFamily")] == ESPHOME_VARIANT) { + if (!build[ESPHOME_F("ota")].is()) { + ESP_LOGE(TAG, "Manifest does not contain required fields"); + return false; + } + JsonObject ota = build[ESPHOME_F("ota")].as(); + if (!ota[ESPHOME_F("path")].is() || !ota[ESPHOME_F("md5")].is()) { + ESP_LOGE(TAG, "Manifest does not contain required fields"); + return false; + } + info->firmware_url = ota[ESPHOME_F("path")].as(); + info->md5 = ota[ESPHOME_F("md5")].as(); + + if (ota[ESPHOME_F("summary")].is()) + info->summary = ota[ESPHOME_F("summary")].as(); + if (ota[ESPHOME_F("release_url")].is()) + info->release_url = ota[ESPHOME_F("release_url")].as(); + + return true; } - this_update->update_info_.firmware_url = ota[ESPHOME_F("path")].as(); - this_update->update_info_.md5 = ota[ESPHOME_F("md5")].as(); - - if (ota[ESPHOME_F("summary")].is()) - this_update->update_info_.summary = ota[ESPHOME_F("summary")].as(); - if (ota[ESPHOME_F("release_url")].is()) - this_update->update_info_.release_url = ota[ESPHOME_F("release_url")].as(); - - return true; } + return false; + }); + } + allocator.deallocate(data, content_length); + + if (!valid) { + ESP_LOGE(TAG, "Failed to parse JSON from %s", this_update->source_url_.c_str()); + result->error_str = LOG_STR("Failed to parse manifest JSON"); + goto defer; // NOLINT(cppcoreguidelines-avoid-goto) + } + + // Merge source_url_ and firmware_url + if (!info->firmware_url.empty() && info->firmware_url.find("http") == std::string::npos) { + std::string path = info->firmware_url; + if (path[0] == '/') { + std::string domain = this_update->source_url_.substr(0, this_update->source_url_.find('/', 8)); + info->firmware_url = domain + path; + } else { + std::string domain = this_update->source_url_.substr(0, this_update->source_url_.rfind('/') + 1); + info->firmware_url = domain + path; } - return false; - }); - } - - if (!valid) { - ESP_LOGE(TAG, "Failed to parse JSON from %s", this_update->source_url_.c_str()); - // Defer to main loop to avoid race condition on component_state_ read-modify-write - this_update->defer([this_update]() { this_update->status_set_error(LOG_STR("Failed to parse manifest JSON")); }); - UPDATE_RETURN; - } - - // Merge source_url_ and this_update->update_info_.firmware_url - if (this_update->update_info_.firmware_url.find("http") == std::string::npos) { - std::string path = this_update->update_info_.firmware_url; - if (path[0] == '/') { - std::string domain = this_update->source_url_.substr(0, this_update->source_url_.find('/', 8)); - this_update->update_info_.firmware_url = domain + path; - } else { - std::string domain = this_update->source_url_.substr(0, this_update->source_url_.rfind('/') + 1); - this_update->update_info_.firmware_url = domain + path; } - } - { // Ensures the current version string falls out of scope and deallocates before the task ends - std::string current_version; #ifdef ESPHOME_PROJECT_VERSION - current_version = ESPHOME_PROJECT_VERSION; + info->current_version = ESPHOME_PROJECT_VERSION; #else - current_version = ESPHOME_VERSION; + info->current_version = ESPHOME_VERSION; #endif - - this_update->update_info_.current_version = current_version; } - bool trigger_update_available = false; +defer: + // Release container before vTaskDelete (which doesn't call destructors) + container.reset(); - if (this_update->update_info_.latest_version.empty() || - this_update->update_info_.latest_version == this_update->update_info_.current_version) { - this_update->state_ = update::UPDATE_STATE_NO_UPDATE; - } else { - if (this_update->state_ != update::UPDATE_STATE_AVAILABLE) { - trigger_update_available = true; + // Defer to the main loop so all update_info_ and state_ writes happen on the + // same thread as readers (API, MQTT, web server). This is a single defer for + // both success and error paths to avoid multiple std::function instantiations. + // Lambda captures only 2 pointers (8 bytes) — fits in std::function SBO on supported toolchains. + this_update->defer([this_update, result]() { +#ifdef USE_ESP32 + this_update->update_task_handle_ = nullptr; +#endif + if (result->error_str != nullptr) { + this_update->status_set_error(result->error_str); + delete result; + return; } - this_update->state_ = update::UPDATE_STATE_AVAILABLE; - } - // Defer to main loop to ensure thread-safe execution of: - // - status_clear_error() performs non-atomic read-modify-write on component_state_ - // - publish_state() triggers API callbacks that write to the shared protobuf buffer - // which can be corrupted if accessed concurrently from task and main loop threads - // - update_available trigger to ensure consistent state when the trigger fires - this_update->defer([this_update, trigger_update_available]() { - this_update->update_info_.has_progress = false; - this_update->update_info_.progress = 0.0f; + // Determine new state on main loop (avoids extra lambda captures from task) + bool trigger_update_available = false; + update::UpdateState new_state; + if (result->info.latest_version.empty() || result->info.latest_version == result->info.current_version) { + new_state = update::UPDATE_STATE_NO_UPDATE; + } else { + new_state = update::UPDATE_STATE_AVAILABLE; + if (this_update->state_ != update::UPDATE_STATE_AVAILABLE) { + trigger_update_available = true; + } + } + + this_update->update_info_ = std::move(result->info); + this_update->state_ = new_state; + delete result; // Safe: moved-from state is valid for destruction this_update->status_clear_error(); this_update->publish_state(); diff --git a/esphome/components/http_request/update/http_request_update.h b/esphome/components/http_request/update/http_request_update.h index cf34ace18e..b8350346f9 100644 --- a/esphome/components/http_request/update/http_request_update.h +++ b/esphome/components/http_request/update/http_request_update.h @@ -40,6 +40,7 @@ class HttpRequestUpdate final : public update::UpdateEntity, public PollingCompo #ifdef USE_ESP32 TaskHandle_t update_task_handle_{nullptr}; #endif + uint8_t initial_check_remaining_{0}; }; } // namespace http_request diff --git a/esphome/components/htu21d/htu21d.cpp b/esphome/components/htu21d/htu21d.cpp index c5d91d3dd0..58a28b213f 100644 --- a/esphome/components/htu21d/htu21d.cpp +++ b/esphome/components/htu21d/htu21d.cpp @@ -143,7 +143,5 @@ uint8_t HTU21DComponent::get_heater_level() { return raw_heater & 0xF; } -float HTU21DComponent::get_setup_priority() const { return setup_priority::DATA; } - } // namespace htu21d } // namespace esphome diff --git a/esphome/components/htu21d/htu21d.h b/esphome/components/htu21d/htu21d.h index 277c6ca3e5..594be78326 100644 --- a/esphome/components/htu21d/htu21d.h +++ b/esphome/components/htu21d/htu21d.h @@ -28,8 +28,6 @@ class HTU21DComponent : public PollingComponent, public i2c::I2CDevice { void set_heater_level(uint8_t level); uint8_t get_heater_level(); - float get_setup_priority() const override; - protected: sensor::Sensor *temperature_{nullptr}; sensor::Sensor *humidity_{nullptr}; diff --git a/esphome/components/htu21d/sensor.py b/esphome/components/htu21d/sensor.py index a578670e37..ed4fb5968a 100644 --- a/esphome/components/htu21d/sensor.py +++ b/esphome/components/htu21d/sensor.py @@ -93,6 +93,7 @@ async def to_code(config): }, key=CONF_LEVEL, ), + synchronous=True, ) async def set_heater_level_to_code(config, action_id, template_arg, args): var = cg.new_Pvariable(action_id, template_arg) @@ -112,10 +113,11 @@ async def set_heater_level_to_code(config, action_id, template_arg, args): }, key=CONF_STATUS, ), + synchronous=True, ) async def set_heater_to_code(config, action_id, template_arg, args): var = cg.new_Pvariable(action_id, template_arg) await cg.register_parented(var, config[CONF_ID]) - status_ = await cg.templatable(config[CONF_LEVEL], args, bool) + status_ = await cg.templatable(config[CONF_STATUS], args, bool) cg.add(var.set_status(status_)) return var diff --git a/esphome/components/htu31d/htu31d.cpp b/esphome/components/htu31d/htu31d.cpp index 562078aacb..4bb38a11a2 100644 --- a/esphome/components/htu31d/htu31d.cpp +++ b/esphome/components/htu31d/htu31d.cpp @@ -259,11 +259,5 @@ void HTU31DComponent::set_heater_state(bool desired) { } } -/** - * Sets the startup priority for this component. - * - * @returns The startup priority - */ -float HTU31DComponent::get_setup_priority() const { return setup_priority::DATA; } } // namespace htu31d } // namespace esphome diff --git a/esphome/components/htu31d/htu31d.h b/esphome/components/htu31d/htu31d.h index 9462133ced..24d85243cc 100644 --- a/esphome/components/htu31d/htu31d.h +++ b/esphome/components/htu31d/htu31d.h @@ -20,8 +20,6 @@ class HTU31DComponent : public PollingComponent, public i2c::I2CDevice { void set_heater_state(bool desired); bool is_heater_enabled(); - float get_setup_priority() const override; - protected: bool reset_(); uint32_t read_serial_num_(); diff --git a/esphome/components/hub75/display.py b/esphome/components/hub75/display.py index f1e6ef4243..0d1b87941d 100644 --- a/esphome/components/hub75/display.py +++ b/esphome/components/hub75/display.py @@ -128,6 +128,7 @@ SCAN_WIRINGS = { "STANDARD_TWO_SCAN": Hub75ScanWiring.STANDARD_TWO_SCAN, "SCAN_1_4_16PX_HIGH": Hub75ScanWiring.SCAN_1_4_16PX_HIGH, "SCAN_1_8_32PX_HIGH": Hub75ScanWiring.SCAN_1_8_32PX_HIGH, + "SCAN_1_8_32PX_FULL": Hub75ScanWiring.SCAN_1_8_32PX_FULL, "SCAN_1_8_40PX_HIGH": Hub75ScanWiring.SCAN_1_8_40PX_HIGH, "SCAN_1_8_64PX_HIGH": Hub75ScanWiring.SCAN_1_8_64PX_HIGH, } @@ -587,7 +588,7 @@ def _build_config_struct( async def to_code(config: ConfigType) -> None: add_idf_component( name="esphome/esp-hub75", - ref="0.3.2", + ref="0.3.5", ) # Set compile-time configuration via build flags (so external library sees them) @@ -652,6 +653,7 @@ async def to_code(config: ConfigType) -> None: }, key=CONF_BRIGHTNESS, ), + synchronous=True, ) async def hub75_set_brightness_to_code( config: ConfigType, diff --git a/esphome/components/hub75/hub75.cpp b/esphome/components/hub75/hub75.cpp index cf8661b2b3..ba652d427d 100644 --- a/esphome/components/hub75/hub75.cpp +++ b/esphome/components/hub75/hub75.cpp @@ -1,6 +1,8 @@ #include "hub75_component.h" #include "esphome/core/application.h" +#include + #ifdef USE_ESP32 namespace esphome::hub75 { @@ -58,7 +60,7 @@ void HUB75Display::dump_config() { config_.pins.oe, config_.pins.clk); ESP_LOGCONFIG(TAG, - " Clock Speed: %u MHz\n" + " Clock Speed: %" PRIu32 " MHz\n" " Latch Blanking: %i\n" " Clock Phase: %s\n" " Min Refresh Rate: %i Hz\n" diff --git a/esphome/components/hx711/hx711.cpp b/esphome/components/hx711/hx711.cpp index 67ec4549df..f2e3234127 100644 --- a/esphome/components/hx711/hx711.cpp +++ b/esphome/components/hx711/hx711.cpp @@ -22,7 +22,6 @@ void HX711Sensor::dump_config() { LOG_PIN(" SCK Pin: ", this->sck_pin_); LOG_UPDATE_INTERVAL(this); } -float HX711Sensor::get_setup_priority() const { return setup_priority::DATA; } void HX711Sensor::update() { uint32_t result; if (this->read_sensor_(&result)) { diff --git a/esphome/components/hx711/hx711.h b/esphome/components/hx711/hx711.h index a92bb9945d..37723ee81f 100644 --- a/esphome/components/hx711/hx711.h +++ b/esphome/components/hx711/hx711.h @@ -23,7 +23,6 @@ class HX711Sensor : public sensor::Sensor, public PollingComponent { void setup() override; void dump_config() override; - float get_setup_priority() const override; void update() override; protected: diff --git a/esphome/components/hydreon_rgxx/hydreon_rgxx.cpp b/esphome/components/hydreon_rgxx/hydreon_rgxx.cpp index 4872d68610..983a0a6649 100644 --- a/esphome/components/hydreon_rgxx/hydreon_rgxx.cpp +++ b/esphome/components/hydreon_rgxx/hydreon_rgxx.cpp @@ -284,7 +284,5 @@ void HydreonRGxxComponent::process_line_() { } } -float HydreonRGxxComponent::get_setup_priority() const { return setup_priority::DATA; } - } // namespace hydreon_rgxx } // namespace esphome diff --git a/esphome/components/hydreon_rgxx/hydreon_rgxx.h b/esphome/components/hydreon_rgxx/hydreon_rgxx.h index 76b0985a24..e3f9798a93 100644 --- a/esphome/components/hydreon_rgxx/hydreon_rgxx.h +++ b/esphome/components/hydreon_rgxx/hydreon_rgxx.h @@ -53,8 +53,6 @@ class HydreonRGxxComponent : public PollingComponent, public uart::UARTDevice { void setup() override; void dump_config() override; - float get_setup_priority() const override; - void set_disable_led(bool disable_led) { this->disable_led_ = disable_led; } protected: diff --git a/esphome/components/hyt271/hyt271.cpp b/esphome/components/hyt271/hyt271.cpp index f187e054a8..4c0e3cd96e 100644 --- a/esphome/components/hyt271/hyt271.cpp +++ b/esphome/components/hyt271/hyt271.cpp @@ -46,7 +46,5 @@ void HYT271Component::update() { this->status_clear_warning(); }); } -float HYT271Component::get_setup_priority() const { return setup_priority::DATA; } - } // namespace hyt271 } // namespace esphome diff --git a/esphome/components/hyt271/hyt271.h b/esphome/components/hyt271/hyt271.h index 64f32a651c..19409d830c 100644 --- a/esphome/components/hyt271/hyt271.h +++ b/esphome/components/hyt271/hyt271.h @@ -16,8 +16,6 @@ class HYT271Component : public PollingComponent, public i2c::I2CDevice { /// Update the sensor values (temperature+humidity). void update() override; - float get_setup_priority() const override; - protected: sensor::Sensor *temperature_{nullptr}; sensor::Sensor *humidity_{nullptr}; diff --git a/esphome/components/i2c/__init__.py b/esphome/components/i2c/__init__.py index 19efda0b49..1684f479ba 100644 --- a/esphome/components/i2c/__init__.py +++ b/esphome/components/i2c/__init__.py @@ -93,11 +93,31 @@ def _bus_declare_type(value): raise NotImplementedError +def _rp2040_i2c_controller(pin): + """Return the I2C controller number (0 or 1) for a given RP2040/RP2350 GPIO pin. + + See RP2040 datasheet Table 2 (section 1.4.3, "GPIO Functions"): + https://datasheets.raspberrypi.com/rp2040/rp2040-datasheet.pdf + See RP2350 datasheet Table 7 (section 9.4, "Function Select"): + https://datasheets.raspberrypi.com/rp2350/rp2350-datasheet.pdf + """ + return (pin // 2) % 2 + + def validate_config(config): if CORE.is_esp32: return cv.require_framework_version( esp_idf=cv.Version(5, 4, 2), esp32_arduino=cv.Version(3, 2, 1) )(config) + if CORE.is_rp2040: + sda_controller = _rp2040_i2c_controller(config[CONF_SDA]) + scl_controller = _rp2040_i2c_controller(config[CONF_SCL]) + if sda_controller != scl_controller: + raise cv.Invalid( + f"SDA pin GPIO{config[CONF_SDA]} is on I2C{sda_controller} but " + f"SCL pin GPIO{config[CONF_SCL]} is on I2C{scl_controller}. " + f"Both pins must be on the same I2C controller." + ) return config @@ -146,6 +166,23 @@ def _final_validate(config): full_config = fv.full_config.get()[CONF_I2C] if CORE.using_zephyr and len(full_config) > 1: raise cv.Invalid("Second i2c is not implemented on Zephyr yet") + if CORE.is_rp2040: + if len(full_config) > 2: + raise cv.Invalid( + "The maximum number of I2C interfaces for RP2040/RP2350 is 2" + ) + if len(full_config) > 1: + controllers = [ + _rp2040_i2c_controller(conf[CONF_SDA]) for conf in full_config + ] + if len(set(controllers)) != len(controllers): + raise cv.Invalid( + "Multiple I2C buses are configured to use the same I2C controller. " + "Each bus must use pins on a different controller. " + "The I2C controller is determined by (gpio / 2) % 2: " + "even pin pairs (0-1, 4-5, 8-9, ...) use I2C0, " + "odd pin pairs (2-3, 6-7, 10-11, ...) use I2C1." + ) if CORE.is_esp32 and get_esp32_variant() in ESP32_I2C_CAPABILITIES: variant = get_esp32_variant() max_num = ESP32_I2C_CAPABILITIES[variant]["NUM"] @@ -183,7 +220,7 @@ async def to_code(config): if CORE.using_zephyr: zephyr_add_prj_conf("I2C", True) i2c = "i2c0" - if zephyr_data()[KEY_BOARD] in ["xiao_ble"]: + if zephyr_data()[KEY_BOARD] == "xiao_ble": i2c = "i2c1" zephyr_add_overlay( f""" diff --git a/esphome/components/i2c/i2c.cpp b/esphome/components/i2c/i2c.cpp index c1e7336ce4..76b3ec3b4d 100644 --- a/esphome/components/i2c/i2c.cpp +++ b/esphome/components/i2c/i2c.cpp @@ -5,18 +5,11 @@ #include "esphome/core/log.h" #include -namespace esphome { -namespace i2c { +namespace esphome::i2c { static const char *const TAG = "i2c"; 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++) { auto err = write_readv(address, nullptr, 0, nullptr, 0); if (err == ERROR_OK) { @@ -27,9 +20,6 @@ void I2CBus::i2c_scan_() { // it takes 16sec to scan on nrf52. It prevents board reset. 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) { @@ -118,5 +108,4 @@ uint8_t I2CRegister16::get() const { return value; } -} // namespace i2c -} // namespace esphome +} // namespace esphome::i2c diff --git a/esphome/components/i2c/i2c.h b/esphome/components/i2c/i2c.h index 48a6e751cf..00929db620 100644 --- a/esphome/components/i2c/i2c.h +++ b/esphome/components/i2c/i2c.h @@ -6,8 +6,7 @@ #include "esphome/core/optional.h" #include "i2c_bus.h" -namespace esphome { -namespace i2c { +namespace esphome::i2c { #define LOG_I2C_DEVICE(this) ESP_LOGCONFIG(TAG, " Address: 0x%02X", this->address_); @@ -267,41 +266,9 @@ class I2CDevice { bool write_byte_16(uint8_t a_register, uint16_t data) const { return write_bytes_16(a_register, &data, 1); } - // Deprecated functions - - ESPDEPRECATED("The stop argument is no longer used. This will be removed from ESPHome 2026.3.0", "2025.9.0") - ErrorCode read_register(uint8_t a_register, uint8_t *data, size_t len, bool stop) { - return this->read_register(a_register, data, len); - } - - ESPDEPRECATED("The stop argument is no longer used. This will be removed from ESPHome 2026.3.0", "2025.9.0") - ErrorCode read_register16(uint16_t a_register, uint8_t *data, size_t len, bool stop) { - return this->read_register16(a_register, data, len); - } - - ESPDEPRECATED("The stop argument is no longer used; use write_read() for consecutive write and read. This will be " - "removed from ESPHome 2026.3.0", - "2025.9.0") - ErrorCode write(const uint8_t *data, size_t len, bool stop) const { return this->write(data, len); } - - ESPDEPRECATED("The stop argument is no longer used; use write_read() for consecutive write and read. This will be " - "removed from ESPHome 2026.3.0", - "2025.9.0") - ErrorCode write_register(uint8_t a_register, const uint8_t *data, size_t len, bool stop) const { - return this->write_register(a_register, data, len); - } - - ESPDEPRECATED("The stop argument is no longer used; use write_read() for consecutive write and read. This will be " - "removed from ESPHome 2026.3.0", - "2025.9.0") - ErrorCode write_register16(uint16_t a_register, const uint8_t *data, size_t len, bool stop) const { - return this->write_register16(a_register, data, len); - } - protected: uint8_t address_{0x00}; ///< store the address of the device on the bus I2CBus *bus_{nullptr}; ///< pointer to I2CBus instance }; -} // namespace i2c -} // namespace esphome +} // namespace esphome::i2c diff --git a/esphome/components/i2c/i2c_bus.h b/esphome/components/i2c/i2c_bus.h index 3de5d5ca7b..0c5e80bfe5 100644 --- a/esphome/components/i2c/i2c_bus.h +++ b/esphome/components/i2c/i2c_bus.h @@ -1,15 +1,12 @@ #pragma once #include #include -#include -#include #include #include #include "esphome/core/helpers.h" -namespace esphome { -namespace i2c { +namespace esphome::i2c { /// @brief Error codes returned by I2CBus and I2CDevice methods enum ErrorCode { @@ -24,18 +21,6 @@ enum ErrorCode { ERROR_CRC = 7, ///< bytes received with a CRC error }; -/// @brief the ReadBuffer structure stores a pointer to a read buffer and its length -struct ReadBuffer { - uint8_t *data; ///< pointer to the read buffer - size_t len; ///< length of the buffer -}; - -/// @brief the WriteBuffer structure stores a pointer to a write buffer and its length -struct WriteBuffer { - const uint8_t *data; ///< pointer to the write buffer - size_t len; ///< length of the buffer -}; - /// @brief This Class provides the methods to read and write bytes from an I2CBus. /// @note The I2CBus virtual class follows a *Factory design pattern* that provides all the interfaces methods required /// by clients while deferring the actual implementation of these methods to a subclasses. I2C-bus specification and @@ -68,50 +53,6 @@ class I2CBus { return this->write_readv(address, buffer, len, nullptr, 0); } - ESPDEPRECATED("This method is deprecated and will be removed in ESPHome 2026.3.0. Use write_readv() instead.", - "2025.9.0") - ErrorCode readv(uint8_t address, ReadBuffer *read_buffers, size_t count) { - size_t total_len = 0; - for (size_t i = 0; i != count; i++) { - total_len += read_buffers[i].len; - } - - SmallBufferWithHeapFallback<128> buffer_alloc(total_len); // Most I2C reads are small - uint8_t *buffer = buffer_alloc.get(); - - auto err = this->write_readv(address, nullptr, 0, buffer, total_len); - if (err != ERROR_OK) - return err; - size_t pos = 0; - for (size_t i = 0; i != count; i++) { - if (read_buffers[i].len != 0) { - std::memcpy(read_buffers[i].data, buffer + pos, read_buffers[i].len); - pos += read_buffers[i].len; - } - } - return ERROR_OK; - } - - ESPDEPRECATED("This method is deprecated and will be removed in ESPHome 2026.3.0. Use write_readv() instead.", - "2025.9.0") - ErrorCode writev(uint8_t address, const WriteBuffer *write_buffers, size_t count, bool stop = true) { - size_t total_len = 0; - for (size_t i = 0; i != count; i++) { - total_len += write_buffers[i].len; - } - - SmallBufferWithHeapFallback<128> buffer_alloc(total_len); // Most I2C writes are small - uint8_t *buffer = buffer_alloc.get(); - - size_t pos = 0; - for (size_t i = 0; i != count; i++) { - std::memcpy(buffer + pos, write_buffers[i].data, write_buffers[i].len); - pos += write_buffers[i].len; - } - - return this->write_readv(address, buffer, total_len, nullptr, 0); - } - protected: /// @brief Scans the I2C bus for devices. Devices presence is kept in an array of std::pair /// that contains the address and the corresponding bool presence flag. @@ -127,5 +68,4 @@ class InternalI2CBus : public I2CBus { virtual int get_port() const = 0; }; -} // namespace i2c -} // namespace esphome +} // namespace esphome::i2c diff --git a/esphome/components/i2c/i2c_bus_arduino.cpp b/esphome/components/i2c/i2c_bus_arduino.cpp index e728830147..47a06abe9e 100644 --- a/esphome/components/i2c/i2c_bus_arduino.cpp +++ b/esphome/components/i2c/i2c_bus_arduino.cpp @@ -7,8 +7,7 @@ #include "esphome/core/helpers.h" #include "esphome/core/log.h" -namespace esphome { -namespace i2c { +namespace esphome::i2c { static const char *const TAG = "i2c.arduino"; @@ -21,12 +20,14 @@ void ArduinoI2CBus::setup() { #if defined(USE_ESP8266) wire_ = new TwoWire(); // NOLINT(cppcoreguidelines-owning-memory) #elif defined(USE_RP2040) - static bool first = true; - if (first) { + // Select Wire instance based on pin assignment, not definition order. + // I2C controller = (gpio / 2) % 2: even pairs (0-1,4-5,...) → I2C0, odd pairs (2-3,6-7,...) → I2C1 + // RP2040 datasheet Table 2 (section 1.4.3): https://datasheets.raspberrypi.com/rp2040/rp2040-datasheet.pdf + // RP2350 datasheet Table 7 (section 9.4): https://datasheets.raspberrypi.com/rp2350/rp2350-datasheet.pdf + if ((this->sda_pin_ / 2) % 2 == 0) { wire_ = &Wire; - first = false; } else { - wire_ = &Wire1; // NOLINT(cppcoreguidelines-owning-memory) + wire_ = &Wire1; } #endif @@ -134,25 +135,23 @@ ErrorCode ArduinoI2CBus::write_readv(uint8_t address, const uint8_t *write_buffe for (size_t j = 0; j != read_count; j++) read_buffer[j] = wire_->read(); } - switch (status) { - case 0: - return ERROR_OK; - case 1: - // transmit buffer not large enough - ESP_LOGVV(TAG, "TX failed: buffer not large enough"); - return ERROR_UNKNOWN; - case 2: - case 3: - ESP_LOGVV(TAG, "TX failed: not acknowledged: %d", status); - return ERROR_NOT_ACKNOWLEDGED; - case 5: - ESP_LOGVV(TAG, "TX failed: timeout"); - return ERROR_UNKNOWN; - case 4: - default: - ESP_LOGVV(TAG, "TX failed: unknown error %u", status); - return ERROR_UNKNOWN; + // Avoid switch to prevent compiler-generated lookup table in RAM on ESP8266 + if (status == 0) + return ERROR_OK; + if (status == 1) { + ESP_LOGVV(TAG, "TX failed: buffer not large enough"); + return ERROR_UNKNOWN; } + if (status == 2 || status == 3) { + ESP_LOGVV(TAG, "TX failed: not acknowledged: %u", status); + return ERROR_NOT_ACKNOWLEDGED; + } + if (status == 5) { + ESP_LOGVV(TAG, "TX failed: timeout"); + return ERROR_UNKNOWN; + } + ESP_LOGVV(TAG, "TX failed: unknown error %u", status); + return ERROR_UNKNOWN; } /// Perform I2C bus recovery, see: @@ -264,7 +263,6 @@ void ArduinoI2CBus::recover_() { recovery_result_ = RECOVERY_COMPLETED; } -} // namespace i2c -} // namespace esphome +} // namespace esphome::i2c #endif // defined(USE_ARDUINO) && !defined(USE_ESP32) diff --git a/esphome/components/i2c/i2c_bus_arduino.h b/esphome/components/i2c/i2c_bus_arduino.h index 2d69e7684c..edc14af7bc 100644 --- a/esphome/components/i2c/i2c_bus_arduino.h +++ b/esphome/components/i2c/i2c_bus_arduino.h @@ -6,8 +6,7 @@ #include "esphome/core/component.h" #include "i2c_bus.h" -namespace esphome { -namespace i2c { +namespace esphome::i2c { enum RecoveryCode { RECOVERY_FAILED_SCL_LOW, @@ -45,7 +44,6 @@ class ArduinoI2CBus : public InternalI2CBus, public Component { bool initialized_ = false; }; -} // namespace i2c -} // namespace esphome +} // namespace esphome::i2c #endif // defined(USE_ARDUINO) && !defined(USE_ESP32) diff --git a/esphome/components/i2c/i2c_bus_esp_idf.cpp b/esphome/components/i2c/i2c_bus_esp_idf.cpp index 7a965ce5ad..4aca4f0fae 100644 --- a/esphome/components/i2c/i2c_bus_esp_idf.cpp +++ b/esphome/components/i2c/i2c_bus_esp_idf.cpp @@ -10,8 +10,7 @@ #include "esphome/core/helpers.h" #include "esphome/core/log.h" -namespace esphome { -namespace i2c { +namespace esphome::i2c { static const char *const TAG = "i2c.idf"; @@ -186,7 +185,7 @@ ErrorCode IDFI2CBus::write_readv(uint8_t address, const uint8_t *write_buffer, s jobs[num_jobs++].command = I2C_MASTER_CMD_STOP; ESP_LOGV(TAG, "Sending %zu jobs", num_jobs); esp_err_t err = i2c_master_execute_defined_operations(this->dev_, jobs, num_jobs, 100); - if (err == ESP_ERR_INVALID_STATE) { + if (err == ESP_ERR_INVALID_STATE || err == ESP_ERR_INVALID_RESPONSE) { ESP_LOGV(TAG, "TX to %02X failed: not acked", address); return ERROR_NOT_ACKNOWLEDGED; } else if (err == ESP_ERR_TIMEOUT) { @@ -312,6 +311,5 @@ void IDFI2CBus::recover_() { recovery_result_ = RECOVERY_COMPLETED; } -} // namespace i2c -} // namespace esphome +} // namespace esphome::i2c #endif // USE_ESP32 diff --git a/esphome/components/i2c/i2c_bus_esp_idf.h b/esphome/components/i2c/i2c_bus_esp_idf.h index 84f4616967..c23f9f0c54 100644 --- a/esphome/components/i2c/i2c_bus_esp_idf.h +++ b/esphome/components/i2c/i2c_bus_esp_idf.h @@ -6,8 +6,7 @@ #include "i2c_bus.h" #include -namespace esphome { -namespace i2c { +namespace esphome::i2c { enum RecoveryCode { RECOVERY_FAILED_SCL_LOW, @@ -56,7 +55,6 @@ class IDFI2CBus : public InternalI2CBus, public Component { #endif }; -} // namespace i2c -} // namespace esphome +} // namespace esphome::i2c #endif // USE_ESP32 diff --git a/esphome/components/i2s_audio/__init__.py b/esphome/components/i2s_audio/__init__.py index d3128c5f4c..ffa63f5ee8 100644 --- a/esphome/components/i2s_audio/__init__.py +++ b/esphome/components/i2s_audio/__init__.py @@ -1,6 +1,13 @@ +from dataclasses import dataclass, field + from esphome import pins import esphome.codegen as cg 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_ESP32C3, VARIANT_ESP32C5, @@ -10,8 +17,6 @@ from esphome.components.esp32 import ( VARIANT_ESP32P4, VARIANT_ESP32S2, VARIANT_ESP32S3, - add_idf_sdkconfig_option, - get_esp32_variant, ) import esphome.config_validation as cv from esphome.const import CONF_BITS_PER_SAMPLE, CONF_CHANNEL, CONF_ID, CONF_SAMPLE_RATE @@ -23,6 +28,9 @@ CODEOWNERS = ["@jesserockz"] DEPENDENCIES = ["esp32"] MULTI_CONF = True +CONF_PDM = "pdm" +CONF_ADC_TYPE = "adc_type" + CONF_I2S_DOUT_PIN = "i2s_dout_pin" CONF_I2S_DIN_PIN = "i2s_din_pin" CONF_I2S_MCLK_PIN = "i2s_mclk_pin" @@ -45,8 +53,6 @@ CONF_RIGHT = "right" CONF_STEREO = "stereo" CONF_BOTH = "both" -CONF_USE_LEGACY = "use_legacy" - i2s_audio_ns = cg.esphome_ns.namespace("i2s_audio") I2SAudioComponent = i2s_audio_ns.class_("I2SAudioComponent", cg.Component) I2SAudioBase = i2s_audio_ns.class_( @@ -146,20 +152,6 @@ def validate_mclk_divisible_by_3(config): return config -# Key for storing legacy driver setting in CORE.data -I2S_USE_LEGACY_DRIVER_KEY = "i2s_use_legacy_driver" - - -def _get_use_legacy_driver(): - """Get the legacy driver setting from CORE.data.""" - return CORE.data.get(I2S_USE_LEGACY_DRIVER_KEY) - - -def _set_use_legacy_driver(value: bool) -> None: - """Set the legacy driver setting in CORE.data.""" - CORE.data[I2S_USE_LEGACY_DRIVER_KEY] = value - - def i2s_audio_component_schema( class_: MockObjClass, *, @@ -184,10 +176,6 @@ def i2s_audio_component_schema( *I2S_MODE_OPTIONS, lower=True ), cv.Optional(CONF_USE_APLL, default=False): cv.boolean, - cv.Optional(CONF_BITS_PER_CHANNEL, default="default"): cv.All( - cv.Any(cv.float_with_unit("bits", "bit"), "default"), - cv.one_of(*I2S_BITS_PER_CHANNEL), - ), cv.Optional(CONF_MCLK_MULTIPLE, default=256): cv.one_of(*I2S_MCLK_MULTIPLE), } ) @@ -195,62 +183,82 @@ def i2s_audio_component_schema( async def register_i2s_audio_component(var, config): await cg.register_parented(var, config[CONF_I2S_AUDIO_ID]) - if use_legacy(): - cg.add(var.set_i2s_mode(I2S_MODE_OPTIONS[config[CONF_I2S_MODE]])) - cg.add(var.set_channel(I2S_CHANNELS[config[CONF_CHANNEL]])) - cg.add( - var.set_bits_per_sample(I2S_BITS_PER_SAMPLE[config[CONF_BITS_PER_SAMPLE]]) - ) - cg.add( - var.set_bits_per_channel( - I2S_BITS_PER_CHANNEL[config[CONF_BITS_PER_CHANNEL]] - ) - ) - else: - cg.add(var.set_i2s_role(I2S_ROLE_OPTIONS[config[CONF_I2S_MODE]])) - slot_mode = config[CONF_CHANNEL] - if slot_mode != CONF_STEREO: - slot_mode = CONF_MONO - slot_mask = config[CONF_CHANNEL] - if slot_mask not in [CONF_LEFT, CONF_RIGHT]: - slot_mask = CONF_BOTH - cg.add(var.set_slot_mode(I2S_SLOT_MODE[slot_mode])) - cg.add(var.set_std_slot_mask(I2S_STD_SLOT_MASK[slot_mask])) - cg.add(var.set_slot_bit_width(I2S_SLOT_BIT_WIDTH[config[CONF_BITS_PER_SAMPLE]])) + cg.add(var.set_i2s_role(I2S_ROLE_OPTIONS[config[CONF_I2S_MODE]])) + slot_mode = config[CONF_CHANNEL] + if slot_mode != CONF_STEREO: + slot_mode = CONF_MONO + slot_mask = config[CONF_CHANNEL] + if slot_mask not in [CONF_LEFT, CONF_RIGHT]: + slot_mask = CONF_BOTH + cg.add(var.set_slot_mode(I2S_SLOT_MODE[slot_mode])) + cg.add(var.set_std_slot_mask(I2S_STD_SLOT_MASK[slot_mask])) + cg.add(var.set_slot_bit_width(I2S_SLOT_BIT_WIDTH[config[CONF_BITS_PER_SAMPLE]])) cg.add(var.set_sample_rate(config[CONF_SAMPLE_RATE])) cg.add(var.set_use_apll(config[CONF_USE_APLL])) cg.add(var.set_mclk_multiple(I2S_MCLK_MULTIPLE[config[CONF_MCLK_MULTIPLE]])) -def validate_use_legacy(value): - if CONF_USE_LEGACY in value: - existing_value = _get_use_legacy_driver() - if (existing_value is not None) and (existing_value != value[CONF_USE_LEGACY]): - raise cv.Invalid( - f"All i2s_audio components must set {CONF_USE_LEGACY} to the same value." - ) - if (not value[CONF_USE_LEGACY]) and (CORE.using_arduino): - raise cv.Invalid("Arduino supports only the legacy i2s driver") - _set_use_legacy_driver(value[CONF_USE_LEGACY]) - elif CORE.using_arduino: - _set_use_legacy_driver(True) - return value - - -CONFIG_SCHEMA = cv.All( - cv.Schema( - { - cv.GenerateID(): cv.declare_id(I2SAudioComponent), - cv.Required(CONF_I2S_LRCLK_PIN): pins.internal_gpio_output_pin_number, - cv.Optional(CONF_I2S_BCLK_PIN): pins.internal_gpio_output_pin_number, - cv.Optional(CONF_I2S_MCLK_PIN): pins.internal_gpio_output_pin_number, - cv.Optional(CONF_USE_LEGACY): cv.boolean, - }, - ), - validate_use_legacy, +CONFIG_SCHEMA = cv.Schema( + { + cv.GenerateID(): cv.declare_id(I2SAudioComponent), + cv.Required(CONF_I2S_LRCLK_PIN): pins.internal_gpio_output_pin_number, + cv.Optional(CONF_I2S_BCLK_PIN): pins.internal_gpio_output_pin_number, + cv.Optional(CONF_I2S_MCLK_PIN): pins.internal_gpio_output_pin_number, + }, ) +@dataclass +class I2SAudioData: + """I2S audio component state stored in CORE.data.""" + + port_map: dict[str, int] = field(default_factory=dict) + + +def _get_data() -> I2SAudioData: + if CONF_I2S_AUDIO not in CORE.data: + CORE.data[CONF_I2S_AUDIO] = I2SAudioData() + return CORE.data[CONF_I2S_AUDIO] + + +def _assign_ports() -> None: + """Assign I2S port numbers, prioritizing instances with microphone children. + + Microphones (especially PDM) require port 0 on most ESP32 variants. + This runs once and stores the mapping in CORE.data. + """ + data = _get_data() + if data.port_map: + return + + full_config = fv.full_config.get() + i2s_configs = full_config[CONF_I2S_AUDIO] + + # Find i2s_audio instances with microphones that require port 0 + # (PDM and internal ADC only work on I2S port 0) + port0_parent_id = None + for mic_config in full_config.get("microphone", []): + if CONF_I2S_AUDIO_ID not in mic_config: + continue + if mic_config.get(CONF_PDM) or mic_config.get(CONF_ADC_TYPE) == "internal": + if port0_parent_id is not None: + raise cv.Invalid( + "Only one PDM/ADC microphone is supported (requires I2S port 0)" + ) + port0_parent_id = str(mic_config[CONF_I2S_AUDIO_ID]) + + # Assign ports: port 0 parent first (if any), rest get sequential + next_port = 0 + if port0_parent_id is not None: + data.port_map[port0_parent_id] = next_port + next_port += 1 + for config in i2s_configs: + config_id = str(config[CONF_ID]) + if config_id != port0_parent_id: + data.port_map[config_id] = next_port + next_port += 1 + + def _final_validate(_): i2s_audio_configs = fv.full_config.get()[CONF_I2S_AUDIO] variant = get_esp32_variant() @@ -260,10 +268,7 @@ def _final_validate(_): raise cv.Invalid( f"Only {I2S_PORTS[variant]} I2S audio ports are supported on {variant}" ) - - -def use_legacy(): - return _get_use_legacy_driver() + _assign_ports() FINAL_VALIDATE_SCHEMA = _final_validate @@ -272,8 +277,15 @@ FINAL_VALIDATE_SCHEMA = _final_validate async def to_code(config): var = cg.new_Pvariable(config[CONF_ID]) await cg.register_component(var, config) - if use_legacy(): - cg.add_define("USE_I2S_LEGACY") + + # Assign I2S port from _final_validate computed mapping + data = _get_data() + if (port := data.port_map.get(str(config[CONF_ID]))) is None: + raise ValueError(f"No I2S port assigned for {config[CONF_ID]}") + cg.add(var.set_port(port)) + + # Re-enable ESP-IDF's I2S driver (excluded by default to save compile time) + include_builtin_idf_component("esp_driver_i2s") # Helps avoid callbacks being skipped due to processor load add_idf_sdkconfig_option("CONFIG_I2S_ISR_IRAM_SAFE", True) diff --git a/esphome/components/i2s_audio/i2s_audio.cpp b/esphome/components/i2s_audio/i2s_audio.cpp deleted file mode 100644 index 43064498cc..0000000000 --- a/esphome/components/i2s_audio/i2s_audio.cpp +++ /dev/null @@ -1,27 +0,0 @@ -#include "i2s_audio.h" - -#ifdef USE_ESP32 - -#include "esphome/core/log.h" - -namespace esphome { -namespace i2s_audio { - -static const char *const TAG = "i2s_audio"; - -void I2SAudioComponent::setup() { - static i2s_port_t next_port_num = I2S_NUM_0; - if (next_port_num >= SOC_I2S_NUM) { - ESP_LOGE(TAG, "Too many components"); - this->mark_failed(); - return; - } - - this->port_ = next_port_num; - next_port_num = (i2s_port_t) (next_port_num + 1); -} - -} // namespace i2s_audio -} // namespace esphome - -#endif // USE_ESP32 diff --git a/esphome/components/i2s_audio/i2s_audio.h b/esphome/components/i2s_audio/i2s_audio.h index cfccf7e01f..5b260fa7ed 100644 --- a/esphome/components/i2s_audio/i2s_audio.h +++ b/esphome/components/i2s_audio/i2s_audio.h @@ -5,11 +5,8 @@ #include "esphome/core/component.h" #include "esphome/core/defines.h" #include "esphome/core/helpers.h" -#ifdef USE_I2S_LEGACY -#include -#else +#include #include -#endif namespace esphome { namespace i2s_audio { @@ -18,33 +15,19 @@ class I2SAudioComponent; class I2SAudioBase : public Parented { public: -#ifdef USE_I2S_LEGACY - void set_i2s_mode(i2s_mode_t mode) { this->i2s_mode_ = mode; } - void set_channel(i2s_channel_fmt_t channel) { this->channel_ = channel; } - void set_bits_per_sample(i2s_bits_per_sample_t bits_per_sample) { this->bits_per_sample_ = bits_per_sample; } - void set_bits_per_channel(i2s_bits_per_chan_t bits_per_channel) { this->bits_per_channel_ = bits_per_channel; } -#else void set_i2s_role(i2s_role_t role) { this->i2s_role_ = role; } void set_slot_mode(i2s_slot_mode_t slot_mode) { this->slot_mode_ = slot_mode; } void set_std_slot_mask(i2s_std_slot_mask_t std_slot_mask) { this->std_slot_mask_ = std_slot_mask; } void set_slot_bit_width(i2s_slot_bit_width_t slot_bit_width) { this->slot_bit_width_ = slot_bit_width; } -#endif void set_sample_rate(uint32_t sample_rate) { this->sample_rate_ = sample_rate; } void set_use_apll(uint32_t use_apll) { this->use_apll_ = use_apll; } void set_mclk_multiple(i2s_mclk_multiple_t mclk_multiple) { this->mclk_multiple_ = mclk_multiple; } protected: -#ifdef USE_I2S_LEGACY - i2s_mode_t i2s_mode_{}; - i2s_channel_fmt_t channel_; - i2s_bits_per_sample_t bits_per_sample_; - i2s_bits_per_chan_t bits_per_channel_; -#else i2s_role_t i2s_role_{}; i2s_slot_mode_t slot_mode_; i2s_std_slot_mask_t std_slot_mask_; i2s_slot_bit_width_t slot_bit_width_; -#endif uint32_t sample_rate_; bool use_apll_; i2s_mclk_multiple_t mclk_multiple_; @@ -56,19 +39,6 @@ class I2SAudioOut : public I2SAudioBase {}; class I2SAudioComponent : public Component { public: - void setup() override; - -#ifdef USE_I2S_LEGACY - i2s_pin_config_t get_pin_config() const { - return { - .mck_io_num = this->mclk_pin_, - .bck_io_num = this->bclk_pin_, - .ws_io_num = this->lrclk_pin_, - .data_out_num = I2S_PIN_NO_CHANGE, - .data_in_num = I2S_PIN_NO_CHANGE, - }; - } -#else i2s_std_gpio_config_t get_pin_config() const { return {.mclk = (gpio_num_t) this->mclk_pin_, .bclk = (gpio_num_t) this->bclk_pin_, @@ -81,32 +51,30 @@ class I2SAudioComponent : public Component { .ws_inv = false, }}; } -#endif void set_mclk_pin(int pin) { this->mclk_pin_ = pin; } void set_bclk_pin(int pin) { this->bclk_pin_ = pin; } void set_lrclk_pin(int pin) { this->lrclk_pin_ = pin; } + void set_port(int port) { this->port_ = port; } +#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(6, 0, 0) + int get_port() const { return this->port_; } +#else + i2s_port_t get_port() const { return static_cast(this->port_); } +#endif void lock() { this->lock_.lock(); } bool try_lock() { return this->lock_.try_lock(); } void unlock() { this->lock_.unlock(); } - i2s_port_t get_port() const { return this->port_; } - protected: Mutex lock_; I2SAudioIn *audio_in_{nullptr}; I2SAudioOut *audio_out_{nullptr}; -#ifdef USE_I2S_LEGACY - int mclk_pin_{I2S_PIN_NO_CHANGE}; - int bclk_pin_{I2S_PIN_NO_CHANGE}; -#else int mclk_pin_{I2S_GPIO_UNUSED}; int bclk_pin_{I2S_GPIO_UNUSED}; -#endif int lrclk_pin_; - i2s_port_t port_{}; + int port_{}; }; } // namespace i2s_audio diff --git a/esphome/components/i2s_audio/media_player/__init__.py b/esphome/components/i2s_audio/media_player/__init__.py index 35c42e1b06..b366d4fb05 100644 --- a/esphome/components/i2s_audio/media_player/__init__.py +++ b/esphome/components/i2s_audio/media_player/__init__.py @@ -1,120 +1,7 @@ -from esphome import pins -import esphome.codegen as cg -from esphome.components import esp32, media_player import esphome.config_validation as cv -from esphome.const import CONF_MODE -from .. import ( - CONF_I2S_AUDIO_ID, - CONF_I2S_DOUT_PIN, - CONF_LEFT, - CONF_MONO, - CONF_RIGHT, - CONF_STEREO, - I2SAudioComponent, - I2SAudioOut, - i2s_audio_ns, - use_legacy, +CONFIG_SCHEMA = cv.invalid( + "The I2S audio media player has been removed. " + "Use the speaker media player component instead. " + "See https://esphome.io/components/media_player/speaker.html for details." ) - -CODEOWNERS = ["@jesserockz"] -DEPENDENCIES = ["i2s_audio"] - -I2SAudioMediaPlayer = i2s_audio_ns.class_( - "I2SAudioMediaPlayer", cg.Component, media_player.MediaPlayer, I2SAudioOut -) - -i2s_dac_mode_t = cg.global_ns.enum("i2s_dac_mode_t") - - -CONF_MUTE_PIN = "mute_pin" -CONF_AUDIO_ID = "audio_id" -CONF_DAC_TYPE = "dac_type" -CONF_I2S_COMM_FMT = "i2s_comm_fmt" - -INTERNAL_DAC_OPTIONS = { - CONF_LEFT: i2s_dac_mode_t.I2S_DAC_CHANNEL_LEFT_EN, - CONF_RIGHT: i2s_dac_mode_t.I2S_DAC_CHANNEL_RIGHT_EN, - CONF_STEREO: i2s_dac_mode_t.I2S_DAC_CHANNEL_BOTH_EN, -} - -EXTERNAL_DAC_OPTIONS = [CONF_MONO, CONF_STEREO] - -NO_INTERNAL_DAC_VARIANTS = [esp32.VARIANT_ESP32S2] - -I2C_COMM_FMT_OPTIONS = ["lsb", "msb"] - - -def validate_esp32_variant(config): - if config[CONF_DAC_TYPE] != "internal": - return config - variant = esp32.get_esp32_variant() - if variant in NO_INTERNAL_DAC_VARIANTS: - raise cv.Invalid(f"{variant} does not have an internal DAC") - return config - - -CONFIG_SCHEMA = cv.All( - cv.typed_schema( - { - "internal": media_player.media_player_schema(I2SAudioMediaPlayer) - .extend( - { - cv.GenerateID(CONF_I2S_AUDIO_ID): cv.use_id(I2SAudioComponent), - cv.Required(CONF_MODE): cv.enum(INTERNAL_DAC_OPTIONS, lower=True), - } - ) - .extend(cv.COMPONENT_SCHEMA), - "external": media_player.media_player_schema(I2SAudioMediaPlayer) - .extend( - { - cv.GenerateID(CONF_I2S_AUDIO_ID): cv.use_id(I2SAudioComponent), - cv.Required( - CONF_I2S_DOUT_PIN - ): pins.internal_gpio_output_pin_number, - cv.Optional(CONF_MUTE_PIN): pins.gpio_output_pin_schema, - cv.Optional(CONF_MODE, default="mono"): cv.one_of( - *EXTERNAL_DAC_OPTIONS, lower=True - ), - cv.Optional(CONF_I2S_COMM_FMT, default="msb"): cv.one_of( - *I2C_COMM_FMT_OPTIONS, lower=True - ), - } - ) - .extend(cv.COMPONENT_SCHEMA), - }, - key=CONF_DAC_TYPE, - ), - cv.only_with_arduino, - validate_esp32_variant, -) - - -def _final_validate(_): - if not use_legacy(): - raise cv.Invalid("I2S media player is only compatible with legacy i2s driver") - - -FINAL_VALIDATE_SCHEMA = _final_validate - - -async def to_code(config): - var = await media_player.new_media_player(config) - await cg.register_component(var, config) - - await cg.register_parented(var, config[CONF_I2S_AUDIO_ID]) - - if config[CONF_DAC_TYPE] == "internal": - cg.add(var.set_internal_dac_mode(config[CONF_MODE])) - else: - cg.add(var.set_dout_pin(config[CONF_I2S_DOUT_PIN])) - if CONF_MUTE_PIN in config: - pin = await cg.gpio_pin_expression(config[CONF_MUTE_PIN]) - cg.add(var.set_mute_pin(pin)) - cg.add(var.set_external_dac_channels(2 if config[CONF_MODE] == "stereo" else 1)) - cg.add(var.set_i2s_comm_fmt_lsb(config[CONF_I2S_COMM_FMT] == "lsb")) - - cg.add_library("NetworkClientSecure", None) - cg.add_library("HTTPClient", None) - cg.add_library("esphome/ESP32-audioI2S", "2.3.0") - cg.add_build_flag("-DAUDIO_NO_SD_FS") diff --git a/esphome/components/i2s_audio/media_player/i2s_audio_media_player.cpp b/esphome/components/i2s_audio/media_player/i2s_audio_media_player.cpp deleted file mode 100644 index 39301220d5..0000000000 --- a/esphome/components/i2s_audio/media_player/i2s_audio_media_player.cpp +++ /dev/null @@ -1,257 +0,0 @@ -#include "i2s_audio_media_player.h" - -#ifdef USE_ESP32_FRAMEWORK_ARDUINO - -#include "esphome/core/log.h" - -namespace esphome { -namespace i2s_audio { - -static const char *const TAG = "audio"; - -void I2SAudioMediaPlayer::control(const media_player::MediaPlayerCall &call) { - media_player::MediaPlayerState play_state = media_player::MEDIA_PLAYER_STATE_PLAYING; - if (call.get_announcement().has_value()) { - play_state = call.get_announcement().value() ? media_player::MEDIA_PLAYER_STATE_ANNOUNCING - : media_player::MEDIA_PLAYER_STATE_PLAYING; - } - if (call.get_media_url().has_value()) { - this->current_url_ = call.get_media_url(); - if (this->i2s_state_ != I2S_STATE_STOPPED && this->audio_ != nullptr) { - if (this->audio_->isRunning()) { - this->audio_->stopSong(); - } - this->audio_->connecttohost(this->current_url_.value().c_str()); - this->state = play_state; - } else { - this->start(); - } - } - - if (play_state == media_player::MEDIA_PLAYER_STATE_ANNOUNCING) { - this->is_announcement_ = true; - } - - if (call.get_volume().has_value()) { - this->volume = call.get_volume().value(); - this->set_volume_(volume); - this->unmute_(); - } - if (call.get_command().has_value()) { - switch (call.get_command().value()) { - case media_player::MEDIA_PLAYER_COMMAND_MUTE: - this->mute_(); - break; - case media_player::MEDIA_PLAYER_COMMAND_UNMUTE: - this->unmute_(); - break; - case media_player::MEDIA_PLAYER_COMMAND_VOLUME_UP: { - float new_volume = this->volume + 0.1f; - if (new_volume > 1.0f) - new_volume = 1.0f; - this->set_volume_(new_volume); - this->unmute_(); - break; - } - case media_player::MEDIA_PLAYER_COMMAND_VOLUME_DOWN: { - float new_volume = this->volume - 0.1f; - if (new_volume < 0.0f) - new_volume = 0.0f; - this->set_volume_(new_volume); - this->unmute_(); - break; - } - default: - break; - } - if (this->i2s_state_ != I2S_STATE_RUNNING) { - return; - } - switch (call.get_command().value()) { - case media_player::MEDIA_PLAYER_COMMAND_PLAY: - if (!this->audio_->isRunning()) - this->audio_->pauseResume(); - this->state = play_state; - break; - case media_player::MEDIA_PLAYER_COMMAND_PAUSE: - if (this->audio_->isRunning()) - this->audio_->pauseResume(); - this->state = media_player::MEDIA_PLAYER_STATE_PAUSED; - break; - case media_player::MEDIA_PLAYER_COMMAND_STOP: - this->stop(); - break; - case media_player::MEDIA_PLAYER_COMMAND_TOGGLE: - this->audio_->pauseResume(); - if (this->audio_->isRunning()) { - this->state = media_player::MEDIA_PLAYER_STATE_PLAYING; - } else { - this->state = media_player::MEDIA_PLAYER_STATE_PAUSED; - } - break; - default: - break; - } - } - this->publish_state(); -} - -void I2SAudioMediaPlayer::mute_() { - if (this->mute_pin_ != nullptr) { - this->mute_pin_->digital_write(true); - } else { - this->set_volume_(0.0f, false); - } - this->muted_ = true; -} -void I2SAudioMediaPlayer::unmute_() { - if (this->mute_pin_ != nullptr) { - this->mute_pin_->digital_write(false); - } else { - this->set_volume_(this->volume, false); - } - this->muted_ = false; -} -void I2SAudioMediaPlayer::set_volume_(float volume, bool publish) { - if (this->audio_ != nullptr) - this->audio_->setVolume(remap(volume, 0.0f, 1.0f, 0, 21)); - if (publish) - this->volume = volume; -} - -void I2SAudioMediaPlayer::setup() { this->state = media_player::MEDIA_PLAYER_STATE_IDLE; } - -void I2SAudioMediaPlayer::loop() { - switch (this->i2s_state_) { - case I2S_STATE_STARTING: - this->start_(); - break; - case I2S_STATE_RUNNING: - this->play_(); - break; - case I2S_STATE_STOPPING: - this->stop_(); - break; - case I2S_STATE_STOPPED: - break; - } -} - -void I2SAudioMediaPlayer::play_() { - this->audio_->loop(); - if ((this->state == media_player::MEDIA_PLAYER_STATE_PLAYING || - this->state == media_player::MEDIA_PLAYER_STATE_ANNOUNCING) && - !this->audio_->isRunning()) { - this->stop(); - } -} - -void I2SAudioMediaPlayer::start() { this->i2s_state_ = I2S_STATE_STARTING; } -void I2SAudioMediaPlayer::start_() { - if (!this->parent_->try_lock()) { - return; // Waiting for another i2s to return lock - } - -#if SOC_I2S_SUPPORTS_DAC - if (this->internal_dac_mode_ != I2S_DAC_CHANNEL_DISABLE) { - this->audio_ = make_unique