diff --git a/.ai/instructions.md b/.ai/instructions.md index a7e08f9c4d..86f554e9ce 100644 --- a/.ai/instructions.md +++ b/.ai/instructions.md @@ -239,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)`. @@ -274,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 @@ -417,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/.github/scripts/auto-label-pr/detectors.js b/.github/scripts/auto-label-pr/detectors.js index fc63198019..fb9dadc6a0 100644 --- a/.github/scripts/auto-label-pr/detectors.js +++ b/.github/scripts/auto-label-pr/detectors.js @@ -235,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/workflows/ci.yml b/.github/workflows/ci.yml index ab7a750388..06e8189f54 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -339,7 +339,7 @@ jobs: echo "binary=$BINARY" >> $GITHUB_OUTPUT - name: Run CodSpeed benchmarks - uses: CodSpeedHQ/action@1c8ae4843586d3ba879736b7f6b7b0c990757fab # v4 + uses: CodSpeedHQ/action@d872884a306dd4853acf0f584f4b706cf0cc72a2 # v4 with: run: ${{ steps.build.outputs.binary }} mode: simulation @@ -723,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() diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index 6baab70b42..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@38697555549f1db7851b81482ff19f1fa5c4fedc # v4.34.1 + 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@38697555549f1db7851b81482ff19f1fa5c4fedc # v4.34.1 + 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 index 2ad023ed1b..0021654def 100644 --- a/.github/workflows/pr-title-check.yml +++ b/.github/workflows/pr-title-check.yml @@ -3,6 +3,9 @@ name: PR Title Check on: pull_request: types: [opened, edited, synchronize, reopened] + branches-ignore: + - release + - beta permissions: contents: read diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 4aa63f6a16..ba6db99b84 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -102,12 +102,12 @@ jobs: uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4.0.0 - name: Log in to docker hub - uses: docker/login-action@b45d80f862d83dbcd57f89517bcf500b2ab88fb2 # v4.0.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@b45d80f862d83dbcd57f89517bcf500b2ab88fb2 # v4.0.0 + uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0 with: registry: ghcr.io username: ${{ github.actor }} @@ -182,13 +182,13 @@ jobs: - name: Log in to docker hub if: matrix.registry == 'dockerhub' - uses: docker/login-action@b45d80f862d83dbcd57f89517bcf500b2ab88fb2 # v4.0.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@b45d80f862d83dbcd57f89517bcf500b2ab88fb2 # v4.0.0 + uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0 with: registry: ghcr.io username: ${{ github.actor }} diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index f4729f211c..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.15.8 + rev: v0.15.9 hooks: # Run the linter. - id: ruff @@ -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 8d297d7b07..c466204b66 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -142,7 +142,7 @@ 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 @@ -217,6 +217,7 @@ 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 @@ -330,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 diff --git a/esphome/automation.py b/esphome/automation.py index 7b1d6ceca1..94d64086ec 100644 --- a/esphome/automation.py +++ b/esphome/automation.py @@ -250,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) @@ -261,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) @@ -272,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) @@ -283,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) @@ -305,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) diff --git a/esphome/components/adc/adc_sensor_esp32.cpp b/esphome/components/adc/adc_sensor_esp32.cpp index 1d3138623e..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 { @@ -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/ads1118/__init__.py b/esphome/components/ads1118/__init__.py index 128e0d0701..45d47a329e 100644 --- a/esphome/components/ads1118/__init__.py +++ b/esphome/components/ads1118/__init__.py @@ -12,11 +12,15 @@ CONF_ADS1118_ID = "ads1118_id" ads1118_ns = cg.esphome_ns.namespace("ads1118") ADS1118 = ads1118_ns.class_("ADS1118", cg.Component, spi.SPIDevice) -CONFIG_SCHEMA = cv.Schema( - { - cv.GenerateID(): cv.declare_id(ADS1118), - } -).extend(spi.spi_device_schema(cs_pin_required=True)) +CONFIG_SCHEMA = ( + cv.Schema( + { + cv.GenerateID(): cv.declare_id(ADS1118), + } + ) + .extend(spi.spi_device_schema(cs_pin_required=True)) + .extend(cv.COMPONENT_SCHEMA) +) async def to_code(config): diff --git a/esphome/components/ags10/sensor.py b/esphome/components/ags10/sensor.py index 4cfa9e67ec..e94504ff1a 100644 --- a/esphome/components/ags10/sensor.py +++ b/esphome/components/ags10/sensor.py @@ -35,7 +35,7 @@ CONFIG_SCHEMA = ( cv.Schema( { cv.GenerateID(): cv.declare_id(AGS10Component), - cv.Optional(CONF_TVOC): sensor.sensor_schema( + cv.Required(CONF_TVOC): sensor.sensor_schema( unit_of_measurement=UNIT_PARTS_PER_BILLION, icon=ICON_RADIATOR, accuracy_decimals=0, @@ -112,7 +112,9 @@ AGS10_SET_ZERO_POINT_ACTION_MODE = { AGS10_SET_ZERO_POINT_SCHEMA = cv.Schema( { cv.GenerateID(): cv.use_id(AGS10Component), - cv.Required(CONF_MODE): cv.enum(AGS10_SET_ZERO_POINT_ACTION_MODE, upper=True), + cv.Required(CONF_MODE): cv.templatable( + cv.enum(AGS10_SET_ZERO_POINT_ACTION_MODE, upper=True) + ), cv.Optional(CONF_VALUE, default=0xFFFF): cv.templatable(cv.uint16_t), }, ) @@ -127,8 +129,10 @@ AGS10_SET_ZERO_POINT_SCHEMA = cv.Schema( async def ags10setzeropoint_to_code(config, action_id, template_arg, args): var = cg.new_Pvariable(action_id, template_arg) await cg.register_parented(var, config[CONF_ID]) - mode = await cg.templatable(config.get(CONF_MODE), args, enumerate) + mode = await cg.templatable( + config.get(CONF_MODE), args, AGS10SetZeroPointActionMode + ) cg.add(var.set_mode(mode)) - value = await cg.templatable(config[CONF_VALUE], args, int) + value = await cg.templatable(config[CONF_VALUE], args, cg.uint16) cg.add(var.set_value(value)) return var diff --git a/esphome/components/alarm_control_panel/__init__.py b/esphome/components/alarm_control_panel/__init__.py index aefb18d25c..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({}), } ) ) @@ -189,38 +114,39 @@ ALARM_CONTROL_PANEL_CONDITION_SCHEMA = maybe_simple_id( @setup_entity("alarm_control_panel") async def setup_alarm_control_panel_core_(var, config): 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): diff --git a/esphome/components/alarm_control_panel/alarm_control_panel.cpp b/esphome/components/alarm_control_panel/alarm_control_panel.cpp index 623241851a..fc72c13ce3 100644 --- a/esphome/components/alarm_control_panel/alarm_control_panel.cpp +++ b/esphome/components/alarm_control_panel/alarm_control_panel.cpp @@ -35,8 +35,8 @@ void AlarmControlPanel::publish_state(AlarmControlPanelState state) { 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 diff --git a/esphome/components/alarm_control_panel/alarm_control_panel.h b/esphome/components/alarm_control_panel/alarm_control_panel.h index cf99d359e7..e748b8621b 100644 --- a/esphome/components/alarm_control_panel/alarm_control_panel.h +++ b/esphome/components/alarm_control_panel/alarm_control_panel.h @@ -145,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/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/sensor.py b/esphome/components/alpha3/sensor.py index 361e1d101f..279ab214cf 100644 --- a/esphome/components/alpha3/sensor.py +++ b/esphome/components/alpha3/sensor.py @@ -9,6 +9,10 @@ from esphome.const import ( CONF_POWER, CONF_SPEED, CONF_VOLTAGE, + DEVICE_CLASS_CURRENT, + DEVICE_CLASS_POWER, + DEVICE_CLASS_VOLTAGE, + STATE_CLASS_MEASUREMENT, UNIT_AMPERE, UNIT_CUBIC_METER_PER_HOUR, UNIT_METER, @@ -27,26 +31,35 @@ CONFIG_SCHEMA = ( cv.Optional(CONF_FLOW): sensor.sensor_schema( unit_of_measurement=UNIT_CUBIC_METER_PER_HOUR, accuracy_decimals=2, + state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional(CONF_HEAD): sensor.sensor_schema( unit_of_measurement=UNIT_METER, accuracy_decimals=2, + state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional(CONF_POWER): sensor.sensor_schema( unit_of_measurement=UNIT_WATT, accuracy_decimals=2, + device_class=DEVICE_CLASS_POWER, + state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional(CONF_CURRENT): sensor.sensor_schema( unit_of_measurement=UNIT_AMPERE, accuracy_decimals=2, + device_class=DEVICE_CLASS_CURRENT, + state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional(CONF_SPEED): sensor.sensor_schema( unit_of_measurement=UNIT_REVOLUTIONS_PER_MINUTE, accuracy_decimals=2, + state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional(CONF_VOLTAGE): sensor.sensor_schema( unit_of_measurement=UNIT_VOLT, accuracy_decimals=2, + device_class=DEVICE_CLASS_VOLTAGE, + state_class=STATE_CLASS_MEASUREMENT, ), } ) diff --git a/esphome/components/am43/sensor/__init__.py b/esphome/components/am43/sensor/__init__.py index 4b3e1716a4..2697d364ad 100644 --- a/esphome/components/am43/sensor/__init__.py +++ b/esphome/components/am43/sensor/__init__.py @@ -8,6 +8,7 @@ from esphome.const import ( DEVICE_CLASS_BATTERY, ENTITY_CATEGORY_DIAGNOSTIC, ICON_BRIGHTNESS_5, + STATE_CLASS_MEASUREMENT, UNIT_PERCENT, ) @@ -26,11 +27,13 @@ CONFIG_SCHEMA = ( device_class=DEVICE_CLASS_BATTERY, accuracy_decimals=0, entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional(CONF_ILLUMINANCE): sensor.sensor_schema( unit_of_measurement=UNIT_PERCENT, icon=ICON_BRIGHTNESS_5, accuracy_decimals=0, + state_class=STATE_CLASS_MEASUREMENT, ), } ) diff --git a/esphome/components/api/api.proto b/esphome/components/api/api.proto index 96ee2fb920..07705baff6 100644 --- a/esphome/components/api/api.proto +++ b/esphome/components/api/api.proto @@ -308,6 +308,12 @@ enum EntityCategory { ENTITY_CATEGORY_DIAGNOSTIC = 2; } +// Entity field max_data_length values match Python validation constants: +// name/object_id = 120 (config_validation.NAME_MAX_LENGTH) +// icon = 63 (core/config.ICON_MAX_LENGTH) +// device_class = 47 (core/config.DEVICE_CLASS_MAX_LENGTH) +// unit_of_measurement = 63 (core/config.UNIT_OF_MEASUREMENT_MAX_LENGTH) + // ==================== BINARY SENSOR ==================== message ListEntitiesBinarySensorResponse { option (id) = 12; @@ -315,15 +321,15 @@ message ListEntitiesBinarySensorResponse { option (source) = SOURCE_SERVER; option (ifdef) = "USE_BINARY_SENSOR"; - string object_id = 1; + string object_id = 1 [(max_data_length) = 120, (force) = true]; fixed32 key = 2 [(force) = true]; - string name = 3; + string name = 3 [(max_data_length) = 120, (force) = true]; reserved 4; // Deprecated: was string unique_id - string device_class = 5; + string device_class = 5 [(max_data_length) = 47]; bool is_status_binary_sensor = 6; bool disabled_by_default = 7; - string icon = 8 [(field_ifdef) = "USE_ENTITY_ICON"]; + string icon = 8 [(field_ifdef) = "USE_ENTITY_ICON", (max_data_length) = 63]; EntityCategory entity_category = 9; uint32 device_id = 10 [(field_ifdef) = "USE_DEVICES"]; } @@ -349,17 +355,17 @@ message ListEntitiesCoverResponse { option (source) = SOURCE_SERVER; option (ifdef) = "USE_COVER"; - string object_id = 1; + string object_id = 1 [(max_data_length) = 120, (force) = true]; fixed32 key = 2 [(force) = true]; - string name = 3; + string name = 3 [(max_data_length) = 120, (force) = true]; reserved 4; // Deprecated: was string unique_id bool assumed_state = 5; bool supports_position = 6; bool supports_tilt = 7; - string device_class = 8; + string device_class = 8 [(max_data_length) = 47]; bool disabled_by_default = 9; - string icon = 10 [(field_ifdef) = "USE_ENTITY_ICON"]; + string icon = 10 [(field_ifdef) = "USE_ENTITY_ICON", (max_data_length) = 63]; EntityCategory entity_category = 11; bool supports_stop = 12; uint32 device_id = 13 [(field_ifdef) = "USE_DEVICES"]; @@ -433,9 +439,9 @@ message ListEntitiesFanResponse { option (source) = SOURCE_SERVER; option (ifdef) = "USE_FAN"; - string object_id = 1; + string object_id = 1 [(max_data_length) = 120, (force) = true]; fixed32 key = 2 [(force) = true]; - string name = 3; + string name = 3 [(max_data_length) = 120, (force) = true]; reserved 4; // Deprecated: was string unique_id bool supports_oscillation = 5; @@ -443,7 +449,7 @@ message ListEntitiesFanResponse { bool supports_direction = 7; int32 supported_speed_count = 8; bool disabled_by_default = 9; - string icon = 10 [(field_ifdef) = "USE_ENTITY_ICON"]; + string icon = 10 [(field_ifdef) = "USE_ENTITY_ICON", (max_data_length) = 63]; EntityCategory entity_category = 11; repeated string supported_preset_modes = 12 [(container_pointer_no_template) = "std::vector"]; uint32 device_id = 13 [(field_ifdef) = "USE_DEVICES"]; @@ -521,9 +527,9 @@ message ListEntitiesLightResponse { option (source) = SOURCE_SERVER; option (ifdef) = "USE_LIGHT"; - string object_id = 1; + string object_id = 1 [(max_data_length) = 120, (force) = true]; fixed32 key = 2 [(force) = true]; - string name = 3; + string name = 3 [(max_data_length) = 120, (force) = true]; reserved 4; // Deprecated: was string unique_id repeated ColorMode supported_color_modes = 12 [(container_pointer_no_template) = "light::ColorModeMask"]; @@ -540,7 +546,7 @@ message ListEntitiesLightResponse { float max_mireds = 10; repeated string effects = 11 [(container_pointer_no_template) = "FixedVector"]; bool disabled_by_default = 13; - string icon = 14 [(field_ifdef) = "USE_ENTITY_ICON"]; + string icon = 14 [(field_ifdef) = "USE_ENTITY_ICON", (max_data_length) = 63]; EntityCategory entity_category = 15; uint32 device_id = 16 [(field_ifdef) = "USE_DEVICES"]; } @@ -626,16 +632,16 @@ message ListEntitiesSensorResponse { option (source) = SOURCE_SERVER; option (ifdef) = "USE_SENSOR"; - string object_id = 1; + string object_id = 1 [(max_data_length) = 120, (force) = true]; fixed32 key = 2 [(force) = true]; - string name = 3; + string name = 3 [(max_data_length) = 120, (force) = true]; reserved 4; // Deprecated: was string unique_id - string icon = 5 [(field_ifdef) = "USE_ENTITY_ICON"]; - string unit_of_measurement = 6; + string icon = 5 [(field_ifdef) = "USE_ENTITY_ICON", (max_data_length) = 63]; + string unit_of_measurement = 6 [(max_data_length) = 63]; int32 accuracy_decimals = 7; bool force_update = 8; - string device_class = 9; + string device_class = 9 [(max_data_length) = 47]; SensorStateClass state_class = 10; // Last reset type removed in 2021.9.0 // Deprecated in API version 1.5 @@ -666,16 +672,16 @@ message ListEntitiesSwitchResponse { option (source) = SOURCE_SERVER; option (ifdef) = "USE_SWITCH"; - string object_id = 1; + string object_id = 1 [(max_data_length) = 120, (force) = true]; fixed32 key = 2 [(force) = true]; - string name = 3; + string name = 3 [(max_data_length) = 120, (force) = true]; reserved 4; // Deprecated: was string unique_id - string icon = 5 [(field_ifdef) = "USE_ENTITY_ICON"]; + string icon = 5 [(field_ifdef) = "USE_ENTITY_ICON", (max_data_length) = 63]; bool assumed_state = 6; bool disabled_by_default = 7; EntityCategory entity_category = 8; - string device_class = 9; + string device_class = 9 [(max_data_length) = 47]; uint32 device_id = 10 [(field_ifdef) = "USE_DEVICES"]; } message SwitchStateResponse { @@ -708,15 +714,15 @@ message ListEntitiesTextSensorResponse { option (source) = SOURCE_SERVER; option (ifdef) = "USE_TEXT_SENSOR"; - string object_id = 1; + string object_id = 1 [(max_data_length) = 120, (force) = true]; fixed32 key = 2 [(force) = true]; - string name = 3; + string name = 3 [(max_data_length) = 120, (force) = true]; reserved 4; // Deprecated: was string unique_id - string icon = 5 [(field_ifdef) = "USE_ENTITY_ICON"]; + string icon = 5 [(field_ifdef) = "USE_ENTITY_ICON", (max_data_length) = 63]; bool disabled_by_default = 6; EntityCategory entity_category = 7; - string device_class = 8; + string device_class = 8 [(max_data_length) = 47]; uint32 device_id = 9 [(field_ifdef) = "USE_DEVICES"]; } message TextSensorStateResponse { @@ -971,12 +977,12 @@ message ListEntitiesCameraResponse { option (source) = SOURCE_SERVER; option (ifdef) = "USE_CAMERA"; - string object_id = 1; + string object_id = 1 [(max_data_length) = 120, (force) = true]; fixed32 key = 2 [(force) = true]; - string name = 3; + string name = 3 [(max_data_length) = 120, (force) = true]; reserved 4; // Deprecated: was string unique_id bool disabled_by_default = 5; - string icon = 6 [(field_ifdef) = "USE_ENTITY_ICON"]; + string icon = 6 [(field_ifdef) = "USE_ENTITY_ICON", (max_data_length) = 63]; EntityCategory entity_category = 7; uint32 device_id = 8 [(field_ifdef) = "USE_DEVICES"]; } @@ -1056,9 +1062,9 @@ message ListEntitiesClimateResponse { option (source) = SOURCE_SERVER; option (ifdef) = "USE_CLIMATE"; - string object_id = 1; + string object_id = 1 [(max_data_length) = 120, (force) = true]; fixed32 key = 2 [(force) = true]; - string name = 3; + string name = 3 [(max_data_length) = 120, (force) = true]; reserved 4; // Deprecated: was string unique_id bool supports_current_temperature = 5; // Deprecated: use feature_flags @@ -1078,7 +1084,7 @@ message ListEntitiesClimateResponse { repeated ClimatePreset supported_presets = 16 [(container_pointer_no_template) = "climate::ClimatePresetMask"]; repeated string supported_custom_presets = 17 [(container_pointer_no_template) = "std::vector"]; bool disabled_by_default = 18; - string icon = 19 [(field_ifdef) = "USE_ENTITY_ICON"]; + string icon = 19 [(field_ifdef) = "USE_ENTITY_ICON", (max_data_length) = 63]; EntityCategory entity_category = 20; float visual_current_temperature_step = 21; bool supports_current_humidity = 22; // Deprecated: use feature_flags @@ -1167,10 +1173,10 @@ message ListEntitiesWaterHeaterResponse { option (source) = SOURCE_SERVER; option (ifdef) = "USE_WATER_HEATER"; - string object_id = 1; + string object_id = 1 [(max_data_length) = 120, (force) = true]; fixed32 key = 2 [(force) = true]; - string name = 3; - string icon = 4 [(field_ifdef) = "USE_ENTITY_ICON"]; + string name = 3 [(max_data_length) = 120, (force) = true]; + string icon = 4 [(field_ifdef) = "USE_ENTITY_ICON", (max_data_length) = 63]; bool disabled_by_default = 5; EntityCategory entity_category = 6; uint32 device_id = 7 [(field_ifdef) = "USE_DEVICES"]; @@ -1243,20 +1249,20 @@ message ListEntitiesNumberResponse { option (source) = SOURCE_SERVER; option (ifdef) = "USE_NUMBER"; - string object_id = 1; + string object_id = 1 [(max_data_length) = 120, (force) = true]; fixed32 key = 2 [(force) = true]; - string name = 3; + string name = 3 [(max_data_length) = 120, (force) = true]; reserved 4; // Deprecated: was string unique_id - string icon = 5 [(field_ifdef) = "USE_ENTITY_ICON"]; + string icon = 5 [(field_ifdef) = "USE_ENTITY_ICON", (max_data_length) = 63]; float min_value = 6; float max_value = 7; float step = 8; bool disabled_by_default = 9; EntityCategory entity_category = 10; - string unit_of_measurement = 11; + string unit_of_measurement = 11 [(max_data_length) = 63]; NumberMode mode = 12; - string device_class = 13; + string device_class = 13 [(max_data_length) = 47]; uint32 device_id = 14 [(field_ifdef) = "USE_DEVICES"]; } message NumberStateResponse { @@ -1292,12 +1298,12 @@ message ListEntitiesSelectResponse { option (source) = SOURCE_SERVER; option (ifdef) = "USE_SELECT"; - string object_id = 1; + string object_id = 1 [(max_data_length) = 120, (force) = true]; fixed32 key = 2 [(force) = true]; - string name = 3; + string name = 3 [(max_data_length) = 120, (force) = true]; reserved 4; // Deprecated: was string unique_id - string icon = 5 [(field_ifdef) = "USE_ENTITY_ICON"]; + string icon = 5 [(field_ifdef) = "USE_ENTITY_ICON", (max_data_length) = 63]; repeated string options = 6 [(container_pointer_no_template) = "FixedVector"]; bool disabled_by_default = 7; EntityCategory entity_category = 8; @@ -1336,12 +1342,12 @@ message ListEntitiesSirenResponse { option (source) = SOURCE_SERVER; option (ifdef) = "USE_SIREN"; - string object_id = 1; + string object_id = 1 [(max_data_length) = 120, (force) = true]; fixed32 key = 2 [(force) = true]; - string name = 3; + string name = 3 [(max_data_length) = 120, (force) = true]; reserved 4; // Deprecated: was string unique_id - string icon = 5 [(field_ifdef) = "USE_ENTITY_ICON"]; + string icon = 5 [(field_ifdef) = "USE_ENTITY_ICON", (max_data_length) = 63]; bool disabled_by_default = 6; repeated string tones = 7 [(container_pointer_no_template) = "FixedVector"]; bool supports_duration = 8; @@ -1399,12 +1405,12 @@ message ListEntitiesLockResponse { option (source) = SOURCE_SERVER; option (ifdef) = "USE_LOCK"; - string object_id = 1; + string object_id = 1 [(max_data_length) = 120, (force) = true]; fixed32 key = 2 [(force) = true]; - string name = 3; + string name = 3 [(max_data_length) = 120, (force) = true]; reserved 4; // Deprecated: was string unique_id - string icon = 5 [(field_ifdef) = "USE_ENTITY_ICON"]; + string icon = 5 [(field_ifdef) = "USE_ENTITY_ICON", (max_data_length) = 63]; bool disabled_by_default = 6; EntityCategory entity_category = 7; bool assumed_state = 8; @@ -1448,15 +1454,15 @@ message ListEntitiesButtonResponse { option (source) = SOURCE_SERVER; option (ifdef) = "USE_BUTTON"; - string object_id = 1; + string object_id = 1 [(max_data_length) = 120, (force) = true]; fixed32 key = 2 [(force) = true]; - string name = 3; + string name = 3 [(max_data_length) = 120, (force) = true]; reserved 4; // Deprecated: was string unique_id - string icon = 5 [(field_ifdef) = "USE_ENTITY_ICON"]; + string icon = 5 [(field_ifdef) = "USE_ENTITY_ICON", (max_data_length) = 63]; bool disabled_by_default = 6; EntityCategory entity_category = 7; - string device_class = 8; + string device_class = 8 [(max_data_length) = 47]; uint32 device_id = 9 [(field_ifdef) = "USE_DEVICES"]; } message ButtonCommandRequest { @@ -1515,12 +1521,12 @@ message ListEntitiesMediaPlayerResponse { option (source) = SOURCE_SERVER; option (ifdef) = "USE_MEDIA_PLAYER"; - string object_id = 1; + string object_id = 1 [(max_data_length) = 120, (force) = true]; fixed32 key = 2 [(force) = true]; - string name = 3; + string name = 3 [(max_data_length) = 120, (force) = true]; reserved 4; // Deprecated: was string unique_id - string icon = 5 [(field_ifdef) = "USE_ENTITY_ICON"]; + string icon = 5 [(field_ifdef) = "USE_ENTITY_ICON", (max_data_length) = 63]; bool disabled_by_default = 6; EntityCategory entity_category = 7; @@ -1606,7 +1612,7 @@ message BluetoothLEAdvertisementResponse { message BluetoothLERawAdvertisement { uint64 address = 1 [(force) = true]; sint32 rssi = 2 [(force) = true]; - uint32 address_type = 3; + uint32 address_type = 3 [(max_value) = 4]; bytes data = 4 [(fixed_array_size) = 62, (force) = true]; } @@ -2103,11 +2109,11 @@ message ListEntitiesAlarmControlPanelResponse { option (source) = SOURCE_SERVER; option (ifdef) = "USE_ALARM_CONTROL_PANEL"; - string object_id = 1; + string object_id = 1 [(max_data_length) = 120, (force) = true]; fixed32 key = 2 [(force) = true]; - string name = 3; + string name = 3 [(max_data_length) = 120, (force) = true]; reserved 4; // Deprecated: was string unique_id - string icon = 5 [(field_ifdef) = "USE_ENTITY_ICON"]; + string icon = 5 [(field_ifdef) = "USE_ENTITY_ICON", (max_data_length) = 63]; bool disabled_by_default = 6; EntityCategory entity_category = 7; uint32 supported_features = 8; @@ -2150,11 +2156,11 @@ message ListEntitiesTextResponse { option (source) = SOURCE_SERVER; option (ifdef) = "USE_TEXT"; - string object_id = 1; + string object_id = 1 [(max_data_length) = 120, (force) = true]; fixed32 key = 2 [(force) = true]; - string name = 3; + string name = 3 [(max_data_length) = 120, (force) = true]; reserved 4; // Deprecated: was string unique_id - string icon = 5 [(field_ifdef) = "USE_ENTITY_ICON"]; + string icon = 5 [(field_ifdef) = "USE_ENTITY_ICON", (max_data_length) = 63]; bool disabled_by_default = 6; EntityCategory entity_category = 7; @@ -2198,12 +2204,12 @@ message ListEntitiesDateResponse { option (source) = SOURCE_SERVER; option (ifdef) = "USE_DATETIME_DATE"; - string object_id = 1; + string object_id = 1 [(max_data_length) = 120, (force) = true]; fixed32 key = 2 [(force) = true]; - string name = 3; + string name = 3 [(max_data_length) = 120, (force) = true]; reserved 4; // Deprecated: was string unique_id - string icon = 5 [(field_ifdef) = "USE_ENTITY_ICON"]; + string icon = 5 [(field_ifdef) = "USE_ENTITY_ICON", (max_data_length) = 63]; bool disabled_by_default = 6; EntityCategory entity_category = 7; uint32 device_id = 8 [(field_ifdef) = "USE_DEVICES"]; @@ -2245,12 +2251,12 @@ message ListEntitiesTimeResponse { option (source) = SOURCE_SERVER; option (ifdef) = "USE_DATETIME_TIME"; - string object_id = 1; + string object_id = 1 [(max_data_length) = 120, (force) = true]; fixed32 key = 2 [(force) = true]; - string name = 3; + string name = 3 [(max_data_length) = 120, (force) = true]; reserved 4; // Deprecated: was string unique_id - string icon = 5 [(field_ifdef) = "USE_ENTITY_ICON"]; + string icon = 5 [(field_ifdef) = "USE_ENTITY_ICON", (max_data_length) = 63]; bool disabled_by_default = 6; EntityCategory entity_category = 7; uint32 device_id = 8 [(field_ifdef) = "USE_DEVICES"]; @@ -2292,15 +2298,15 @@ message ListEntitiesEventResponse { option (source) = SOURCE_SERVER; option (ifdef) = "USE_EVENT"; - string object_id = 1; + string object_id = 1 [(max_data_length) = 120, (force) = true]; fixed32 key = 2 [(force) = true]; - string name = 3; + string name = 3 [(max_data_length) = 120, (force) = true]; reserved 4; // Deprecated: was string unique_id - string icon = 5 [(field_ifdef) = "USE_ENTITY_ICON"]; + string icon = 5 [(field_ifdef) = "USE_ENTITY_ICON", (max_data_length) = 63]; bool disabled_by_default = 6; EntityCategory entity_category = 7; - string device_class = 8; + string device_class = 8 [(max_data_length) = 47]; repeated string event_types = 9 [(container_pointer_no_template) = "FixedVector"]; uint32 device_id = 10 [(field_ifdef) = "USE_DEVICES"]; @@ -2323,15 +2329,15 @@ message ListEntitiesValveResponse { option (source) = SOURCE_SERVER; option (ifdef) = "USE_VALVE"; - string object_id = 1; + string object_id = 1 [(max_data_length) = 120, (force) = true]; fixed32 key = 2 [(force) = true]; - string name = 3; + string name = 3 [(max_data_length) = 120, (force) = true]; reserved 4; // Deprecated: was string unique_id - string icon = 5 [(field_ifdef) = "USE_ENTITY_ICON"]; + string icon = 5 [(field_ifdef) = "USE_ENTITY_ICON", (max_data_length) = 63]; bool disabled_by_default = 6; EntityCategory entity_category = 7; - string device_class = 8; + string device_class = 8 [(max_data_length) = 47]; bool assumed_state = 9; bool supports_position = 10; @@ -2378,12 +2384,12 @@ message ListEntitiesDateTimeResponse { option (source) = SOURCE_SERVER; option (ifdef) = "USE_DATETIME_DATETIME"; - string object_id = 1; + string object_id = 1 [(max_data_length) = 120, (force) = true]; fixed32 key = 2 [(force) = true]; - string name = 3; + string name = 3 [(max_data_length) = 120, (force) = true]; reserved 4; // Deprecated: was string unique_id - string icon = 5 [(field_ifdef) = "USE_ENTITY_ICON"]; + string icon = 5 [(field_ifdef) = "USE_ENTITY_ICON", (max_data_length) = 63]; bool disabled_by_default = 6; EntityCategory entity_category = 7; uint32 device_id = 8 [(field_ifdef) = "USE_DEVICES"]; @@ -2421,15 +2427,15 @@ message ListEntitiesUpdateResponse { option (source) = SOURCE_SERVER; option (ifdef) = "USE_UPDATE"; - string object_id = 1; + string object_id = 1 [(max_data_length) = 120, (force) = true]; fixed32 key = 2 [(force) = true]; - string name = 3; + string name = 3 [(max_data_length) = 120, (force) = true]; reserved 4; // Deprecated: was string unique_id - string icon = 5 [(field_ifdef) = "USE_ENTITY_ICON"]; + string icon = 5 [(field_ifdef) = "USE_ENTITY_ICON", (max_data_length) = 63]; bool disabled_by_default = 6; EntityCategory entity_category = 7; - string device_class = 8; + string device_class = 8 [(max_data_length) = 47]; uint32 device_id = 9 [(field_ifdef) = "USE_DEVICES"]; } message UpdateStateResponse { @@ -2504,10 +2510,10 @@ message ListEntitiesInfraredResponse { option (source) = SOURCE_SERVER; option (ifdef) = "USE_INFRARED"; - string object_id = 1; + string object_id = 1 [(max_data_length) = 120, (force) = true]; fixed32 key = 2 [(force) = true]; - string name = 3; - string icon = 4 [(field_ifdef) = "USE_ENTITY_ICON"]; + string name = 3 [(max_data_length) = 120, (force) = true]; + string icon = 4 [(field_ifdef) = "USE_ENTITY_ICON", (max_data_length) = 63]; bool disabled_by_default = 5; EntityCategory entity_category = 6; uint32 device_id = 7 [(field_ifdef) = "USE_DEVICES"]; diff --git a/esphome/components/api/api_connection.cpp b/esphome/components/api/api_connection.cpp index 0a99adcacf..feb16e4f4c 100644 --- a/esphome/components/api/api_connection.cpp +++ b/esphome/components/api/api_connection.cpp @@ -132,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(); @@ -1465,7 +1463,7 @@ void APIConnection::send_infrared_rf_receive_event(const InfraredRFReceiveEvent 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 %u out of range (max %u)", msg.instance, + ESP_LOGW(TAG, "Serial proxy instance %" PRIu32 " out of range (max %" PRIu32 ")", msg.instance, static_cast(proxies.size())); return; } @@ -1476,7 +1474,7 @@ void APIConnection::on_serial_proxy_configure_request(const SerialProxyConfigure 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 %u out of range", msg.instance); + ESP_LOGW(TAG, "Serial proxy instance %" PRIu32 " out of range", msg.instance); return; } proxies[msg.instance]->write_from_client(msg.data, msg.data_len); @@ -1485,7 +1483,7 @@ void APIConnection::on_serial_proxy_write_request(const SerialProxyWriteRequest 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 %u out of range", msg.instance); + ESP_LOGW(TAG, "Serial proxy instance %" PRIu32 " out of range", msg.instance); return; } proxies[msg.instance]->set_modem_pins(msg.line_states); @@ -1494,7 +1492,7 @@ void APIConnection::on_serial_proxy_set_modem_pins_request(const SerialProxySetM 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 %u out of range", msg.instance); + ESP_LOGW(TAG, "Serial proxy instance %" PRIu32 " out of range", msg.instance); return; } SerialProxyGetModemPinsResponse resp{}; @@ -1506,7 +1504,7 @@ void APIConnection::on_serial_proxy_get_modem_pins_request(const SerialProxyGetM 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 %u out of range", msg.instance); + ESP_LOGW(TAG, "Serial proxy instance %" PRIu32 " out of range", msg.instance); return; } switch (msg.type) { @@ -1536,7 +1534,7 @@ void APIConnection::on_serial_proxy_request(const SerialProxyRequest &msg) { break; } default: - ESP_LOGW(TAG, "Unknown serial proxy request type: %u", static_cast(msg.type)); + ESP_LOGW(TAG, "Unknown serial proxy request type: %" PRIu32, static_cast(msg.type)); break; } } @@ -1995,7 +1993,7 @@ bool APIConnection::send_message_(uint32_t payload_size, uint8_t message_type, M size_t write_start = shared_buf.size(); shared_buf.resize(write_start + payload_size); ProtoWriteBuffer buffer{&shared_buf, write_start}; - encode_fn(msg, buffer); + encode_fn(msg, buffer PROTO_ENCODE_DEBUG_INIT(&shared_buf)); return this->send_buffer(ProtoWriteBuffer{&shared_buf}, message_type); } // Encodes a message to the buffer and returns the total number of bytes used, @@ -2023,24 +2021,23 @@ uint16_t APIConnection::encode_to_buffer(uint32_t calculated_size, MessageEncode 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 - // Add padding for previous message footer + this message header - size_t current_size = shared_buf.size(); - shared_buf.reserve_and_resize(current_size + total_calculated_size, current_size + footer_size + header_padding); + // Reserve for full message, resize to include footer gap + header padding + payload + to_add = total_calculated_size; } - // Pre-resize buffer to include payload, then encode through raw pointer - size_t write_start = shared_buf.size(); - shared_buf.resize(write_start + calculated_size); - ProtoWriteBuffer buffer{&shared_buf, write_start}; - encode_fn(msg, buffer); + shared_buf.resize(shared_buf.size() + to_add); + ProtoWriteBuffer buffer{&shared_buf, shared_buf.size() - calculated_size}; + encode_fn(msg, buffer PROTO_ENCODE_DEBUG_INIT(&shared_buf)); // Return total size (header + payload + footer) - return static_cast(header_padding + calculated_size + footer_size); + 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); @@ -2072,37 +2069,9 @@ void APIConnection::on_fatal_error() { this->flags_.remove = true; } -void __attribute__((flatten)) APIConnection::DeferredBatch::push_item(const BatchItem &item) { items.push_back(item); } - -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 - this->push_item({entity, message_type, estimated_size, aux_data_index}); -} - -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 - this->push_item({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::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_(); } bool APIConnection::send_message_smart_(EntityBase *entity, uint8_t message_type, uint8_t estimated_size, diff --git a/esphome/components/api/api_connection.h b/esphome/components/api/api_connection.h index 3d8563b1ae..2f685b0b8a 100644 --- a/esphome/components/api/api_connection.h +++ b/esphome/components/api/api_connection.h @@ -20,6 +20,9 @@ #ifdef USE_RP2040_CRASH_HANDLER #include "esphome/components/rp2040/crash_handler.h" #endif +#ifdef USE_ESP8266_CRASH_HANDLER +#include "esphome/components/esp8266/crash_handler.h" +#endif #include "esphome/core/entity_base.h" #include "esphome/core/string_ref.h" @@ -44,10 +47,22 @@ 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"); +#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); ~APIConnection(); @@ -264,6 +279,9 @@ class APIConnection final : public APIServerConnectionBase { #endif #ifdef USE_RP2040_CRASH_HANDLER rp2040::crash_handler_log(); +#endif +#ifdef USE_ESP8266_CRASH_HANDLER + esp8266::crash_handler_log(); #endif } #ifdef USE_API_HOMEASSISTANT_SERVICES @@ -306,7 +324,7 @@ class APIConnection final : public APIServerConnectionBase { void on_no_setup_connection(); // Function pointer type for type-erased message encoding - using MessageEncodeFn = void (*)(const void *, ProtoWriteBuffer &); + using MessageEncodeFn = uint8_t *(*) (const void *, ProtoWriteBuffer &PROTO_ENCODE_DEBUG_PARAM); // Function pointer type for type-erased size calculation using CalculateSizeFn = uint32_t (*)(const void *); @@ -385,7 +403,9 @@ class APIConnection final : public APIServerConnectionBase { } // Shared no-op encode thunk for empty messages (ESTIMATED_SIZE == 0) - static void encode_msg_noop(const void *, ProtoWriteBuffer &) {} + static uint8_t *encode_msg_noop(const void *, ProtoWriteBuffer &buf PROTO_ENCODE_DEBUG_PARAM) { + return buf.get_pos(); + } // Non-template buffer management for send_message bool send_message_(uint32_t payload_size, uint8_t message_type, MessageEncodeFn encode_fn, const void *msg); @@ -632,11 +652,28 @@ class APIConnection final : public APIServerConnectionBase { // 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); - // Single push_back site to avoid duplicate _M_realloc_insert instantiation - void push_item(const BatchItem &item); + 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() { @@ -701,7 +738,7 @@ class APIConnection final : public APIServerConnectionBase { 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 @@ -768,10 +805,8 @@ class APIConnection final : public APIServerConnectionBase { } // 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_noise.cpp b/esphome/components/api/api_frame_helper_noise.cpp index 78e87793fc..162f4ef605 100644 --- a/esphome/components/api/api_frame_helper_noise.cpp +++ b/esphome/components/api/api_frame_helper_noise.cpp @@ -244,132 +244,144 @@ APIError APINoiseFrameHelper::try_read_frame_() { * If an error occurred, returns that error. Only returns OK if the transport is ready for data * traffic. */ +// Split into per-state methods so the compiler doesn't allocate stack space +// for all branches simultaneously. On RP2040 the core0 stack lives in a 4KB +// scratch RAM bank; the Noise crypto path (curve25519) needs ~2KB+ of stack, +// so every byte saved in the caller matters. APIError APINoiseFrameHelper::state_action_() { - int err; - APIError aerr; - if (state_ == State::INITIALIZE) { - HELPER_LOG("Bad state for method: %d", (int) state_); - return APIError::BAD_STATE; + switch (this->state_) { + case State::INITIALIZE: + HELPER_LOG("Bad state for method: %d", (int) this->state_); + return APIError::BAD_STATE; + case State::CLIENT_HELLO: + return this->state_action_client_hello_(); + case State::SERVER_HELLO: + return this->state_action_server_hello_(); + case State::HANDSHAKE: + return this->state_action_handshake_(); + case State::CLOSED: + case State::FAILED: + return APIError::BAD_STATE; + default: + return APIError::OK; } - if (state_ == State::CLIENT_HELLO) { - // waiting for client hello - aerr = this->try_read_frame_(); - if (aerr != APIError::OK) { - return handle_handshake_frame_error_(aerr); - } - // 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(); - 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; +} +APIError APINoiseFrameHelper::state_action_client_hello_() { + // waiting for client hello + APIError aerr = this->try_read_frame_(); + if (aerr != APIError::OK) { + return handle_handshake_frame_error_(aerr); } - if (state_ == State::SERVER_HELLO) { - // send server hello - const auto &name = App.get_name(); - char mac[MAC_ADDRESS_BUFFER_SIZE]; - get_mac_address_into_buffer(mac); - - // Calculate positions and sizes - size_t name_len = name.size() + 1; // including null terminator - size_t name_offset = 1; - size_t mac_offset = name_offset + name_len; - size_t total_size = 1 + name_len + MAC_ADDRESS_BUFFER_SIZE; - - // 1 (proto) + name (max ESPHOME_DEVICE_NAME_MAX_LEN) + 1 (name null) - // + mac (MAC_ADDRESS_BUFFER_SIZE - 1) + 1 (mac null) - constexpr size_t max_msg_size = 1 + ESPHOME_DEVICE_NAME_MAX_LEN + 1 + MAC_ADDRESS_BUFFER_SIZE; - uint8_t msg[max_msg_size]; - - // chosen proto - msg[0] = 0x01; - - // node name, terminated by null byte - std::memcpy(msg + name_offset, name.c_str(), name_len); - // node mac, terminated by null byte - std::memcpy(msg + mac_offset, mac, MAC_ADDRESS_BUFFER_SIZE); - - aerr = write_frame_(msg, total_size); - if (aerr != APIError::OK) - return aerr; - - // start handshake - aerr = init_handshake_(); - if (aerr != APIError::OK) - return aerr; - - state_ = State::HANDSHAKE; + // 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(); + 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); } - if (state_ == State::HANDSHAKE) { - int action = noise_handshakestate_get_action(handshake_); - if (action == NOISE_ACTION_READ_MESSAGE) { - // waiting for handshake msg - aerr = this->try_read_frame_(); - if (aerr != APIError::OK) { - return handle_handshake_frame_error_(aerr); - } - if (this->rx_buf_.empty()) { - send_explicit_handshake_reject_(LOG_STR("Empty handshake message")); - return APIError::BAD_HANDSHAKE_ERROR_BYTE; - } else if (this->rx_buf_[0] != 0x00) { - HELPER_LOG("Bad handshake error byte: %u", this->rx_buf_[0]); - send_explicit_handshake_reject_(LOG_STR("Bad handshake error byte")); - return APIError::BAD_HANDSHAKE_ERROR_BYTE; - } - - NoiseBuffer mbuf; - noise_buffer_init(mbuf); - noise_buffer_set_input(mbuf, this->rx_buf_.data() + 1, this->rx_buf_.size() - 1); - err = noise_handshakestate_read_message(handshake_, &mbuf, nullptr); - if (err != 0) { - // Special handling for MAC failure - send_explicit_handshake_reject_(err == NOISE_ERROR_MAC_FAILURE ? LOG_STR("Handshake MAC failure") - : LOG_STR("Handshake error")); - return handle_noise_error_(err, LOG_STR("noise_handshakestate_read_message"), - APIError::HANDSHAKESTATE_READ_FAILED); - } - - aerr = check_handshake_finished_(); - if (aerr != APIError::OK) - return aerr; - } else if (action == NOISE_ACTION_WRITE_MESSAGE) { - uint8_t buffer[65]; - NoiseBuffer mbuf; - noise_buffer_init(mbuf); - noise_buffer_set_output(mbuf, buffer + 1, sizeof(buffer) - 1); - - err = noise_handshakestate_write_message(handshake_, &mbuf, nullptr); - APIError aerr_write = handle_noise_error_(err, LOG_STR("noise_handshakestate_write_message"), - APIError::HANDSHAKESTATE_WRITE_FAILED); - if (aerr_write != APIError::OK) - return aerr_write; - buffer[0] = 0x00; // success - - aerr = write_frame_(buffer, mbuf.size + 1); - if (aerr != APIError::OK) - return aerr; - aerr = check_handshake_finished_(); - if (aerr != APIError::OK) - return aerr; - } else { - // bad state for action - state_ = State::FAILED; - HELPER_LOG("Bad action for handshake: %d", action); - return APIError::HANDSHAKESTATE_BAD_STATE; - } - } - if (state_ == State::CLOSED || state_ == State::FAILED) { - return APIError::BAD_STATE; - } + state_ = State::SERVER_HELLO; return APIError::OK; } +APIError APINoiseFrameHelper::state_action_server_hello_() { + // send server hello + const auto &name = App.get_name(); + char mac[MAC_ADDRESS_BUFFER_SIZE]; + get_mac_address_into_buffer(mac); + + // Calculate positions and sizes + size_t name_len = name.size() + 1; // including null terminator + size_t name_offset = 1; + size_t mac_offset = name_offset + name_len; + size_t total_size = 1 + name_len + MAC_ADDRESS_BUFFER_SIZE; + + // 1 (proto) + name (max ESPHOME_DEVICE_NAME_MAX_LEN) + 1 (name null) + // + mac (MAC_ADDRESS_BUFFER_SIZE - 1) + 1 (mac null) + constexpr size_t max_msg_size = 1 + ESPHOME_DEVICE_NAME_MAX_LEN + 1 + MAC_ADDRESS_BUFFER_SIZE; + uint8_t msg[max_msg_size]; + + // chosen proto + msg[0] = 0x01; + + // node name, terminated by null byte + std::memcpy(msg + name_offset, name.c_str(), name_len); + // node mac, terminated by null byte + std::memcpy(msg + mac_offset, mac, MAC_ADDRESS_BUFFER_SIZE); + + APIError aerr = write_frame_(msg, total_size); + if (aerr != APIError::OK) + return aerr; + + // start handshake + aerr = init_handshake_(); + if (aerr != APIError::OK) + return aerr; + + state_ = State::HANDSHAKE; + return APIError::OK; +} +APIError APINoiseFrameHelper::state_action_handshake_() { + int action = noise_handshakestate_get_action(this->handshake_); + if (action == NOISE_ACTION_READ_MESSAGE) { + return this->state_action_handshake_read_(); + } else if (action == NOISE_ACTION_WRITE_MESSAGE) { + return this->state_action_handshake_write_(); + } + // bad state for action + this->state_ = State::FAILED; + HELPER_LOG("Bad action for handshake: %d", action); + return APIError::HANDSHAKESTATE_BAD_STATE; +} +APIError APINoiseFrameHelper::state_action_handshake_read_() { + APIError aerr = this->try_read_frame_(); + if (aerr != APIError::OK) { + return this->handle_handshake_frame_error_(aerr); + } + + if (this->rx_buf_.empty()) { + this->send_explicit_handshake_reject_(LOG_STR("Empty handshake message")); + return APIError::BAD_HANDSHAKE_ERROR_BYTE; + } else if (this->rx_buf_[0] != 0x00) { + HELPER_LOG("Bad handshake error byte: %u", this->rx_buf_[0]); + this->send_explicit_handshake_reject_(LOG_STR("Bad handshake error byte")); + return APIError::BAD_HANDSHAKE_ERROR_BYTE; + } + + NoiseBuffer mbuf; + noise_buffer_init(mbuf); + noise_buffer_set_input(mbuf, this->rx_buf_.data() + 1, this->rx_buf_.size() - 1); + int err = noise_handshakestate_read_message(this->handshake_, &mbuf, nullptr); + if (err != 0) { + // Special handling for MAC failure + this->send_explicit_handshake_reject_(err == NOISE_ERROR_MAC_FAILURE ? LOG_STR("Handshake MAC failure") + : LOG_STR("Handshake error")); + return this->handle_noise_error_(err, LOG_STR("noise_handshakestate_read_message"), + APIError::HANDSHAKESTATE_READ_FAILED); + } + + return this->check_handshake_finished_(); +} +APIError APINoiseFrameHelper::state_action_handshake_write_() { + uint8_t buffer[65]; + NoiseBuffer mbuf; + noise_buffer_init(mbuf); + noise_buffer_set_output(mbuf, buffer + 1, sizeof(buffer) - 1); + + int err = noise_handshakestate_write_message(this->handshake_, &mbuf, nullptr); + APIError aerr = this->handle_noise_error_(err, LOG_STR("noise_handshakestate_write_message"), + APIError::HANDSHAKESTATE_WRITE_FAILED); + if (aerr != APIError::OK) + return aerr; + buffer[0] = 0x00; // success + + aerr = this->write_frame_(buffer, mbuf.size + 1); + if (aerr != APIError::OK) + return aerr; + return this->check_handshake_finished_(); +} void APINoiseFrameHelper::send_explicit_handshake_reject_(const LogString *reason) { // Max reject message: "Bad handshake packet len" (24) + 1 (failure byte) = 25 bytes uint8_t data[32]; diff --git a/esphome/components/api/api_frame_helper_noise.h b/esphome/components/api/api_frame_helper_noise.h index a6b17ff3b9..f44bde0755 100644 --- a/esphome/components/api/api_frame_helper_noise.h +++ b/esphome/components/api/api_frame_helper_noise.h @@ -26,6 +26,11 @@ class APINoiseFrameHelper final : public APIFrameHelper { protected: APIError state_action_(); + APIError state_action_client_hello_(); + APIError state_action_server_hello_(); + APIError state_action_handshake_(); + APIError state_action_handshake_read_(); + APIError state_action_handshake_write_(); APIError try_read_frame_(); APIError write_frame_(const uint8_t *data, uint16_t len); APIError init_handshake_(); diff --git a/esphome/components/api/api_options.proto b/esphome/components/api/api_options.proto index 02600f0977..0f71268d70 100644 --- a/esphome/components/api/api_options.proto +++ b/esphome/components/api/api_options.proto @@ -96,4 +96,16 @@ extend google.protobuf.FieldOptions { // 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]; + + // max_value: Maximum value a field can have. + // When max_value < 128, the code generator emits constant-size calculations + // and direct byte writes instead of varint branching, since the encoded varint + // is guaranteed to be 1 byte. + optional uint32 max_value = 50017; + + // max_data_length: Maximum length of a string or bytes field. + // When max_data_length < 128, the code generator emits constant-size + // length varint calculations and direct byte writes, since the length + // varint is guaranteed to be 1 byte. + optional uint32 max_data_length = 50018; } diff --git a/esphome/components/api/api_pb2.cpp b/esphome/components/api/api_pb2.cpp index ae2cd2bae8..f7c68b95a7 100644 --- a/esphome/components/api/api_pb2.cpp +++ b/esphome/components/api/api_pb2.cpp @@ -31,11 +31,13 @@ bool HelloRequest::decode_length(uint32_t field_id, ProtoLengthDelimited value) } return true; } -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); +uint8_t *HelloResponse::encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const { + uint8_t *__restrict__ pos = buffer.get_pos(); + ProtoEncode::encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, 1, this->api_version_major); + ProtoEncode::encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, 2, this->api_version_minor); + ProtoEncode::encode_string(pos PROTO_ENCODE_DEBUG_ARG, 3, this->server_info); + ProtoEncode::encode_string(pos PROTO_ENCODE_DEBUG_ARG, 4, this->name); + return pos; } uint32_t HelloResponse::calculate_size() const { uint32_t size = 0; @@ -46,9 +48,11 @@ uint32_t HelloResponse::calculate_size() const { return size; } #ifdef USE_AREAS -void AreaInfo::encode(ProtoWriteBuffer &buffer) const { - buffer.encode_uint32(1, this->area_id); - buffer.encode_string(2, this->name); +uint8_t *AreaInfo::encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const { + uint8_t *__restrict__ pos = buffer.get_pos(); + ProtoEncode::encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, 1, this->area_id); + ProtoEncode::encode_string(pos PROTO_ENCODE_DEBUG_ARG, 2, this->name); + return pos; } uint32_t AreaInfo::calculate_size() const { uint32_t size = 0; @@ -58,10 +62,12 @@ uint32_t AreaInfo::calculate_size() const { } #endif #ifdef USE_DEVICES -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); +uint8_t *DeviceInfo::encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const { + uint8_t *__restrict__ pos = buffer.get_pos(); + ProtoEncode::encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, 1, this->device_id); + ProtoEncode::encode_string(pos PROTO_ENCODE_DEBUG_ARG, 2, this->name); + ProtoEncode::encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, 3, this->area_id); + return pos; } uint32_t DeviceInfo::calculate_size() const { uint32_t size = 0; @@ -72,76 +78,80 @@ uint32_t DeviceInfo::calculate_size() const { } #endif #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)); +uint8_t *SerialProxyInfo::encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const { + uint8_t *__restrict__ pos = buffer.get_pos(); + ProtoEncode::encode_string(pos PROTO_ENCODE_DEBUG_ARG, 1, this->name); + ProtoEncode::encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, 2, static_cast(this->port_type)); + return pos; } 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)); + size += this->port_type ? 2 : 0; 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); - buffer.encode_string(5, this->compilation_time); - buffer.encode_string(6, this->model); +uint8_t *DeviceInfoResponse::encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const { + uint8_t *__restrict__ pos = buffer.get_pos(); + ProtoEncode::encode_string(pos PROTO_ENCODE_DEBUG_ARG, 2, this->name); + ProtoEncode::encode_string(pos PROTO_ENCODE_DEBUG_ARG, 3, this->mac_address); + ProtoEncode::encode_string(pos PROTO_ENCODE_DEBUG_ARG, 4, this->esphome_version); + ProtoEncode::encode_string(pos PROTO_ENCODE_DEBUG_ARG, 5, this->compilation_time); + ProtoEncode::encode_string(pos PROTO_ENCODE_DEBUG_ARG, 6, this->model); #ifdef USE_DEEP_SLEEP - buffer.encode_bool(7, this->has_deep_sleep); + ProtoEncode::encode_bool(pos PROTO_ENCODE_DEBUG_ARG, 7, this->has_deep_sleep); #endif #ifdef ESPHOME_PROJECT_NAME - buffer.encode_string(8, this->project_name); + ProtoEncode::encode_string(pos PROTO_ENCODE_DEBUG_ARG, 8, this->project_name); #endif #ifdef ESPHOME_PROJECT_NAME - buffer.encode_string(9, this->project_version); + ProtoEncode::encode_string(pos PROTO_ENCODE_DEBUG_ARG, 9, this->project_version); #endif #ifdef USE_WEBSERVER - buffer.encode_uint32(10, this->webserver_port); + ProtoEncode::encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, 10, this->webserver_port); #endif #ifdef USE_BLUETOOTH_PROXY - buffer.encode_uint32(15, this->bluetooth_proxy_feature_flags); + ProtoEncode::encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, 15, this->bluetooth_proxy_feature_flags); #endif - buffer.encode_string(12, this->manufacturer); - buffer.encode_string(13, this->friendly_name); + ProtoEncode::encode_string(pos PROTO_ENCODE_DEBUG_ARG, 12, this->manufacturer); + ProtoEncode::encode_string(pos PROTO_ENCODE_DEBUG_ARG, 13, this->friendly_name); #ifdef USE_VOICE_ASSISTANT - buffer.encode_uint32(17, this->voice_assistant_feature_flags); + ProtoEncode::encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, 17, this->voice_assistant_feature_flags); #endif #ifdef USE_AREAS - buffer.encode_string(16, this->suggested_area); + ProtoEncode::encode_string(pos PROTO_ENCODE_DEBUG_ARG, 16, this->suggested_area); #endif #ifdef USE_BLUETOOTH_PROXY - buffer.encode_string(18, this->bluetooth_mac_address); + ProtoEncode::encode_string(pos PROTO_ENCODE_DEBUG_ARG, 18, this->bluetooth_mac_address); #endif #ifdef USE_API_NOISE - buffer.encode_bool(19, this->api_encryption_supported); + ProtoEncode::encode_bool(pos PROTO_ENCODE_DEBUG_ARG, 19, this->api_encryption_supported); #endif #ifdef USE_DEVICES for (const auto &it : this->devices) { - buffer.encode_sub_message(20, it); + ProtoEncode::encode_sub_message(pos PROTO_ENCODE_DEBUG_ARG, buffer, 20, it); } #endif #ifdef USE_AREAS for (const auto &it : this->areas) { - buffer.encode_sub_message(21, it); + ProtoEncode::encode_sub_message(pos PROTO_ENCODE_DEBUG_ARG, buffer, 21, it); } #endif #ifdef USE_AREAS - buffer.encode_optional_sub_message(22, this->area); + ProtoEncode::encode_optional_sub_message(pos PROTO_ENCODE_DEBUG_ARG, buffer, 22, this->area); #endif #ifdef USE_ZWAVE_PROXY - buffer.encode_uint32(23, this->zwave_proxy_feature_flags); + ProtoEncode::encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, 23, this->zwave_proxy_feature_flags); #endif #ifdef USE_ZWAVE_PROXY - buffer.encode_uint32(24, this->zwave_home_id); + ProtoEncode::encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, 24, this->zwave_home_id); #endif #ifdef USE_SERIAL_PROXY for (const auto &it : this->serial_proxies) { - buffer.encode_sub_message(25, it); + ProtoEncode::encode_sub_message(pos PROTO_ENCODE_DEBUG_ARG, buffer, 25, it); } #endif + return pos; } uint32_t DeviceInfoResponse::calculate_size() const { uint32_t size = 0; @@ -206,45 +216,49 @@ uint32_t DeviceInfoResponse::calculate_size() const { return size; } #ifdef USE_BINARY_SENSOR -void ListEntitiesBinarySensorResponse::encode(ProtoWriteBuffer &buffer) const { - buffer.encode_string(1, this->object_id); - 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); - buffer.encode_bool(7, this->disabled_by_default); +uint8_t *ListEntitiesBinarySensorResponse::encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const { + uint8_t *__restrict__ pos = buffer.get_pos(); + ProtoEncode::encode_short_string_force(pos PROTO_ENCODE_DEBUG_ARG, 10, this->object_id); + ProtoEncode::write_tag_and_fixed32(pos PROTO_ENCODE_DEBUG_ARG, 21, this->key); + ProtoEncode::encode_short_string_force(pos PROTO_ENCODE_DEBUG_ARG, 26, this->name); + ProtoEncode::encode_string(pos PROTO_ENCODE_DEBUG_ARG, 5, this->device_class); + ProtoEncode::encode_bool(pos PROTO_ENCODE_DEBUG_ARG, 6, this->is_status_binary_sensor); + ProtoEncode::encode_bool(pos PROTO_ENCODE_DEBUG_ARG, 7, this->disabled_by_default); #ifdef USE_ENTITY_ICON - buffer.encode_string(8, this->icon); + ProtoEncode::encode_string(pos PROTO_ENCODE_DEBUG_ARG, 8, this->icon); #endif - buffer.encode_uint32(9, static_cast(this->entity_category)); + ProtoEncode::encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, 9, static_cast(this->entity_category)); #ifdef USE_DEVICES - buffer.encode_uint32(10, this->device_id); + ProtoEncode::encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, 10, this->device_id); #endif + return pos; } uint32_t ListEntitiesBinarySensorResponse::calculate_size() const { uint32_t size = 0; - size += ProtoSize::calc_length(1, this->object_id.size()); + size += 2 + this->object_id.size(); size += 5; - size += ProtoSize::calc_length(1, this->name.size()); - size += ProtoSize::calc_length(1, this->device_class.size()); + size += 2 + this->name.size(); + size += !this->device_class.empty() ? 2 + this->device_class.size() : 0; size += ProtoSize::calc_bool(1, this->is_status_binary_sensor); size += ProtoSize::calc_bool(1, this->disabled_by_default); #ifdef USE_ENTITY_ICON - size += ProtoSize::calc_length(1, this->icon.size()); + size += !this->icon.empty() ? 2 + this->icon.size() : 0; #endif - size += ProtoSize::calc_uint32(1, static_cast(this->entity_category)); + size += this->entity_category ? 2 : 0; #ifdef USE_DEVICES size += ProtoSize::calc_uint32(1, this->device_id); #endif return size; } -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); +uint8_t *BinarySensorStateResponse::encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const { + uint8_t *__restrict__ pos = buffer.get_pos(); + ProtoEncode::write_tag_and_fixed32(pos PROTO_ENCODE_DEBUG_ARG, 13, this->key); + ProtoEncode::encode_bool(pos PROTO_ENCODE_DEBUG_ARG, 2, this->state); + ProtoEncode::encode_bool(pos PROTO_ENCODE_DEBUG_ARG, 3, this->missing_state); #ifdef USE_DEVICES - buffer.encode_uint32(4, this->device_id); + ProtoEncode::encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, 4, this->device_id); #endif + return pos; } uint32_t BinarySensorStateResponse::calculate_size() const { uint32_t size = 0; @@ -258,59 +272,63 @@ uint32_t BinarySensorStateResponse::calculate_size() const { } #endif #ifdef USE_COVER -void ListEntitiesCoverResponse::encode(ProtoWriteBuffer &buffer) const { - buffer.encode_string(1, this->object_id); - 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); - buffer.encode_bool(7, this->supports_tilt); - buffer.encode_string(8, this->device_class); - buffer.encode_bool(9, this->disabled_by_default); +uint8_t *ListEntitiesCoverResponse::encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const { + uint8_t *__restrict__ pos = buffer.get_pos(); + ProtoEncode::encode_short_string_force(pos PROTO_ENCODE_DEBUG_ARG, 10, this->object_id); + ProtoEncode::write_tag_and_fixed32(pos PROTO_ENCODE_DEBUG_ARG, 21, this->key); + ProtoEncode::encode_short_string_force(pos PROTO_ENCODE_DEBUG_ARG, 26, this->name); + ProtoEncode::encode_bool(pos PROTO_ENCODE_DEBUG_ARG, 5, this->assumed_state); + ProtoEncode::encode_bool(pos PROTO_ENCODE_DEBUG_ARG, 6, this->supports_position); + ProtoEncode::encode_bool(pos PROTO_ENCODE_DEBUG_ARG, 7, this->supports_tilt); + ProtoEncode::encode_string(pos PROTO_ENCODE_DEBUG_ARG, 8, this->device_class); + ProtoEncode::encode_bool(pos PROTO_ENCODE_DEBUG_ARG, 9, this->disabled_by_default); #ifdef USE_ENTITY_ICON - buffer.encode_string(10, this->icon); + ProtoEncode::encode_string(pos PROTO_ENCODE_DEBUG_ARG, 10, this->icon); #endif - buffer.encode_uint32(11, static_cast(this->entity_category)); - buffer.encode_bool(12, this->supports_stop); + ProtoEncode::encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, 11, static_cast(this->entity_category)); + ProtoEncode::encode_bool(pos PROTO_ENCODE_DEBUG_ARG, 12, this->supports_stop); #ifdef USE_DEVICES - buffer.encode_uint32(13, this->device_id); + ProtoEncode::encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, 13, this->device_id); #endif + return pos; } uint32_t ListEntitiesCoverResponse::calculate_size() const { uint32_t size = 0; - size += ProtoSize::calc_length(1, this->object_id.size()); + size += 2 + this->object_id.size(); size += 5; - size += ProtoSize::calc_length(1, this->name.size()); + size += 2 + 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 += !this->device_class.empty() ? 2 + this->device_class.size() : 0; size += ProtoSize::calc_bool(1, this->disabled_by_default); #ifdef USE_ENTITY_ICON - size += ProtoSize::calc_length(1, this->icon.size()); + size += !this->icon.empty() ? 2 + this->icon.size() : 0; #endif - size += ProtoSize::calc_uint32(1, static_cast(this->entity_category)); + size += this->entity_category ? 2 : 0; size += ProtoSize::calc_bool(1, this->supports_stop); #ifdef USE_DEVICES size += ProtoSize::calc_uint32(1, this->device_id); #endif return size; } -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)); +uint8_t *CoverStateResponse::encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const { + uint8_t *__restrict__ pos = buffer.get_pos(); + ProtoEncode::write_tag_and_fixed32(pos PROTO_ENCODE_DEBUG_ARG, 13, this->key); + ProtoEncode::encode_float(pos PROTO_ENCODE_DEBUG_ARG, 3, this->position); + ProtoEncode::encode_float(pos PROTO_ENCODE_DEBUG_ARG, 4, this->tilt); + ProtoEncode::encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, 5, static_cast(this->current_operation)); #ifdef USE_DEVICES - buffer.encode_uint32(6, this->device_id); + ProtoEncode::encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, 6, this->device_id); #endif + return pos; } 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)); + size += this->current_operation ? 2 : 0; #ifdef USE_DEVICES size += ProtoSize::calc_uint32(1, this->device_id); #endif @@ -355,40 +373,42 @@ bool CoverCommandRequest::decode_32bit(uint32_t field_id, Proto32Bit value) { } #endif #ifdef USE_FAN -void ListEntitiesFanResponse::encode(ProtoWriteBuffer &buffer) const { - buffer.encode_string(1, this->object_id); - 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); - buffer.encode_bool(7, this->supports_direction); - buffer.encode_int32(8, this->supported_speed_count); - buffer.encode_bool(9, this->disabled_by_default); +uint8_t *ListEntitiesFanResponse::encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const { + uint8_t *__restrict__ pos = buffer.get_pos(); + ProtoEncode::encode_short_string_force(pos PROTO_ENCODE_DEBUG_ARG, 10, this->object_id); + ProtoEncode::write_tag_and_fixed32(pos PROTO_ENCODE_DEBUG_ARG, 21, this->key); + ProtoEncode::encode_short_string_force(pos PROTO_ENCODE_DEBUG_ARG, 26, this->name); + ProtoEncode::encode_bool(pos PROTO_ENCODE_DEBUG_ARG, 5, this->supports_oscillation); + ProtoEncode::encode_bool(pos PROTO_ENCODE_DEBUG_ARG, 6, this->supports_speed); + ProtoEncode::encode_bool(pos PROTO_ENCODE_DEBUG_ARG, 7, this->supports_direction); + ProtoEncode::encode_int32(pos PROTO_ENCODE_DEBUG_ARG, 8, this->supported_speed_count); + ProtoEncode::encode_bool(pos PROTO_ENCODE_DEBUG_ARG, 9, this->disabled_by_default); #ifdef USE_ENTITY_ICON - buffer.encode_string(10, this->icon); + ProtoEncode::encode_string(pos PROTO_ENCODE_DEBUG_ARG, 10, this->icon); #endif - buffer.encode_uint32(11, static_cast(this->entity_category)); + ProtoEncode::encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, 11, static_cast(this->entity_category)); for (const char *it : *this->supported_preset_modes) { - buffer.encode_string(12, it, strlen(it), true); + ProtoEncode::encode_string(pos PROTO_ENCODE_DEBUG_ARG, 12, it, strlen(it), true); } #ifdef USE_DEVICES - buffer.encode_uint32(13, this->device_id); + ProtoEncode::encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, 13, this->device_id); #endif + return pos; } uint32_t ListEntitiesFanResponse::calculate_size() const { uint32_t size = 0; - size += ProtoSize::calc_length(1, this->object_id.size()); + size += 2 + this->object_id.size(); size += 5; - size += ProtoSize::calc_length(1, this->name.size()); + size += 2 + 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 += ProtoSize::calc_length(1, this->icon.size()); + size += !this->icon.empty() ? 2 + this->icon.size() : 0; #endif - size += ProtoSize::calc_uint32(1, static_cast(this->entity_category)); + size += this->entity_category ? 2 : 0; if (!this->supported_preset_modes->empty()) { for (const char *it : *this->supported_preset_modes) { size += ProtoSize::calc_length_force(1, strlen(it)); @@ -399,23 +419,25 @@ uint32_t ListEntitiesFanResponse::calculate_size() const { #endif return size; } -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)); - buffer.encode_int32(6, this->speed_level); - buffer.encode_string(7, this->preset_mode); +uint8_t *FanStateResponse::encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const { + uint8_t *__restrict__ pos = buffer.get_pos(); + ProtoEncode::write_tag_and_fixed32(pos PROTO_ENCODE_DEBUG_ARG, 13, this->key); + ProtoEncode::encode_bool(pos PROTO_ENCODE_DEBUG_ARG, 2, this->state); + ProtoEncode::encode_bool(pos PROTO_ENCODE_DEBUG_ARG, 3, this->oscillating); + ProtoEncode::encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, 5, static_cast(this->direction)); + ProtoEncode::encode_int32(pos PROTO_ENCODE_DEBUG_ARG, 6, this->speed_level); + ProtoEncode::encode_string(pos PROTO_ENCODE_DEBUG_ARG, 7, this->preset_mode); #ifdef USE_DEVICES - buffer.encode_uint32(8, this->device_id); + ProtoEncode::encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, 8, this->device_id); #endif + return pos; } 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 += this->direction ? 2 : 0; size += ProtoSize::calc_int32(1, this->speed_level); size += ProtoSize::calc_length(1, this->preset_mode.size()); #ifdef USE_DEVICES @@ -485,36 +507,36 @@ bool FanCommandRequest::decode_32bit(uint32_t field_id, Proto32Bit value) { } #endif #ifdef USE_LIGHT -void ListEntitiesLightResponse::encode(ProtoWriteBuffer &buffer) const { - buffer.encode_string(1, this->object_id); - buffer.write_tag_and_fixed32(21, this->key); - buffer.encode_string(3, this->name); +uint8_t *ListEntitiesLightResponse::encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const { + uint8_t *__restrict__ pos = buffer.get_pos(); + ProtoEncode::encode_short_string_force(pos PROTO_ENCODE_DEBUG_ARG, 10, this->object_id); + ProtoEncode::write_tag_and_fixed32(pos PROTO_ENCODE_DEBUG_ARG, 21, this->key); + ProtoEncode::encode_short_string_force(pos PROTO_ENCODE_DEBUG_ARG, 26, this->name); for (const auto &it : *this->supported_color_modes) { - buffer.encode_uint32(12, static_cast(it), true); + ProtoEncode::encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, 12, static_cast(it), true); } - buffer.encode_float(9, this->min_mireds); - buffer.encode_float(10, this->max_mireds); + ProtoEncode::encode_float(pos PROTO_ENCODE_DEBUG_ARG, 9, this->min_mireds); + ProtoEncode::encode_float(pos PROTO_ENCODE_DEBUG_ARG, 10, this->max_mireds); for (const char *it : *this->effects) { - buffer.encode_string(11, it, strlen(it), true); + ProtoEncode::encode_string(pos PROTO_ENCODE_DEBUG_ARG, 11, it, strlen(it), true); } - buffer.encode_bool(13, this->disabled_by_default); + ProtoEncode::encode_bool(pos PROTO_ENCODE_DEBUG_ARG, 13, this->disabled_by_default); #ifdef USE_ENTITY_ICON - buffer.encode_string(14, this->icon); + ProtoEncode::encode_string(pos PROTO_ENCODE_DEBUG_ARG, 14, this->icon); #endif - buffer.encode_uint32(15, static_cast(this->entity_category)); + ProtoEncode::encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, 15, static_cast(this->entity_category)); #ifdef USE_DEVICES - buffer.encode_uint32(16, this->device_id); + ProtoEncode::encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, 16, this->device_id); #endif + return pos; } uint32_t ListEntitiesLightResponse::calculate_size() const { uint32_t size = 0; - size += ProtoSize::calc_length(1, this->object_id.size()); + size += 2 + this->object_id.size(); size += 5; - size += ProtoSize::calc_length(1, this->name.size()); + size += 2 + this->name.size(); if (!this->supported_color_modes->empty()) { - for (const auto &it : *this->supported_color_modes) { - size += ProtoSize::calc_uint32_force(1, static_cast(it)); - } + size += this->supported_color_modes->size() * 2; } size += ProtoSize::calc_float(1, this->min_mireds); size += ProtoSize::calc_float(1, this->max_mireds); @@ -525,38 +547,40 @@ uint32_t ListEntitiesLightResponse::calculate_size() const { } size += ProtoSize::calc_bool(1, this->disabled_by_default); #ifdef USE_ENTITY_ICON - size += ProtoSize::calc_length(1, this->icon.size()); + size += !this->icon.empty() ? 2 + this->icon.size() : 0; #endif - size += ProtoSize::calc_uint32(1, static_cast(this->entity_category)); + size += this->entity_category ? 2 : 0; #ifdef USE_DEVICES size += ProtoSize::calc_uint32(2, this->device_id); #endif return size; } -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)); - buffer.encode_float(10, this->color_brightness); - buffer.encode_float(4, this->red); - buffer.encode_float(5, this->green); - buffer.encode_float(6, this->blue); - buffer.encode_float(7, this->white); - buffer.encode_float(8, this->color_temperature); - buffer.encode_float(12, this->cold_white); - buffer.encode_float(13, this->warm_white); - buffer.encode_string(9, this->effect); +uint8_t *LightStateResponse::encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const { + uint8_t *__restrict__ pos = buffer.get_pos(); + ProtoEncode::write_tag_and_fixed32(pos PROTO_ENCODE_DEBUG_ARG, 13, this->key); + ProtoEncode::encode_bool(pos PROTO_ENCODE_DEBUG_ARG, 2, this->state); + ProtoEncode::encode_float(pos PROTO_ENCODE_DEBUG_ARG, 3, this->brightness); + ProtoEncode::encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, 11, static_cast(this->color_mode)); + ProtoEncode::encode_float(pos PROTO_ENCODE_DEBUG_ARG, 10, this->color_brightness); + ProtoEncode::encode_float(pos PROTO_ENCODE_DEBUG_ARG, 4, this->red); + ProtoEncode::encode_float(pos PROTO_ENCODE_DEBUG_ARG, 5, this->green); + ProtoEncode::encode_float(pos PROTO_ENCODE_DEBUG_ARG, 6, this->blue); + ProtoEncode::encode_float(pos PROTO_ENCODE_DEBUG_ARG, 7, this->white); + ProtoEncode::encode_float(pos PROTO_ENCODE_DEBUG_ARG, 8, this->color_temperature); + ProtoEncode::encode_float(pos PROTO_ENCODE_DEBUG_ARG, 12, this->cold_white); + ProtoEncode::encode_float(pos PROTO_ENCODE_DEBUG_ARG, 13, this->warm_white); + ProtoEncode::encode_string(pos PROTO_ENCODE_DEBUG_ARG, 9, this->effect); #ifdef USE_DEVICES - buffer.encode_uint32(14, this->device_id); + ProtoEncode::encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, 14, this->device_id); #endif + return pos; } 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 += this->color_mode ? 2 : 0; size += ProtoSize::calc_float(1, this->color_brightness); size += ProtoSize::calc_float(1, this->red); size += ProtoSize::calc_float(1, this->green); @@ -681,51 +705,55 @@ bool LightCommandRequest::decode_32bit(uint32_t field_id, Proto32Bit value) { } #endif #ifdef USE_SENSOR -void ListEntitiesSensorResponse::encode(ProtoWriteBuffer &buffer) const { - buffer.encode_string(1, this->object_id); - buffer.write_tag_and_fixed32(21, this->key); - buffer.encode_string(3, this->name); +uint8_t *ListEntitiesSensorResponse::encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const { + uint8_t *__restrict__ pos = buffer.get_pos(); + ProtoEncode::encode_short_string_force(pos PROTO_ENCODE_DEBUG_ARG, 10, this->object_id); + ProtoEncode::write_tag_and_fixed32(pos PROTO_ENCODE_DEBUG_ARG, 21, this->key); + ProtoEncode::encode_short_string_force(pos PROTO_ENCODE_DEBUG_ARG, 26, this->name); #ifdef USE_ENTITY_ICON - buffer.encode_string(5, this->icon); + ProtoEncode::encode_string(pos PROTO_ENCODE_DEBUG_ARG, 5, this->icon); #endif - buffer.encode_string(6, this->unit_of_measurement); - buffer.encode_int32(7, this->accuracy_decimals); - buffer.encode_bool(8, this->force_update); - buffer.encode_string(9, this->device_class); - buffer.encode_uint32(10, static_cast(this->state_class)); - buffer.encode_bool(12, this->disabled_by_default); - buffer.encode_uint32(13, static_cast(this->entity_category)); + ProtoEncode::encode_string(pos PROTO_ENCODE_DEBUG_ARG, 6, this->unit_of_measurement); + ProtoEncode::encode_int32(pos PROTO_ENCODE_DEBUG_ARG, 7, this->accuracy_decimals); + ProtoEncode::encode_bool(pos PROTO_ENCODE_DEBUG_ARG, 8, this->force_update); + ProtoEncode::encode_string(pos PROTO_ENCODE_DEBUG_ARG, 9, this->device_class); + ProtoEncode::encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, 10, static_cast(this->state_class)); + ProtoEncode::encode_bool(pos PROTO_ENCODE_DEBUG_ARG, 12, this->disabled_by_default); + ProtoEncode::encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, 13, static_cast(this->entity_category)); #ifdef USE_DEVICES - buffer.encode_uint32(14, this->device_id); + ProtoEncode::encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, 14, this->device_id); #endif + return pos; } uint32_t ListEntitiesSensorResponse::calculate_size() const { uint32_t size = 0; - size += ProtoSize::calc_length(1, this->object_id.size()); + size += 2 + this->object_id.size(); size += 5; - size += ProtoSize::calc_length(1, this->name.size()); + size += 2 + this->name.size(); #ifdef USE_ENTITY_ICON - size += ProtoSize::calc_length(1, this->icon.size()); + size += !this->icon.empty() ? 2 + this->icon.size() : 0; #endif - size += ProtoSize::calc_length(1, this->unit_of_measurement.size()); + size += !this->unit_of_measurement.empty() ? 2 + this->unit_of_measurement.size() : 0; 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 += !this->device_class.empty() ? 2 + this->device_class.size() : 0; + size += this->state_class ? 2 : 0; size += ProtoSize::calc_bool(1, this->disabled_by_default); - size += ProtoSize::calc_uint32(1, static_cast(this->entity_category)); + size += this->entity_category ? 2 : 0; #ifdef USE_DEVICES size += ProtoSize::calc_uint32(1, this->device_id); #endif return size; } -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); +uint8_t *SensorStateResponse::encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const { + uint8_t *__restrict__ pos = buffer.get_pos(); + ProtoEncode::write_tag_and_fixed32(pos PROTO_ENCODE_DEBUG_ARG, 13, this->key); + ProtoEncode::encode_float(pos PROTO_ENCODE_DEBUG_ARG, 2, this->state); + ProtoEncode::encode_bool(pos PROTO_ENCODE_DEBUG_ARG, 3, this->missing_state); #ifdef USE_DEVICES - buffer.encode_uint32(4, this->device_id); + ProtoEncode::encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, 4, this->device_id); #endif + return pos; } uint32_t SensorStateResponse::calculate_size() const { uint32_t size = 0; @@ -739,44 +767,48 @@ uint32_t SensorStateResponse::calculate_size() const { } #endif #ifdef USE_SWITCH -void ListEntitiesSwitchResponse::encode(ProtoWriteBuffer &buffer) const { - buffer.encode_string(1, this->object_id); - buffer.write_tag_and_fixed32(21, this->key); - buffer.encode_string(3, this->name); +uint8_t *ListEntitiesSwitchResponse::encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const { + uint8_t *__restrict__ pos = buffer.get_pos(); + ProtoEncode::encode_short_string_force(pos PROTO_ENCODE_DEBUG_ARG, 10, this->object_id); + ProtoEncode::write_tag_and_fixed32(pos PROTO_ENCODE_DEBUG_ARG, 21, this->key); + ProtoEncode::encode_short_string_force(pos PROTO_ENCODE_DEBUG_ARG, 26, this->name); #ifdef USE_ENTITY_ICON - buffer.encode_string(5, this->icon); + ProtoEncode::encode_string(pos PROTO_ENCODE_DEBUG_ARG, 5, this->icon); #endif - buffer.encode_bool(6, this->assumed_state); - buffer.encode_bool(7, this->disabled_by_default); - buffer.encode_uint32(8, static_cast(this->entity_category)); - buffer.encode_string(9, this->device_class); + ProtoEncode::encode_bool(pos PROTO_ENCODE_DEBUG_ARG, 6, this->assumed_state); + ProtoEncode::encode_bool(pos PROTO_ENCODE_DEBUG_ARG, 7, this->disabled_by_default); + ProtoEncode::encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, 8, static_cast(this->entity_category)); + ProtoEncode::encode_string(pos PROTO_ENCODE_DEBUG_ARG, 9, this->device_class); #ifdef USE_DEVICES - buffer.encode_uint32(10, this->device_id); + ProtoEncode::encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, 10, this->device_id); #endif + return pos; } uint32_t ListEntitiesSwitchResponse::calculate_size() const { uint32_t size = 0; - size += ProtoSize::calc_length(1, this->object_id.size()); + size += 2 + this->object_id.size(); size += 5; - size += ProtoSize::calc_length(1, this->name.size()); + size += 2 + this->name.size(); #ifdef USE_ENTITY_ICON - size += ProtoSize::calc_length(1, this->icon.size()); + size += !this->icon.empty() ? 2 + this->icon.size() : 0; #endif 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()); + size += this->entity_category ? 2 : 0; + size += !this->device_class.empty() ? 2 + this->device_class.size() : 0; #ifdef USE_DEVICES size += ProtoSize::calc_uint32(1, this->device_id); #endif return size; } -void SwitchStateResponse::encode(ProtoWriteBuffer &buffer) const { - buffer.write_tag_and_fixed32(13, this->key); - buffer.encode_bool(2, this->state); +uint8_t *SwitchStateResponse::encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const { + uint8_t *__restrict__ pos = buffer.get_pos(); + ProtoEncode::write_tag_and_fixed32(pos PROTO_ENCODE_DEBUG_ARG, 13, this->key); + ProtoEncode::encode_bool(pos PROTO_ENCODE_DEBUG_ARG, 2, this->state); #ifdef USE_DEVICES - buffer.encode_uint32(3, this->device_id); + ProtoEncode::encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, 3, this->device_id); #endif + return pos; } uint32_t SwitchStateResponse::calculate_size() const { uint32_t size = 0; @@ -814,43 +846,47 @@ bool SwitchCommandRequest::decode_32bit(uint32_t field_id, Proto32Bit value) { } #endif #ifdef USE_TEXT_SENSOR -void ListEntitiesTextSensorResponse::encode(ProtoWriteBuffer &buffer) const { - buffer.encode_string(1, this->object_id); - buffer.write_tag_and_fixed32(21, this->key); - buffer.encode_string(3, this->name); +uint8_t *ListEntitiesTextSensorResponse::encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const { + uint8_t *__restrict__ pos = buffer.get_pos(); + ProtoEncode::encode_short_string_force(pos PROTO_ENCODE_DEBUG_ARG, 10, this->object_id); + ProtoEncode::write_tag_and_fixed32(pos PROTO_ENCODE_DEBUG_ARG, 21, this->key); + ProtoEncode::encode_short_string_force(pos PROTO_ENCODE_DEBUG_ARG, 26, this->name); #ifdef USE_ENTITY_ICON - buffer.encode_string(5, this->icon); + ProtoEncode::encode_string(pos PROTO_ENCODE_DEBUG_ARG, 5, this->icon); #endif - buffer.encode_bool(6, this->disabled_by_default); - buffer.encode_uint32(7, static_cast(this->entity_category)); - buffer.encode_string(8, this->device_class); + ProtoEncode::encode_bool(pos PROTO_ENCODE_DEBUG_ARG, 6, this->disabled_by_default); + ProtoEncode::encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, 7, static_cast(this->entity_category)); + ProtoEncode::encode_string(pos PROTO_ENCODE_DEBUG_ARG, 8, this->device_class); #ifdef USE_DEVICES - buffer.encode_uint32(9, this->device_id); + ProtoEncode::encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, 9, this->device_id); #endif + return pos; } uint32_t ListEntitiesTextSensorResponse::calculate_size() const { uint32_t size = 0; - size += ProtoSize::calc_length(1, this->object_id.size()); + size += 2 + this->object_id.size(); size += 5; - size += ProtoSize::calc_length(1, this->name.size()); + size += 2 + this->name.size(); #ifdef USE_ENTITY_ICON - size += ProtoSize::calc_length(1, this->icon.size()); + size += !this->icon.empty() ? 2 + this->icon.size() : 0; #endif 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 += this->entity_category ? 2 : 0; + size += !this->device_class.empty() ? 2 + this->device_class.size() : 0; #ifdef USE_DEVICES size += ProtoSize::calc_uint32(1, this->device_id); #endif return size; } -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); +uint8_t *TextSensorStateResponse::encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const { + uint8_t *__restrict__ pos = buffer.get_pos(); + ProtoEncode::write_tag_and_fixed32(pos PROTO_ENCODE_DEBUG_ARG, 13, this->key); + ProtoEncode::encode_string(pos PROTO_ENCODE_DEBUG_ARG, 2, this->state); + ProtoEncode::encode_bool(pos PROTO_ENCODE_DEBUG_ARG, 3, this->missing_state); #ifdef USE_DEVICES - buffer.encode_uint32(4, this->device_id); + ProtoEncode::encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, 4, this->device_id); #endif + return pos; } uint32_t TextSensorStateResponse::calculate_size() const { uint32_t size = 0; @@ -876,13 +912,15 @@ bool SubscribeLogsRequest::decode_varint(uint32_t field_id, proto_varint_value_t } return true; } -void SubscribeLogsResponse::encode(ProtoWriteBuffer &buffer) const { - buffer.encode_uint32(1, static_cast(this->level)); - buffer.encode_bytes(3, this->message_ptr_, this->message_len_); +uint8_t *SubscribeLogsResponse::encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const { + uint8_t *__restrict__ pos = buffer.get_pos(); + ProtoEncode::encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, 1, static_cast(this->level)); + ProtoEncode::encode_bytes(pos PROTO_ENCODE_DEBUG_ARG, 3, this->message_ptr_, this->message_len_); + return pos; } uint32_t SubscribeLogsResponse::calculate_size() const { uint32_t size = 0; - size += ProtoSize::calc_uint32(1, static_cast(this->level)); + size += this->level ? 2 : 0; size += ProtoSize::calc_length(1, this->message_len_); return size; } @@ -899,7 +937,11 @@ bool NoiseEncryptionSetKeyRequest::decode_length(uint32_t field_id, ProtoLengthD } return true; } -void NoiseEncryptionSetKeyResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_bool(1, this->success); } +uint8_t *NoiseEncryptionSetKeyResponse::encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const { + uint8_t *__restrict__ pos = buffer.get_pos(); + ProtoEncode::encode_bool(pos PROTO_ENCODE_DEBUG_ARG, 1, this->success); + return pos; +} uint32_t NoiseEncryptionSetKeyResponse::calculate_size() const { uint32_t size = 0; size += ProtoSize::calc_bool(1, this->success); @@ -907,9 +949,11 @@ uint32_t NoiseEncryptionSetKeyResponse::calculate_size() const { } #endif #ifdef USE_API_HOMEASSISTANT_SERVICES -void HomeassistantServiceMap::encode(ProtoWriteBuffer &buffer) const { - buffer.encode_string(1, this->key); - buffer.encode_string(2, this->value); +uint8_t *HomeassistantServiceMap::encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const { + uint8_t *__restrict__ pos = buffer.get_pos(); + ProtoEncode::encode_string(pos PROTO_ENCODE_DEBUG_ARG, 1, this->key); + ProtoEncode::encode_string(pos PROTO_ENCODE_DEBUG_ARG, 2, this->value); + return pos; } uint32_t HomeassistantServiceMap::calculate_size() const { uint32_t size = 0; @@ -917,27 +961,29 @@ uint32_t HomeassistantServiceMap::calculate_size() const { size += ProtoSize::calc_length(1, this->value.size()); return size; } -void HomeassistantActionRequest::encode(ProtoWriteBuffer &buffer) const { - buffer.encode_string(1, this->service); +uint8_t *HomeassistantActionRequest::encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const { + uint8_t *__restrict__ pos = buffer.get_pos(); + ProtoEncode::encode_string(pos PROTO_ENCODE_DEBUG_ARG, 1, this->service); for (auto &it : this->data) { - buffer.encode_sub_message(2, it); + ProtoEncode::encode_sub_message(pos PROTO_ENCODE_DEBUG_ARG, buffer, 2, it); } for (auto &it : this->data_template) { - buffer.encode_sub_message(3, it); + ProtoEncode::encode_sub_message(pos PROTO_ENCODE_DEBUG_ARG, buffer, 3, it); } for (auto &it : this->variables) { - buffer.encode_sub_message(4, it); + ProtoEncode::encode_sub_message(pos PROTO_ENCODE_DEBUG_ARG, buffer, 4, it); } - buffer.encode_bool(5, this->is_event); + ProtoEncode::encode_bool(pos PROTO_ENCODE_DEBUG_ARG, 5, this->is_event); #ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES - buffer.encode_uint32(6, this->call_id); + ProtoEncode::encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, 6, this->call_id); #endif #ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES_JSON - buffer.encode_bool(7, this->wants_response); + ProtoEncode::encode_bool(pos PROTO_ENCODE_DEBUG_ARG, 7, this->wants_response); #endif #ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES_JSON - buffer.encode_string(8, this->response_template); + ProtoEncode::encode_string(pos PROTO_ENCODE_DEBUG_ARG, 8, this->response_template); #endif + return pos; } uint32_t HomeassistantActionRequest::calculate_size() const { uint32_t size = 0; @@ -1004,10 +1050,12 @@ bool HomeassistantActionResponse::decode_length(uint32_t field_id, ProtoLengthDe } #endif #ifdef USE_API_HOMEASSISTANT_STATES -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); +uint8_t *SubscribeHomeAssistantStateResponse::encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const { + uint8_t *__restrict__ pos = buffer.get_pos(); + ProtoEncode::encode_string(pos PROTO_ENCODE_DEBUG_ARG, 1, this->entity_id); + ProtoEncode::encode_string(pos PROTO_ENCODE_DEBUG_ARG, 2, this->attribute); + ProtoEncode::encode_bool(pos PROTO_ENCODE_DEBUG_ARG, 3, this->once); + return pos; } uint32_t SubscribeHomeAssistantStateResponse::calculate_size() const { uint32_t size = 0; @@ -1112,23 +1160,27 @@ bool GetTimeResponse::decode_32bit(uint32_t field_id, Proto32Bit value) { return true; } #ifdef USE_API_USER_DEFINED_ACTIONS -void ListEntitiesServicesArgument::encode(ProtoWriteBuffer &buffer) const { - buffer.encode_string(1, this->name); - buffer.encode_uint32(2, static_cast(this->type)); +uint8_t *ListEntitiesServicesArgument::encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const { + uint8_t *__restrict__ pos = buffer.get_pos(); + ProtoEncode::encode_string(pos PROTO_ENCODE_DEBUG_ARG, 1, this->name); + ProtoEncode::encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, 2, static_cast(this->type)); + return pos; } 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)); + size += this->type ? 2 : 0; return size; } -void ListEntitiesServicesResponse::encode(ProtoWriteBuffer &buffer) const { - buffer.encode_string(1, this->name); - buffer.write_tag_and_fixed32(21, this->key); +uint8_t *ListEntitiesServicesResponse::encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const { + uint8_t *__restrict__ pos = buffer.get_pos(); + ProtoEncode::encode_string(pos PROTO_ENCODE_DEBUG_ARG, 1, this->name); + ProtoEncode::write_tag_and_fixed32(pos PROTO_ENCODE_DEBUG_ARG, 21, this->key); for (auto &it : this->args) { - buffer.encode_sub_message(3, it); + ProtoEncode::encode_sub_message(pos PROTO_ENCODE_DEBUG_ARG, buffer, 3, it); } - buffer.encode_uint32(4, static_cast(this->supports_response)); + ProtoEncode::encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, 4, static_cast(this->supports_response)); + return pos; } uint32_t ListEntitiesServicesResponse::calculate_size() const { uint32_t size = 0; @@ -1139,7 +1191,7 @@ uint32_t ListEntitiesServicesResponse::calculate_size() const { size += ProtoSize::calc_message_force(1, it.calculate_size()); } } - size += ProtoSize::calc_uint32(1, static_cast(this->supports_response)); + size += this->supports_response ? 2 : 0; return size; } bool ExecuteServiceArgument::decode_varint(uint32_t field_id, proto_varint_value_t value) { @@ -1247,13 +1299,15 @@ void ExecuteServiceRequest::decode(const uint8_t *buffer, size_t length) { } #endif #ifdef USE_API_USER_DEFINED_ACTION_RESPONSES -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); +uint8_t *ExecuteServiceResponse::encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const { + uint8_t *__restrict__ pos = buffer.get_pos(); + ProtoEncode::encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, 1, this->call_id); + ProtoEncode::encode_bool(pos PROTO_ENCODE_DEBUG_ARG, 2, this->success); + ProtoEncode::encode_string(pos PROTO_ENCODE_DEBUG_ARG, 3, this->error_message); #ifdef USE_API_USER_DEFINED_ACTION_RESPONSES_JSON - buffer.encode_bytes(4, this->response_data, this->response_data_len); + ProtoEncode::encode_bytes(pos PROTO_ENCODE_DEBUG_ARG, 4, this->response_data, this->response_data_len); #endif + return pos; } uint32_t ExecuteServiceResponse::calculate_size() const { uint32_t size = 0; @@ -1267,41 +1321,45 @@ uint32_t ExecuteServiceResponse::calculate_size() const { } #endif #ifdef USE_CAMERA -void ListEntitiesCameraResponse::encode(ProtoWriteBuffer &buffer) const { - buffer.encode_string(1, this->object_id); - buffer.write_tag_and_fixed32(21, this->key); - buffer.encode_string(3, this->name); - buffer.encode_bool(5, this->disabled_by_default); +uint8_t *ListEntitiesCameraResponse::encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const { + uint8_t *__restrict__ pos = buffer.get_pos(); + ProtoEncode::encode_short_string_force(pos PROTO_ENCODE_DEBUG_ARG, 10, this->object_id); + ProtoEncode::write_tag_and_fixed32(pos PROTO_ENCODE_DEBUG_ARG, 21, this->key); + ProtoEncode::encode_short_string_force(pos PROTO_ENCODE_DEBUG_ARG, 26, this->name); + ProtoEncode::encode_bool(pos PROTO_ENCODE_DEBUG_ARG, 5, this->disabled_by_default); #ifdef USE_ENTITY_ICON - buffer.encode_string(6, this->icon); + ProtoEncode::encode_string(pos PROTO_ENCODE_DEBUG_ARG, 6, this->icon); #endif - buffer.encode_uint32(7, static_cast(this->entity_category)); + ProtoEncode::encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, 7, static_cast(this->entity_category)); #ifdef USE_DEVICES - buffer.encode_uint32(8, this->device_id); + ProtoEncode::encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, 8, this->device_id); #endif + return pos; } uint32_t ListEntitiesCameraResponse::calculate_size() const { uint32_t size = 0; - size += ProtoSize::calc_length(1, this->object_id.size()); + size += 2 + this->object_id.size(); size += 5; - size += ProtoSize::calc_length(1, this->name.size()); + size += 2 + this->name.size(); size += ProtoSize::calc_bool(1, this->disabled_by_default); #ifdef USE_ENTITY_ICON - size += ProtoSize::calc_length(1, this->icon.size()); + size += !this->icon.empty() ? 2 + this->icon.size() : 0; #endif - size += ProtoSize::calc_uint32(1, static_cast(this->entity_category)); + size += this->entity_category ? 2 : 0; #ifdef USE_DEVICES size += ProtoSize::calc_uint32(1, this->device_id); #endif return size; } -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); +uint8_t *CameraImageResponse::encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const { + uint8_t *__restrict__ pos = buffer.get_pos(); + ProtoEncode::write_tag_and_fixed32(pos PROTO_ENCODE_DEBUG_ARG, 13, this->key); + ProtoEncode::encode_bytes(pos PROTO_ENCODE_DEBUG_ARG, 2, this->data_ptr_, this->data_len_); + ProtoEncode::encode_bool(pos PROTO_ENCODE_DEBUG_ARG, 3, this->done); #ifdef USE_DEVICES - buffer.encode_uint32(4, this->device_id); + ProtoEncode::encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, 4, this->device_id); #endif + return pos; } uint32_t CameraImageResponse::calculate_size() const { uint32_t size = 0; @@ -1328,74 +1386,70 @@ bool CameraImageRequest::decode_varint(uint32_t field_id, proto_varint_value_t v } #endif #ifdef USE_CLIMATE -void ListEntitiesClimateResponse::encode(ProtoWriteBuffer &buffer) const { - buffer.encode_string(1, this->object_id); - 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); +uint8_t *ListEntitiesClimateResponse::encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const { + uint8_t *__restrict__ pos = buffer.get_pos(); + ProtoEncode::encode_short_string_force(pos PROTO_ENCODE_DEBUG_ARG, 10, this->object_id); + ProtoEncode::write_tag_and_fixed32(pos PROTO_ENCODE_DEBUG_ARG, 21, this->key); + ProtoEncode::encode_short_string_force(pos PROTO_ENCODE_DEBUG_ARG, 26, this->name); + ProtoEncode::encode_bool(pos PROTO_ENCODE_DEBUG_ARG, 5, this->supports_current_temperature); + ProtoEncode::encode_bool(pos PROTO_ENCODE_DEBUG_ARG, 6, this->supports_two_point_target_temperature); for (const auto &it : *this->supported_modes) { - buffer.encode_uint32(7, static_cast(it), true); + ProtoEncode::encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, 7, static_cast(it), true); } - buffer.encode_float(8, this->visual_min_temperature); - buffer.encode_float(9, this->visual_max_temperature); - buffer.encode_float(10, this->visual_target_temperature_step); - buffer.encode_bool(12, this->supports_action); + ProtoEncode::encode_float(pos PROTO_ENCODE_DEBUG_ARG, 8, this->visual_min_temperature); + ProtoEncode::encode_float(pos PROTO_ENCODE_DEBUG_ARG, 9, this->visual_max_temperature); + ProtoEncode::encode_float(pos PROTO_ENCODE_DEBUG_ARG, 10, this->visual_target_temperature_step); + ProtoEncode::encode_bool(pos PROTO_ENCODE_DEBUG_ARG, 12, this->supports_action); for (const auto &it : *this->supported_fan_modes) { - buffer.encode_uint32(13, static_cast(it), true); + ProtoEncode::encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, 13, static_cast(it), true); } for (const auto &it : *this->supported_swing_modes) { - buffer.encode_uint32(14, static_cast(it), true); + ProtoEncode::encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, 14, static_cast(it), true); } for (const char *it : *this->supported_custom_fan_modes) { - buffer.encode_string(15, it, strlen(it), true); + ProtoEncode::encode_string(pos PROTO_ENCODE_DEBUG_ARG, 15, it, strlen(it), true); } for (const auto &it : *this->supported_presets) { - buffer.encode_uint32(16, static_cast(it), true); + ProtoEncode::encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, 16, static_cast(it), true); } for (const char *it : *this->supported_custom_presets) { - buffer.encode_string(17, it, strlen(it), true); + ProtoEncode::encode_string(pos PROTO_ENCODE_DEBUG_ARG, 17, it, strlen(it), true); } - buffer.encode_bool(18, this->disabled_by_default); + ProtoEncode::encode_bool(pos PROTO_ENCODE_DEBUG_ARG, 18, this->disabled_by_default); #ifdef USE_ENTITY_ICON - buffer.encode_string(19, this->icon); + ProtoEncode::encode_string(pos PROTO_ENCODE_DEBUG_ARG, 19, this->icon); #endif - buffer.encode_uint32(20, static_cast(this->entity_category)); - buffer.encode_float(21, this->visual_current_temperature_step); - buffer.encode_bool(22, this->supports_current_humidity); - buffer.encode_bool(23, this->supports_target_humidity); - buffer.encode_float(24, this->visual_min_humidity); - buffer.encode_float(25, this->visual_max_humidity); + ProtoEncode::encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, 20, static_cast(this->entity_category)); + ProtoEncode::encode_float(pos PROTO_ENCODE_DEBUG_ARG, 21, this->visual_current_temperature_step); + ProtoEncode::encode_bool(pos PROTO_ENCODE_DEBUG_ARG, 22, this->supports_current_humidity); + ProtoEncode::encode_bool(pos PROTO_ENCODE_DEBUG_ARG, 23, this->supports_target_humidity); + ProtoEncode::encode_float(pos PROTO_ENCODE_DEBUG_ARG, 24, this->visual_min_humidity); + ProtoEncode::encode_float(pos PROTO_ENCODE_DEBUG_ARG, 25, this->visual_max_humidity); #ifdef USE_DEVICES - buffer.encode_uint32(26, this->device_id); + ProtoEncode::encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, 26, this->device_id); #endif - buffer.encode_uint32(27, this->feature_flags); + ProtoEncode::encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, 27, this->feature_flags); + return pos; } uint32_t ListEntitiesClimateResponse::calculate_size() const { uint32_t size = 0; - size += ProtoSize::calc_length(1, this->object_id.size()); + size += 2 + this->object_id.size(); size += 5; - size += ProtoSize::calc_length(1, this->name.size()); + size += 2 + 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 += ProtoSize::calc_uint32_force(1, static_cast(it)); - } + size += this->supported_modes->size() * 2; } 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 += ProtoSize::calc_uint32_force(1, static_cast(it)); - } + size += this->supported_fan_modes->size() * 2; } if (!this->supported_swing_modes->empty()) { - for (const auto &it : *this->supported_swing_modes) { - size += ProtoSize::calc_uint32_force(1, static_cast(it)); - } + size += this->supported_swing_modes->size() * 2; } if (!this->supported_custom_fan_modes->empty()) { for (const char *it : *this->supported_custom_fan_modes) { @@ -1403,9 +1457,7 @@ uint32_t ListEntitiesClimateResponse::calculate_size() const { } } if (!this->supported_presets->empty()) { - for (const auto &it : *this->supported_presets) { - size += ProtoSize::calc_uint32_force(2, static_cast(it)); - } + size += this->supported_presets->size() * 3; } if (!this->supported_custom_presets->empty()) { for (const char *it : *this->supported_custom_presets) { @@ -1414,9 +1466,9 @@ uint32_t ListEntitiesClimateResponse::calculate_size() const { } size += ProtoSize::calc_bool(2, this->disabled_by_default); #ifdef USE_ENTITY_ICON - size += ProtoSize::calc_length(2, this->icon.size()); + size += !this->icon.empty() ? 3 + this->icon.size() : 0; #endif - size += ProtoSize::calc_uint32(2, static_cast(this->entity_category)); + size += this->entity_category ? 3 : 0; 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); @@ -1428,38 +1480,40 @@ uint32_t ListEntitiesClimateResponse::calculate_size() const { size += ProtoSize::calc_uint32(2, this->feature_flags); return size; } -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); - buffer.encode_float(5, this->target_temperature_low); - buffer.encode_float(6, this->target_temperature_high); - buffer.encode_uint32(8, static_cast(this->action)); - buffer.encode_uint32(9, static_cast(this->fan_mode)); - buffer.encode_uint32(10, static_cast(this->swing_mode)); - buffer.encode_string(11, this->custom_fan_mode); - buffer.encode_uint32(12, static_cast(this->preset)); - buffer.encode_string(13, this->custom_preset); - buffer.encode_float(14, this->current_humidity); - buffer.encode_float(15, this->target_humidity); +uint8_t *ClimateStateResponse::encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const { + uint8_t *__restrict__ pos = buffer.get_pos(); + ProtoEncode::write_tag_and_fixed32(pos PROTO_ENCODE_DEBUG_ARG, 13, this->key); + ProtoEncode::encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, 2, static_cast(this->mode)); + ProtoEncode::encode_float(pos PROTO_ENCODE_DEBUG_ARG, 3, this->current_temperature); + ProtoEncode::encode_float(pos PROTO_ENCODE_DEBUG_ARG, 4, this->target_temperature); + ProtoEncode::encode_float(pos PROTO_ENCODE_DEBUG_ARG, 5, this->target_temperature_low); + ProtoEncode::encode_float(pos PROTO_ENCODE_DEBUG_ARG, 6, this->target_temperature_high); + ProtoEncode::encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, 8, static_cast(this->action)); + ProtoEncode::encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, 9, static_cast(this->fan_mode)); + ProtoEncode::encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, 10, static_cast(this->swing_mode)); + ProtoEncode::encode_string(pos PROTO_ENCODE_DEBUG_ARG, 11, this->custom_fan_mode); + ProtoEncode::encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, 12, static_cast(this->preset)); + ProtoEncode::encode_string(pos PROTO_ENCODE_DEBUG_ARG, 13, this->custom_preset); + ProtoEncode::encode_float(pos PROTO_ENCODE_DEBUG_ARG, 14, this->current_humidity); + ProtoEncode::encode_float(pos PROTO_ENCODE_DEBUG_ARG, 15, this->target_humidity); #ifdef USE_DEVICES - buffer.encode_uint32(16, this->device_id); + ProtoEncode::encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, 16, this->device_id); #endif + return pos; } uint32_t ClimateStateResponse::calculate_size() const { uint32_t size = 0; size += 5; - size += ProtoSize::calc_uint32(1, static_cast(this->mode)); + size += this->mode ? 2 : 0; 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 += this->action ? 2 : 0; + size += this->fan_mode ? 2 : 0; + size += this->swing_mode ? 2 : 0; size += ProtoSize::calc_length(1, this->custom_fan_mode.size()); - size += ProtoSize::calc_uint32(1, static_cast(this->preset)); + size += this->preset ? 2 : 0; 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); @@ -1561,36 +1615,38 @@ bool ClimateCommandRequest::decode_32bit(uint32_t field_id, Proto32Bit value) { } #endif #ifdef USE_WATER_HEATER -void ListEntitiesWaterHeaterResponse::encode(ProtoWriteBuffer &buffer) const { - buffer.encode_string(1, this->object_id); - buffer.write_tag_and_fixed32(21, this->key); - buffer.encode_string(3, this->name); +uint8_t *ListEntitiesWaterHeaterResponse::encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const { + uint8_t *__restrict__ pos = buffer.get_pos(); + ProtoEncode::encode_short_string_force(pos PROTO_ENCODE_DEBUG_ARG, 10, this->object_id); + ProtoEncode::write_tag_and_fixed32(pos PROTO_ENCODE_DEBUG_ARG, 21, this->key); + ProtoEncode::encode_short_string_force(pos PROTO_ENCODE_DEBUG_ARG, 26, this->name); #ifdef USE_ENTITY_ICON - buffer.encode_string(4, this->icon); + ProtoEncode::encode_string(pos PROTO_ENCODE_DEBUG_ARG, 4, this->icon); #endif - buffer.encode_bool(5, this->disabled_by_default); - buffer.encode_uint32(6, static_cast(this->entity_category)); + ProtoEncode::encode_bool(pos PROTO_ENCODE_DEBUG_ARG, 5, this->disabled_by_default); + ProtoEncode::encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, 6, static_cast(this->entity_category)); #ifdef USE_DEVICES - buffer.encode_uint32(7, this->device_id); + ProtoEncode::encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, 7, this->device_id); #endif - buffer.encode_float(8, this->min_temperature); - buffer.encode_float(9, this->max_temperature); - buffer.encode_float(10, this->target_temperature_step); + ProtoEncode::encode_float(pos PROTO_ENCODE_DEBUG_ARG, 8, this->min_temperature); + ProtoEncode::encode_float(pos PROTO_ENCODE_DEBUG_ARG, 9, this->max_temperature); + ProtoEncode::encode_float(pos PROTO_ENCODE_DEBUG_ARG, 10, this->target_temperature_step); for (const auto &it : *this->supported_modes) { - buffer.encode_uint32(11, static_cast(it), true); + ProtoEncode::encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, 11, static_cast(it), true); } - buffer.encode_uint32(12, this->supported_features); + ProtoEncode::encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, 12, this->supported_features); + return pos; } uint32_t ListEntitiesWaterHeaterResponse::calculate_size() const { uint32_t size = 0; - size += ProtoSize::calc_length(1, this->object_id.size()); + size += 2 + this->object_id.size(); size += 5; - size += ProtoSize::calc_length(1, this->name.size()); + size += 2 + this->name.size(); #ifdef USE_ENTITY_ICON - size += ProtoSize::calc_length(1, this->icon.size()); + size += !this->icon.empty() ? 2 + this->icon.size() : 0; #endif size += ProtoSize::calc_bool(1, this->disabled_by_default); - size += ProtoSize::calc_uint32(1, static_cast(this->entity_category)); + size += this->entity_category ? 2 : 0; #ifdef USE_DEVICES size += ProtoSize::calc_uint32(1, this->device_id); #endif @@ -1598,31 +1654,31 @@ uint32_t ListEntitiesWaterHeaterResponse::calculate_size() const { 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 += ProtoSize::calc_uint32_force(1, static_cast(it)); - } + size += this->supported_modes->size() * 2; } size += ProtoSize::calc_uint32(1, this->supported_features); return size; } -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)); +uint8_t *WaterHeaterStateResponse::encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const { + uint8_t *__restrict__ pos = buffer.get_pos(); + ProtoEncode::write_tag_and_fixed32(pos PROTO_ENCODE_DEBUG_ARG, 13, this->key); + ProtoEncode::encode_float(pos PROTO_ENCODE_DEBUG_ARG, 2, this->current_temperature); + ProtoEncode::encode_float(pos PROTO_ENCODE_DEBUG_ARG, 3, this->target_temperature); + ProtoEncode::encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, 4, static_cast(this->mode)); #ifdef USE_DEVICES - buffer.encode_uint32(5, this->device_id); + ProtoEncode::encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, 5, this->device_id); #endif - buffer.encode_uint32(6, this->state); - buffer.encode_float(7, this->target_temperature_low); - buffer.encode_float(8, this->target_temperature_high); + ProtoEncode::encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, 6, this->state); + ProtoEncode::encode_float(pos PROTO_ENCODE_DEBUG_ARG, 7, this->target_temperature_low); + ProtoEncode::encode_float(pos PROTO_ENCODE_DEBUG_ARG, 8, this->target_temperature_high); + return pos; } 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)); + size += this->mode ? 2 : 0; #ifdef USE_DEVICES size += ProtoSize::calc_uint32(1, this->device_id); #endif @@ -1673,53 +1729,57 @@ bool WaterHeaterCommandRequest::decode_32bit(uint32_t field_id, Proto32Bit value } #endif #ifdef USE_NUMBER -void ListEntitiesNumberResponse::encode(ProtoWriteBuffer &buffer) const { - buffer.encode_string(1, this->object_id); - buffer.write_tag_and_fixed32(21, this->key); - buffer.encode_string(3, this->name); +uint8_t *ListEntitiesNumberResponse::encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const { + uint8_t *__restrict__ pos = buffer.get_pos(); + ProtoEncode::encode_short_string_force(pos PROTO_ENCODE_DEBUG_ARG, 10, this->object_id); + ProtoEncode::write_tag_and_fixed32(pos PROTO_ENCODE_DEBUG_ARG, 21, this->key); + ProtoEncode::encode_short_string_force(pos PROTO_ENCODE_DEBUG_ARG, 26, this->name); #ifdef USE_ENTITY_ICON - buffer.encode_string(5, this->icon); + ProtoEncode::encode_string(pos PROTO_ENCODE_DEBUG_ARG, 5, this->icon); #endif - buffer.encode_float(6, this->min_value); - buffer.encode_float(7, this->max_value); - buffer.encode_float(8, this->step); - buffer.encode_bool(9, this->disabled_by_default); - buffer.encode_uint32(10, static_cast(this->entity_category)); - buffer.encode_string(11, this->unit_of_measurement); - buffer.encode_uint32(12, static_cast(this->mode)); - buffer.encode_string(13, this->device_class); + ProtoEncode::encode_float(pos PROTO_ENCODE_DEBUG_ARG, 6, this->min_value); + ProtoEncode::encode_float(pos PROTO_ENCODE_DEBUG_ARG, 7, this->max_value); + ProtoEncode::encode_float(pos PROTO_ENCODE_DEBUG_ARG, 8, this->step); + ProtoEncode::encode_bool(pos PROTO_ENCODE_DEBUG_ARG, 9, this->disabled_by_default); + ProtoEncode::encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, 10, static_cast(this->entity_category)); + ProtoEncode::encode_string(pos PROTO_ENCODE_DEBUG_ARG, 11, this->unit_of_measurement); + ProtoEncode::encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, 12, static_cast(this->mode)); + ProtoEncode::encode_string(pos PROTO_ENCODE_DEBUG_ARG, 13, this->device_class); #ifdef USE_DEVICES - buffer.encode_uint32(14, this->device_id); + ProtoEncode::encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, 14, this->device_id); #endif + return pos; } uint32_t ListEntitiesNumberResponse::calculate_size() const { uint32_t size = 0; - size += ProtoSize::calc_length(1, this->object_id.size()); + size += 2 + this->object_id.size(); size += 5; - size += ProtoSize::calc_length(1, this->name.size()); + size += 2 + this->name.size(); #ifdef USE_ENTITY_ICON - size += ProtoSize::calc_length(1, this->icon.size()); + size += !this->icon.empty() ? 2 + this->icon.size() : 0; #endif 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()); + size += this->entity_category ? 2 : 0; + size += !this->unit_of_measurement.empty() ? 2 + this->unit_of_measurement.size() : 0; + size += this->mode ? 2 : 0; + size += !this->device_class.empty() ? 2 + this->device_class.size() : 0; #ifdef USE_DEVICES size += ProtoSize::calc_uint32(1, this->device_id); #endif return size; } -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); +uint8_t *NumberStateResponse::encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const { + uint8_t *__restrict__ pos = buffer.get_pos(); + ProtoEncode::write_tag_and_fixed32(pos PROTO_ENCODE_DEBUG_ARG, 13, this->key); + ProtoEncode::encode_float(pos PROTO_ENCODE_DEBUG_ARG, 2, this->state); + ProtoEncode::encode_bool(pos PROTO_ENCODE_DEBUG_ARG, 3, this->missing_state); #ifdef USE_DEVICES - buffer.encode_uint32(4, this->device_id); + ProtoEncode::encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, 4, this->device_id); #endif + return pos; } uint32_t NumberStateResponse::calculate_size() const { uint32_t size = 0; @@ -1758,29 +1818,31 @@ bool NumberCommandRequest::decode_32bit(uint32_t field_id, Proto32Bit value) { } #endif #ifdef USE_SELECT -void ListEntitiesSelectResponse::encode(ProtoWriteBuffer &buffer) const { - buffer.encode_string(1, this->object_id); - buffer.write_tag_and_fixed32(21, this->key); - buffer.encode_string(3, this->name); +uint8_t *ListEntitiesSelectResponse::encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const { + uint8_t *__restrict__ pos = buffer.get_pos(); + ProtoEncode::encode_short_string_force(pos PROTO_ENCODE_DEBUG_ARG, 10, this->object_id); + ProtoEncode::write_tag_and_fixed32(pos PROTO_ENCODE_DEBUG_ARG, 21, this->key); + ProtoEncode::encode_short_string_force(pos PROTO_ENCODE_DEBUG_ARG, 26, this->name); #ifdef USE_ENTITY_ICON - buffer.encode_string(5, this->icon); + ProtoEncode::encode_string(pos PROTO_ENCODE_DEBUG_ARG, 5, this->icon); #endif for (const char *it : *this->options) { - buffer.encode_string(6, it, strlen(it), true); + ProtoEncode::encode_string(pos PROTO_ENCODE_DEBUG_ARG, 6, it, strlen(it), true); } - buffer.encode_bool(7, this->disabled_by_default); - buffer.encode_uint32(8, static_cast(this->entity_category)); + ProtoEncode::encode_bool(pos PROTO_ENCODE_DEBUG_ARG, 7, this->disabled_by_default); + ProtoEncode::encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, 8, static_cast(this->entity_category)); #ifdef USE_DEVICES - buffer.encode_uint32(9, this->device_id); + ProtoEncode::encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, 9, this->device_id); #endif + return pos; } uint32_t ListEntitiesSelectResponse::calculate_size() const { uint32_t size = 0; - size += ProtoSize::calc_length(1, this->object_id.size()); + size += 2 + this->object_id.size(); size += 5; - size += ProtoSize::calc_length(1, this->name.size()); + size += 2 + this->name.size(); #ifdef USE_ENTITY_ICON - size += ProtoSize::calc_length(1, this->icon.size()); + size += !this->icon.empty() ? 2 + this->icon.size() : 0; #endif if (!this->options->empty()) { for (const char *it : *this->options) { @@ -1788,19 +1850,21 @@ uint32_t ListEntitiesSelectResponse::calculate_size() const { } } size += ProtoSize::calc_bool(1, this->disabled_by_default); - size += ProtoSize::calc_uint32(1, static_cast(this->entity_category)); + size += this->entity_category ? 2 : 0; #ifdef USE_DEVICES size += ProtoSize::calc_uint32(1, this->device_id); #endif return size; } -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); +uint8_t *SelectStateResponse::encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const { + uint8_t *__restrict__ pos = buffer.get_pos(); + ProtoEncode::write_tag_and_fixed32(pos PROTO_ENCODE_DEBUG_ARG, 13, this->key); + ProtoEncode::encode_string(pos PROTO_ENCODE_DEBUG_ARG, 2, this->state); + ProtoEncode::encode_bool(pos PROTO_ENCODE_DEBUG_ARG, 3, this->missing_state); #ifdef USE_DEVICES - buffer.encode_uint32(4, this->device_id); + ProtoEncode::encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, 4, this->device_id); #endif + return pos; } uint32_t SelectStateResponse::calculate_size() const { uint32_t size = 0; @@ -1847,31 +1911,33 @@ bool SelectCommandRequest::decode_32bit(uint32_t field_id, Proto32Bit value) { } #endif #ifdef USE_SIREN -void ListEntitiesSirenResponse::encode(ProtoWriteBuffer &buffer) const { - buffer.encode_string(1, this->object_id); - buffer.write_tag_and_fixed32(21, this->key); - buffer.encode_string(3, this->name); +uint8_t *ListEntitiesSirenResponse::encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const { + uint8_t *__restrict__ pos = buffer.get_pos(); + ProtoEncode::encode_short_string_force(pos PROTO_ENCODE_DEBUG_ARG, 10, this->object_id); + ProtoEncode::write_tag_and_fixed32(pos PROTO_ENCODE_DEBUG_ARG, 21, this->key); + ProtoEncode::encode_short_string_force(pos PROTO_ENCODE_DEBUG_ARG, 26, this->name); #ifdef USE_ENTITY_ICON - buffer.encode_string(5, this->icon); + ProtoEncode::encode_string(pos PROTO_ENCODE_DEBUG_ARG, 5, this->icon); #endif - buffer.encode_bool(6, this->disabled_by_default); + ProtoEncode::encode_bool(pos PROTO_ENCODE_DEBUG_ARG, 6, this->disabled_by_default); for (const char *it : *this->tones) { - buffer.encode_string(7, it, strlen(it), true); + ProtoEncode::encode_string(pos PROTO_ENCODE_DEBUG_ARG, 7, it, strlen(it), true); } - buffer.encode_bool(8, this->supports_duration); - buffer.encode_bool(9, this->supports_volume); - buffer.encode_uint32(10, static_cast(this->entity_category)); + ProtoEncode::encode_bool(pos PROTO_ENCODE_DEBUG_ARG, 8, this->supports_duration); + ProtoEncode::encode_bool(pos PROTO_ENCODE_DEBUG_ARG, 9, this->supports_volume); + ProtoEncode::encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, 10, static_cast(this->entity_category)); #ifdef USE_DEVICES - buffer.encode_uint32(11, this->device_id); + ProtoEncode::encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, 11, this->device_id); #endif + return pos; } uint32_t ListEntitiesSirenResponse::calculate_size() const { uint32_t size = 0; - size += ProtoSize::calc_length(1, this->object_id.size()); + size += 2 + this->object_id.size(); size += 5; - size += ProtoSize::calc_length(1, this->name.size()); + size += 2 + this->name.size(); #ifdef USE_ENTITY_ICON - size += ProtoSize::calc_length(1, this->icon.size()); + size += !this->icon.empty() ? 2 + this->icon.size() : 0; #endif size += ProtoSize::calc_bool(1, this->disabled_by_default); if (!this->tones->empty()) { @@ -1881,18 +1947,20 @@ uint32_t ListEntitiesSirenResponse::calculate_size() const { } 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)); + size += this->entity_category ? 2 : 0; #ifdef USE_DEVICES size += ProtoSize::calc_uint32(1, this->device_id); #endif return size; } -void SirenStateResponse::encode(ProtoWriteBuffer &buffer) const { - buffer.write_tag_and_fixed32(13, this->key); - buffer.encode_bool(2, this->state); +uint8_t *SirenStateResponse::encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const { + uint8_t *__restrict__ pos = buffer.get_pos(); + ProtoEncode::write_tag_and_fixed32(pos PROTO_ENCODE_DEBUG_ARG, 13, this->key); + ProtoEncode::encode_bool(pos PROTO_ENCODE_DEBUG_ARG, 2, this->state); #ifdef USE_DEVICES - buffer.encode_uint32(3, this->device_id); + ProtoEncode::encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, 3, this->device_id); #endif + return pos; } uint32_t SirenStateResponse::calculate_size() const { uint32_t size = 0; @@ -1959,33 +2027,35 @@ bool SirenCommandRequest::decode_32bit(uint32_t field_id, Proto32Bit value) { } #endif #ifdef USE_LOCK -void ListEntitiesLockResponse::encode(ProtoWriteBuffer &buffer) const { - buffer.encode_string(1, this->object_id); - buffer.write_tag_and_fixed32(21, this->key); - buffer.encode_string(3, this->name); +uint8_t *ListEntitiesLockResponse::encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const { + uint8_t *__restrict__ pos = buffer.get_pos(); + ProtoEncode::encode_short_string_force(pos PROTO_ENCODE_DEBUG_ARG, 10, this->object_id); + ProtoEncode::write_tag_and_fixed32(pos PROTO_ENCODE_DEBUG_ARG, 21, this->key); + ProtoEncode::encode_short_string_force(pos PROTO_ENCODE_DEBUG_ARG, 26, this->name); #ifdef USE_ENTITY_ICON - buffer.encode_string(5, this->icon); + ProtoEncode::encode_string(pos PROTO_ENCODE_DEBUG_ARG, 5, this->icon); #endif - buffer.encode_bool(6, this->disabled_by_default); - buffer.encode_uint32(7, static_cast(this->entity_category)); - buffer.encode_bool(8, this->assumed_state); - buffer.encode_bool(9, this->supports_open); - buffer.encode_bool(10, this->requires_code); - buffer.encode_string(11, this->code_format); + ProtoEncode::encode_bool(pos PROTO_ENCODE_DEBUG_ARG, 6, this->disabled_by_default); + ProtoEncode::encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, 7, static_cast(this->entity_category)); + ProtoEncode::encode_bool(pos PROTO_ENCODE_DEBUG_ARG, 8, this->assumed_state); + ProtoEncode::encode_bool(pos PROTO_ENCODE_DEBUG_ARG, 9, this->supports_open); + ProtoEncode::encode_bool(pos PROTO_ENCODE_DEBUG_ARG, 10, this->requires_code); + ProtoEncode::encode_string(pos PROTO_ENCODE_DEBUG_ARG, 11, this->code_format); #ifdef USE_DEVICES - buffer.encode_uint32(12, this->device_id); + ProtoEncode::encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, 12, this->device_id); #endif + return pos; } uint32_t ListEntitiesLockResponse::calculate_size() const { uint32_t size = 0; - size += ProtoSize::calc_length(1, this->object_id.size()); + size += 2 + this->object_id.size(); size += 5; - size += ProtoSize::calc_length(1, this->name.size()); + size += 2 + this->name.size(); #ifdef USE_ENTITY_ICON - size += ProtoSize::calc_length(1, this->icon.size()); + size += !this->icon.empty() ? 2 + this->icon.size() : 0; #endif size += ProtoSize::calc_bool(1, this->disabled_by_default); - size += ProtoSize::calc_uint32(1, static_cast(this->entity_category)); + size += this->entity_category ? 2 : 0; size += ProtoSize::calc_bool(1, this->assumed_state); size += ProtoSize::calc_bool(1, this->supports_open); size += ProtoSize::calc_bool(1, this->requires_code); @@ -1995,17 +2065,19 @@ uint32_t ListEntitiesLockResponse::calculate_size() const { #endif return size; } -void LockStateResponse::encode(ProtoWriteBuffer &buffer) const { - buffer.write_tag_and_fixed32(13, this->key); - buffer.encode_uint32(2, static_cast(this->state)); +uint8_t *LockStateResponse::encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const { + uint8_t *__restrict__ pos = buffer.get_pos(); + ProtoEncode::write_tag_and_fixed32(pos PROTO_ENCODE_DEBUG_ARG, 13, this->key); + ProtoEncode::encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, 2, static_cast(this->state)); #ifdef USE_DEVICES - buffer.encode_uint32(3, this->device_id); + ProtoEncode::encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, 3, this->device_id); #endif + return pos; } uint32_t LockStateResponse::calculate_size() const { uint32_t size = 0; size += 5; - size += ProtoSize::calc_uint32(1, static_cast(this->state)); + size += this->state ? 2 : 0; #ifdef USE_DEVICES size += ProtoSize::calc_uint32(1, this->device_id); #endif @@ -2052,31 +2124,33 @@ bool LockCommandRequest::decode_32bit(uint32_t field_id, Proto32Bit value) { } #endif #ifdef USE_BUTTON -void ListEntitiesButtonResponse::encode(ProtoWriteBuffer &buffer) const { - buffer.encode_string(1, this->object_id); - buffer.write_tag_and_fixed32(21, this->key); - buffer.encode_string(3, this->name); +uint8_t *ListEntitiesButtonResponse::encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const { + uint8_t *__restrict__ pos = buffer.get_pos(); + ProtoEncode::encode_short_string_force(pos PROTO_ENCODE_DEBUG_ARG, 10, this->object_id); + ProtoEncode::write_tag_and_fixed32(pos PROTO_ENCODE_DEBUG_ARG, 21, this->key); + ProtoEncode::encode_short_string_force(pos PROTO_ENCODE_DEBUG_ARG, 26, this->name); #ifdef USE_ENTITY_ICON - buffer.encode_string(5, this->icon); + ProtoEncode::encode_string(pos PROTO_ENCODE_DEBUG_ARG, 5, this->icon); #endif - buffer.encode_bool(6, this->disabled_by_default); - buffer.encode_uint32(7, static_cast(this->entity_category)); - buffer.encode_string(8, this->device_class); + ProtoEncode::encode_bool(pos PROTO_ENCODE_DEBUG_ARG, 6, this->disabled_by_default); + ProtoEncode::encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, 7, static_cast(this->entity_category)); + ProtoEncode::encode_string(pos PROTO_ENCODE_DEBUG_ARG, 8, this->device_class); #ifdef USE_DEVICES - buffer.encode_uint32(9, this->device_id); + ProtoEncode::encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, 9, this->device_id); #endif + return pos; } uint32_t ListEntitiesButtonResponse::calculate_size() const { uint32_t size = 0; - size += ProtoSize::calc_length(1, this->object_id.size()); + size += 2 + this->object_id.size(); size += 5; - size += ProtoSize::calc_length(1, this->name.size()); + size += 2 + this->name.size(); #ifdef USE_ENTITY_ICON - size += ProtoSize::calc_length(1, this->icon.size()); + size += !this->icon.empty() ? 2 + this->icon.size() : 0; #endif 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 += this->entity_category ? 2 : 0; + size += !this->device_class.empty() ? 2 + this->device_class.size() : 0; #ifdef USE_DEVICES size += ProtoSize::calc_uint32(1, this->device_id); #endif @@ -2106,50 +2180,54 @@ bool ButtonCommandRequest::decode_32bit(uint32_t field_id, Proto32Bit value) { } #endif #ifdef USE_MEDIA_PLAYER -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); +uint8_t *MediaPlayerSupportedFormat::encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const { + uint8_t *__restrict__ pos = buffer.get_pos(); + ProtoEncode::encode_string(pos PROTO_ENCODE_DEBUG_ARG, 1, this->format); + ProtoEncode::encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, 2, this->sample_rate); + ProtoEncode::encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, 3, this->num_channels); + ProtoEncode::encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, 4, static_cast(this->purpose)); + ProtoEncode::encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, 5, this->sample_bytes); + return pos; } 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 += this->purpose ? 2 : 0; size += ProtoSize::calc_uint32(1, this->sample_bytes); return size; } -void ListEntitiesMediaPlayerResponse::encode(ProtoWriteBuffer &buffer) const { - buffer.encode_string(1, this->object_id); - buffer.write_tag_and_fixed32(21, this->key); - buffer.encode_string(3, this->name); +uint8_t *ListEntitiesMediaPlayerResponse::encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const { + uint8_t *__restrict__ pos = buffer.get_pos(); + ProtoEncode::encode_short_string_force(pos PROTO_ENCODE_DEBUG_ARG, 10, this->object_id); + ProtoEncode::write_tag_and_fixed32(pos PROTO_ENCODE_DEBUG_ARG, 21, this->key); + ProtoEncode::encode_short_string_force(pos PROTO_ENCODE_DEBUG_ARG, 26, this->name); #ifdef USE_ENTITY_ICON - buffer.encode_string(5, this->icon); + ProtoEncode::encode_string(pos PROTO_ENCODE_DEBUG_ARG, 5, this->icon); #endif - buffer.encode_bool(6, this->disabled_by_default); - buffer.encode_uint32(7, static_cast(this->entity_category)); - buffer.encode_bool(8, this->supports_pause); + ProtoEncode::encode_bool(pos PROTO_ENCODE_DEBUG_ARG, 6, this->disabled_by_default); + ProtoEncode::encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, 7, static_cast(this->entity_category)); + ProtoEncode::encode_bool(pos PROTO_ENCODE_DEBUG_ARG, 8, this->supports_pause); for (auto &it : this->supported_formats) { - buffer.encode_sub_message(9, it); + ProtoEncode::encode_sub_message(pos PROTO_ENCODE_DEBUG_ARG, buffer, 9, it); } #ifdef USE_DEVICES - buffer.encode_uint32(10, this->device_id); + ProtoEncode::encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, 10, this->device_id); #endif - buffer.encode_uint32(11, this->feature_flags); + ProtoEncode::encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, 11, this->feature_flags); + return pos; } uint32_t ListEntitiesMediaPlayerResponse::calculate_size() const { uint32_t size = 0; - size += ProtoSize::calc_length(1, this->object_id.size()); + size += 2 + this->object_id.size(); size += 5; - size += ProtoSize::calc_length(1, this->name.size()); + size += 2 + this->name.size(); #ifdef USE_ENTITY_ICON - size += ProtoSize::calc_length(1, this->icon.size()); + size += !this->icon.empty() ? 2 + this->icon.size() : 0; #endif size += ProtoSize::calc_bool(1, this->disabled_by_default); - size += ProtoSize::calc_uint32(1, static_cast(this->entity_category)); + size += this->entity_category ? 2 : 0; size += ProtoSize::calc_bool(1, this->supports_pause); if (!this->supported_formats.empty()) { for (const auto &it : this->supported_formats) { @@ -2162,19 +2240,21 @@ uint32_t ListEntitiesMediaPlayerResponse::calculate_size() const { size += ProtoSize::calc_uint32(1, this->feature_flags); return size; } -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); +uint8_t *MediaPlayerStateResponse::encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const { + uint8_t *__restrict__ pos = buffer.get_pos(); + ProtoEncode::write_tag_and_fixed32(pos PROTO_ENCODE_DEBUG_ARG, 13, this->key); + ProtoEncode::encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, 2, static_cast(this->state)); + ProtoEncode::encode_float(pos PROTO_ENCODE_DEBUG_ARG, 3, this->volume); + ProtoEncode::encode_bool(pos PROTO_ENCODE_DEBUG_ARG, 4, this->muted); #ifdef USE_DEVICES - buffer.encode_uint32(5, this->device_id); + ProtoEncode::encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, 5, this->device_id); #endif + return pos; } uint32_t MediaPlayerStateResponse::calculate_size() const { uint32_t size = 0; size += 5; - size += ProtoSize::calc_uint32(1, static_cast(this->state)); + size += this->state ? 2 : 0; size += ProtoSize::calc_float(1, this->volume); size += ProtoSize::calc_bool(1, this->muted); #ifdef USE_DEVICES @@ -2248,28 +2328,35 @@ bool SubscribeBluetoothLEAdvertisementsRequest::decode_varint(uint32_t field_id, } return true; } -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.write_raw_byte(34); - buffer.encode_varint_raw(this->data_len); - buffer.encode_raw(this->data, this->data_len); +uint8_t *BluetoothLERawAdvertisement::encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const { + uint8_t *__restrict__ pos = buffer.get_pos(); + ProtoEncode::write_raw_byte(pos PROTO_ENCODE_DEBUG_ARG, 8); + ProtoEncode::encode_varint_raw_64(pos PROTO_ENCODE_DEBUG_ARG, this->address); + ProtoEncode::write_raw_byte(pos PROTO_ENCODE_DEBUG_ARG, 16); + ProtoEncode::encode_varint_raw_short(pos PROTO_ENCODE_DEBUG_ARG, encode_zigzag32(this->rssi)); + if (this->address_type) { + ProtoEncode::write_raw_byte(pos PROTO_ENCODE_DEBUG_ARG, 24); + ProtoEncode::encode_varint_raw(pos PROTO_ENCODE_DEBUG_ARG, this->address_type); + } + ProtoEncode::write_raw_byte(pos PROTO_ENCODE_DEBUG_ARG, 34); + ProtoEncode::write_raw_byte(pos PROTO_ENCODE_DEBUG_ARG, static_cast(this->data_len)); + ProtoEncode::encode_raw(pos PROTO_ENCODE_DEBUG_ARG, this->data, this->data_len); + return pos; } 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); + size += this->address_type ? 2 : 0; + size += 2 + this->data_len; return size; } -void BluetoothLERawAdvertisementsResponse::encode(ProtoWriteBuffer &buffer) const { +uint8_t *BluetoothLERawAdvertisementsResponse::encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const { + uint8_t *__restrict__ pos = buffer.get_pos(); for (uint16_t i = 0; i < this->advertisements_len; i++) { - buffer.encode_sub_message(1, this->advertisements[i]); + ProtoEncode::encode_sub_message(pos PROTO_ENCODE_DEBUG_ARG, buffer, 1, this->advertisements[i]); } + return pos; } uint32_t BluetoothLERawAdvertisementsResponse::calculate_size() const { uint32_t size = 0; @@ -2297,11 +2384,13 @@ bool BluetoothDeviceRequest::decode_varint(uint32_t field_id, proto_varint_value } return true; } -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); +uint8_t *BluetoothDeviceConnectionResponse::encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const { + uint8_t *__restrict__ pos = buffer.get_pos(); + ProtoEncode::encode_uint64(pos PROTO_ENCODE_DEBUG_ARG, 1, this->address); + ProtoEncode::encode_bool(pos PROTO_ENCODE_DEBUG_ARG, 2, this->connected); + ProtoEncode::encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, 3, this->mtu); + ProtoEncode::encode_int32(pos PROTO_ENCODE_DEBUG_ARG, 4, this->error); + return pos; } uint32_t BluetoothDeviceConnectionResponse::calculate_size() const { uint32_t size = 0; @@ -2321,13 +2410,15 @@ bool BluetoothGATTGetServicesRequest::decode_varint(uint32_t field_id, proto_var } return true; } -void BluetoothGATTDescriptor::encode(ProtoWriteBuffer &buffer) const { +uint8_t *BluetoothGATTDescriptor::encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const { + uint8_t *__restrict__ pos = buffer.get_pos(); 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); + ProtoEncode::encode_uint64(pos PROTO_ENCODE_DEBUG_ARG, 1, this->uuid[0], true); + ProtoEncode::encode_uint64(pos PROTO_ENCODE_DEBUG_ARG, 1, this->uuid[1], true); } - buffer.encode_uint32(2, this->handle); - buffer.encode_uint32(3, this->short_uuid); + ProtoEncode::encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, 2, this->handle); + ProtoEncode::encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, 3, this->short_uuid); + return pos; } uint32_t BluetoothGATTDescriptor::calculate_size() const { uint32_t size = 0; @@ -2339,17 +2430,19 @@ uint32_t BluetoothGATTDescriptor::calculate_size() const { size += ProtoSize::calc_uint32(1, this->short_uuid); return size; } -void BluetoothGATTCharacteristic::encode(ProtoWriteBuffer &buffer) const { +uint8_t *BluetoothGATTCharacteristic::encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const { + uint8_t *__restrict__ pos = buffer.get_pos(); 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); + ProtoEncode::encode_uint64(pos PROTO_ENCODE_DEBUG_ARG, 1, this->uuid[0], true); + ProtoEncode::encode_uint64(pos PROTO_ENCODE_DEBUG_ARG, 1, this->uuid[1], true); } - buffer.encode_uint32(2, this->handle); - buffer.encode_uint32(3, this->properties); + ProtoEncode::encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, 2, this->handle); + ProtoEncode::encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, 3, this->properties); for (auto &it : this->descriptors) { - buffer.encode_sub_message(4, it); + ProtoEncode::encode_sub_message(pos PROTO_ENCODE_DEBUG_ARG, buffer, 4, it); } - buffer.encode_uint32(5, this->short_uuid); + ProtoEncode::encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, 5, this->short_uuid); + return pos; } uint32_t BluetoothGATTCharacteristic::calculate_size() const { uint32_t size = 0; @@ -2367,16 +2460,18 @@ uint32_t BluetoothGATTCharacteristic::calculate_size() const { size += ProtoSize::calc_uint32(1, this->short_uuid); return size; } -void BluetoothGATTService::encode(ProtoWriteBuffer &buffer) const { +uint8_t *BluetoothGATTService::encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const { + uint8_t *__restrict__ pos = buffer.get_pos(); 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); + ProtoEncode::encode_uint64(pos PROTO_ENCODE_DEBUG_ARG, 1, this->uuid[0], true); + ProtoEncode::encode_uint64(pos PROTO_ENCODE_DEBUG_ARG, 1, this->uuid[1], true); } - buffer.encode_uint32(2, this->handle); + ProtoEncode::encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, 2, this->handle); for (auto &it : this->characteristics) { - buffer.encode_sub_message(3, it); + ProtoEncode::encode_sub_message(pos PROTO_ENCODE_DEBUG_ARG, buffer, 3, it); } - buffer.encode_uint32(4, this->short_uuid); + ProtoEncode::encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, 4, this->short_uuid); + return pos; } uint32_t BluetoothGATTService::calculate_size() const { uint32_t size = 0; @@ -2393,11 +2488,13 @@ uint32_t BluetoothGATTService::calculate_size() const { size += ProtoSize::calc_uint32(1, this->short_uuid); return size; } -void BluetoothGATTGetServicesResponse::encode(ProtoWriteBuffer &buffer) const { - buffer.encode_uint64(1, this->address); +uint8_t *BluetoothGATTGetServicesResponse::encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const { + uint8_t *__restrict__ pos = buffer.get_pos(); + ProtoEncode::encode_uint64(pos PROTO_ENCODE_DEBUG_ARG, 1, this->address); for (auto &it : this->services) { - buffer.encode_sub_message(2, it); + ProtoEncode::encode_sub_message(pos PROTO_ENCODE_DEBUG_ARG, buffer, 2, it); } + return pos; } uint32_t BluetoothGATTGetServicesResponse::calculate_size() const { uint32_t size = 0; @@ -2409,8 +2506,10 @@ uint32_t BluetoothGATTGetServicesResponse::calculate_size() const { } return size; } -void BluetoothGATTGetServicesDoneResponse::encode(ProtoWriteBuffer &buffer) const { - buffer.encode_uint64(1, this->address); +uint8_t *BluetoothGATTGetServicesDoneResponse::encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const { + uint8_t *__restrict__ pos = buffer.get_pos(); + ProtoEncode::encode_uint64(pos PROTO_ENCODE_DEBUG_ARG, 1, this->address); + return pos; } uint32_t BluetoothGATTGetServicesDoneResponse::calculate_size() const { uint32_t size = 0; @@ -2430,10 +2529,12 @@ bool BluetoothGATTReadRequest::decode_varint(uint32_t field_id, proto_varint_val } return true; } -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_); +uint8_t *BluetoothGATTReadResponse::encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const { + uint8_t *__restrict__ pos = buffer.get_pos(); + ProtoEncode::encode_uint64(pos PROTO_ENCODE_DEBUG_ARG, 1, this->address); + ProtoEncode::encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, 2, this->handle); + ProtoEncode::encode_bytes(pos PROTO_ENCODE_DEBUG_ARG, 3, this->data_ptr_, this->data_len_); + return pos; } uint32_t BluetoothGATTReadResponse::calculate_size() const { uint32_t size = 0; @@ -2524,10 +2625,12 @@ bool BluetoothGATTNotifyRequest::decode_varint(uint32_t field_id, proto_varint_v } return true; } -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_); +uint8_t *BluetoothGATTNotifyDataResponse::encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const { + uint8_t *__restrict__ pos = buffer.get_pos(); + ProtoEncode::encode_uint64(pos PROTO_ENCODE_DEBUG_ARG, 1, this->address); + ProtoEncode::encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, 2, this->handle); + ProtoEncode::encode_bytes(pos PROTO_ENCODE_DEBUG_ARG, 3, this->data_ptr_, this->data_len_); + return pos; } uint32_t BluetoothGATTNotifyDataResponse::calculate_size() const { uint32_t size = 0; @@ -2536,14 +2639,16 @@ uint32_t BluetoothGATTNotifyDataResponse::calculate_size() const { size += ProtoSize::calc_length(1, this->data_len_); return size; } -void BluetoothConnectionsFreeResponse::encode(ProtoWriteBuffer &buffer) const { - buffer.encode_uint32(1, this->free); - buffer.encode_uint32(2, this->limit); +uint8_t *BluetoothConnectionsFreeResponse::encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const { + uint8_t *__restrict__ pos = buffer.get_pos(); + ProtoEncode::encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, 1, this->free); + ProtoEncode::encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, 2, this->limit); for (const auto &it : this->allocated) { if (it != 0) { - buffer.encode_uint64(3, it, true); + ProtoEncode::encode_uint64(pos PROTO_ENCODE_DEBUG_ARG, 3, it, true); } } + return pos; } uint32_t BluetoothConnectionsFreeResponse::calculate_size() const { uint32_t size = 0; @@ -2556,10 +2661,12 @@ uint32_t BluetoothConnectionsFreeResponse::calculate_size() const { } return size; } -void BluetoothGATTErrorResponse::encode(ProtoWriteBuffer &buffer) const { - buffer.encode_uint64(1, this->address); - buffer.encode_uint32(2, this->handle); - buffer.encode_int32(3, this->error); +uint8_t *BluetoothGATTErrorResponse::encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const { + uint8_t *__restrict__ pos = buffer.get_pos(); + ProtoEncode::encode_uint64(pos PROTO_ENCODE_DEBUG_ARG, 1, this->address); + ProtoEncode::encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, 2, this->handle); + ProtoEncode::encode_int32(pos PROTO_ENCODE_DEBUG_ARG, 3, this->error); + return pos; } uint32_t BluetoothGATTErrorResponse::calculate_size() const { uint32_t size = 0; @@ -2568,9 +2675,11 @@ uint32_t BluetoothGATTErrorResponse::calculate_size() const { size += ProtoSize::calc_int32(1, this->error); return size; } -void BluetoothGATTWriteResponse::encode(ProtoWriteBuffer &buffer) const { - buffer.encode_uint64(1, this->address); - buffer.encode_uint32(2, this->handle); +uint8_t *BluetoothGATTWriteResponse::encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const { + uint8_t *__restrict__ pos = buffer.get_pos(); + ProtoEncode::encode_uint64(pos PROTO_ENCODE_DEBUG_ARG, 1, this->address); + ProtoEncode::encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, 2, this->handle); + return pos; } uint32_t BluetoothGATTWriteResponse::calculate_size() const { uint32_t size = 0; @@ -2578,9 +2687,11 @@ uint32_t BluetoothGATTWriteResponse::calculate_size() const { size += ProtoSize::calc_uint32(1, this->handle); return size; } -void BluetoothGATTNotifyResponse::encode(ProtoWriteBuffer &buffer) const { - buffer.encode_uint64(1, this->address); - buffer.encode_uint32(2, this->handle); +uint8_t *BluetoothGATTNotifyResponse::encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const { + uint8_t *__restrict__ pos = buffer.get_pos(); + ProtoEncode::encode_uint64(pos PROTO_ENCODE_DEBUG_ARG, 1, this->address); + ProtoEncode::encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, 2, this->handle); + return pos; } uint32_t BluetoothGATTNotifyResponse::calculate_size() const { uint32_t size = 0; @@ -2588,10 +2699,12 @@ uint32_t BluetoothGATTNotifyResponse::calculate_size() const { size += ProtoSize::calc_uint32(1, this->handle); return size; } -void BluetoothDevicePairingResponse::encode(ProtoWriteBuffer &buffer) const { - buffer.encode_uint64(1, this->address); - buffer.encode_bool(2, this->paired); - buffer.encode_int32(3, this->error); +uint8_t *BluetoothDevicePairingResponse::encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const { + uint8_t *__restrict__ pos = buffer.get_pos(); + ProtoEncode::encode_uint64(pos PROTO_ENCODE_DEBUG_ARG, 1, this->address); + ProtoEncode::encode_bool(pos PROTO_ENCODE_DEBUG_ARG, 2, this->paired); + ProtoEncode::encode_int32(pos PROTO_ENCODE_DEBUG_ARG, 3, this->error); + return pos; } uint32_t BluetoothDevicePairingResponse::calculate_size() const { uint32_t size = 0; @@ -2600,10 +2713,12 @@ uint32_t BluetoothDevicePairingResponse::calculate_size() const { size += ProtoSize::calc_int32(1, this->error); return size; } -void BluetoothDeviceUnpairingResponse::encode(ProtoWriteBuffer &buffer) const { - buffer.encode_uint64(1, this->address); - buffer.encode_bool(2, this->success); - buffer.encode_int32(3, this->error); +uint8_t *BluetoothDeviceUnpairingResponse::encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const { + uint8_t *__restrict__ pos = buffer.get_pos(); + ProtoEncode::encode_uint64(pos PROTO_ENCODE_DEBUG_ARG, 1, this->address); + ProtoEncode::encode_bool(pos PROTO_ENCODE_DEBUG_ARG, 2, this->success); + ProtoEncode::encode_int32(pos PROTO_ENCODE_DEBUG_ARG, 3, this->error); + return pos; } uint32_t BluetoothDeviceUnpairingResponse::calculate_size() const { uint32_t size = 0; @@ -2612,10 +2727,12 @@ uint32_t BluetoothDeviceUnpairingResponse::calculate_size() const { size += ProtoSize::calc_int32(1, this->error); return size; } -void BluetoothDeviceClearCacheResponse::encode(ProtoWriteBuffer &buffer) const { - buffer.encode_uint64(1, this->address); - buffer.encode_bool(2, this->success); - buffer.encode_int32(3, this->error); +uint8_t *BluetoothDeviceClearCacheResponse::encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const { + uint8_t *__restrict__ pos = buffer.get_pos(); + ProtoEncode::encode_uint64(pos PROTO_ENCODE_DEBUG_ARG, 1, this->address); + ProtoEncode::encode_bool(pos PROTO_ENCODE_DEBUG_ARG, 2, this->success); + ProtoEncode::encode_int32(pos PROTO_ENCODE_DEBUG_ARG, 3, this->error); + return pos; } uint32_t BluetoothDeviceClearCacheResponse::calculate_size() const { uint32_t size = 0; @@ -2624,16 +2741,18 @@ uint32_t BluetoothDeviceClearCacheResponse::calculate_size() const { size += ProtoSize::calc_int32(1, this->error); return size; } -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)); +uint8_t *BluetoothScannerStateResponse::encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const { + uint8_t *__restrict__ pos = buffer.get_pos(); + ProtoEncode::encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, 1, static_cast(this->state)); + ProtoEncode::encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, 2, static_cast(this->mode)); + ProtoEncode::encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, 3, static_cast(this->configured_mode)); + return pos; } 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)); + size += this->state ? 2 : 0; + size += this->mode ? 2 : 0; + size += this->configured_mode ? 2 : 0; return size; } bool BluetoothScannerSetModeRequest::decode_varint(uint32_t field_id, proto_varint_value_t value) { @@ -2661,10 +2780,12 @@ bool SubscribeVoiceAssistantRequest::decode_varint(uint32_t field_id, proto_vari } return true; } -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); +uint8_t *VoiceAssistantAudioSettings::encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const { + uint8_t *__restrict__ pos = buffer.get_pos(); + ProtoEncode::encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, 1, this->noise_suppression_level); + ProtoEncode::encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, 2, this->auto_gain); + ProtoEncode::encode_float(pos PROTO_ENCODE_DEBUG_ARG, 3, this->volume_multiplier); + return pos; } uint32_t VoiceAssistantAudioSettings::calculate_size() const { uint32_t size = 0; @@ -2673,12 +2794,14 @@ uint32_t VoiceAssistantAudioSettings::calculate_size() const { size += ProtoSize::calc_float(1, this->volume_multiplier); return size; } -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_optional_sub_message(4, this->audio_settings); - buffer.encode_string(5, this->wake_word_phrase); +uint8_t *VoiceAssistantRequest::encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const { + uint8_t *__restrict__ pos = buffer.get_pos(); + ProtoEncode::encode_bool(pos PROTO_ENCODE_DEBUG_ARG, 1, this->start); + ProtoEncode::encode_string(pos PROTO_ENCODE_DEBUG_ARG, 2, this->conversation_id); + ProtoEncode::encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, 3, this->flags); + ProtoEncode::encode_optional_sub_message(pos PROTO_ENCODE_DEBUG_ARG, buffer, 4, this->audio_settings); + ProtoEncode::encode_string(pos PROTO_ENCODE_DEBUG_ARG, 5, this->wake_word_phrase); + return pos; } uint32_t VoiceAssistantRequest::calculate_size() const { uint32_t size = 0; @@ -2760,9 +2883,11 @@ bool VoiceAssistantAudio::decode_length(uint32_t field_id, ProtoLengthDelimited } return true; } -void VoiceAssistantAudio::encode(ProtoWriteBuffer &buffer) const { - buffer.encode_bytes(1, this->data, this->data_len); - buffer.encode_bool(2, this->end); +uint8_t *VoiceAssistantAudio::encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const { + uint8_t *__restrict__ pos = buffer.get_pos(); + ProtoEncode::encode_bytes(pos PROTO_ENCODE_DEBUG_ARG, 1, this->data, this->data_len); + ProtoEncode::encode_bool(pos PROTO_ENCODE_DEBUG_ARG, 2, this->end); + return pos; } uint32_t VoiceAssistantAudio::calculate_size() const { uint32_t size = 0; @@ -2833,18 +2958,24 @@ bool VoiceAssistantAnnounceRequest::decode_length(uint32_t field_id, ProtoLength } return true; } -void VoiceAssistantAnnounceFinished::encode(ProtoWriteBuffer &buffer) const { buffer.encode_bool(1, this->success); } +uint8_t *VoiceAssistantAnnounceFinished::encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const { + uint8_t *__restrict__ pos = buffer.get_pos(); + ProtoEncode::encode_bool(pos PROTO_ENCODE_DEBUG_ARG, 1, this->success); + return pos; +} 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); +uint8_t *VoiceAssistantWakeWord::encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const { + uint8_t *__restrict__ pos = buffer.get_pos(); + ProtoEncode::encode_string(pos PROTO_ENCODE_DEBUG_ARG, 1, this->id); + ProtoEncode::encode_string(pos PROTO_ENCODE_DEBUG_ARG, 2, this->wake_word); for (auto &it : this->trained_languages) { - buffer.encode_string(3, it, true); + ProtoEncode::encode_string(pos PROTO_ENCODE_DEBUG_ARG, 3, it, true); } + return pos; } uint32_t VoiceAssistantWakeWord::calculate_size() const { uint32_t size = 0; @@ -2908,14 +3039,16 @@ bool VoiceAssistantConfigurationRequest::decode_length(uint32_t field_id, ProtoL } return true; } -void VoiceAssistantConfigurationResponse::encode(ProtoWriteBuffer &buffer) const { +uint8_t *VoiceAssistantConfigurationResponse::encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const { + uint8_t *__restrict__ pos = buffer.get_pos(); for (auto &it : this->available_wake_words) { - buffer.encode_sub_message(1, it); + ProtoEncode::encode_sub_message(pos PROTO_ENCODE_DEBUG_ARG, buffer, 1, it); } for (const auto &it : *this->active_wake_words) { - buffer.encode_string(2, it, true); + ProtoEncode::encode_string(pos PROTO_ENCODE_DEBUG_ARG, 2, it, true); } - buffer.encode_uint32(3, this->max_active_wake_words); + ProtoEncode::encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, 3, this->max_active_wake_words); + return pos; } uint32_t VoiceAssistantConfigurationResponse::calculate_size() const { uint32_t size = 0; @@ -2944,32 +3077,34 @@ bool VoiceAssistantSetConfiguration::decode_length(uint32_t field_id, ProtoLengt } #endif #ifdef USE_ALARM_CONTROL_PANEL -void ListEntitiesAlarmControlPanelResponse::encode(ProtoWriteBuffer &buffer) const { - buffer.encode_string(1, this->object_id); - buffer.write_tag_and_fixed32(21, this->key); - buffer.encode_string(3, this->name); +uint8_t *ListEntitiesAlarmControlPanelResponse::encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const { + uint8_t *__restrict__ pos = buffer.get_pos(); + ProtoEncode::encode_short_string_force(pos PROTO_ENCODE_DEBUG_ARG, 10, this->object_id); + ProtoEncode::write_tag_and_fixed32(pos PROTO_ENCODE_DEBUG_ARG, 21, this->key); + ProtoEncode::encode_short_string_force(pos PROTO_ENCODE_DEBUG_ARG, 26, this->name); #ifdef USE_ENTITY_ICON - buffer.encode_string(5, this->icon); + ProtoEncode::encode_string(pos PROTO_ENCODE_DEBUG_ARG, 5, this->icon); #endif - buffer.encode_bool(6, this->disabled_by_default); - buffer.encode_uint32(7, static_cast(this->entity_category)); - buffer.encode_uint32(8, this->supported_features); - buffer.encode_bool(9, this->requires_code); - buffer.encode_bool(10, this->requires_code_to_arm); + ProtoEncode::encode_bool(pos PROTO_ENCODE_DEBUG_ARG, 6, this->disabled_by_default); + ProtoEncode::encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, 7, static_cast(this->entity_category)); + ProtoEncode::encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, 8, this->supported_features); + ProtoEncode::encode_bool(pos PROTO_ENCODE_DEBUG_ARG, 9, this->requires_code); + ProtoEncode::encode_bool(pos PROTO_ENCODE_DEBUG_ARG, 10, this->requires_code_to_arm); #ifdef USE_DEVICES - buffer.encode_uint32(11, this->device_id); + ProtoEncode::encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, 11, this->device_id); #endif + return pos; } uint32_t ListEntitiesAlarmControlPanelResponse::calculate_size() const { uint32_t size = 0; - size += ProtoSize::calc_length(1, this->object_id.size()); + size += 2 + this->object_id.size(); size += 5; - size += ProtoSize::calc_length(1, this->name.size()); + size += 2 + this->name.size(); #ifdef USE_ENTITY_ICON - size += ProtoSize::calc_length(1, this->icon.size()); + size += !this->icon.empty() ? 2 + this->icon.size() : 0; #endif size += ProtoSize::calc_bool(1, this->disabled_by_default); - size += ProtoSize::calc_uint32(1, static_cast(this->entity_category)); + size += this->entity_category ? 2 : 0; 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); @@ -2978,17 +3113,19 @@ uint32_t ListEntitiesAlarmControlPanelResponse::calculate_size() const { #endif return size; } -void AlarmControlPanelStateResponse::encode(ProtoWriteBuffer &buffer) const { - buffer.write_tag_and_fixed32(13, this->key); - buffer.encode_uint32(2, static_cast(this->state)); +uint8_t *AlarmControlPanelStateResponse::encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const { + uint8_t *__restrict__ pos = buffer.get_pos(); + ProtoEncode::write_tag_and_fixed32(pos PROTO_ENCODE_DEBUG_ARG, 13, this->key); + ProtoEncode::encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, 2, static_cast(this->state)); #ifdef USE_DEVICES - buffer.encode_uint32(3, this->device_id); + ProtoEncode::encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, 3, this->device_id); #endif + return pos; } uint32_t AlarmControlPanelStateResponse::calculate_size() const { uint32_t size = 0; size += 5; - size += ProtoSize::calc_uint32(1, static_cast(this->state)); + size += this->state ? 2 : 0; #ifdef USE_DEVICES size += ProtoSize::calc_uint32(1, this->device_id); #endif @@ -3032,49 +3169,53 @@ bool AlarmControlPanelCommandRequest::decode_32bit(uint32_t field_id, Proto32Bit } #endif #ifdef USE_TEXT -void ListEntitiesTextResponse::encode(ProtoWriteBuffer &buffer) const { - buffer.encode_string(1, this->object_id); - buffer.write_tag_and_fixed32(21, this->key); - buffer.encode_string(3, this->name); +uint8_t *ListEntitiesTextResponse::encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const { + uint8_t *__restrict__ pos = buffer.get_pos(); + ProtoEncode::encode_short_string_force(pos PROTO_ENCODE_DEBUG_ARG, 10, this->object_id); + ProtoEncode::write_tag_and_fixed32(pos PROTO_ENCODE_DEBUG_ARG, 21, this->key); + ProtoEncode::encode_short_string_force(pos PROTO_ENCODE_DEBUG_ARG, 26, this->name); #ifdef USE_ENTITY_ICON - buffer.encode_string(5, this->icon); + ProtoEncode::encode_string(pos PROTO_ENCODE_DEBUG_ARG, 5, this->icon); #endif - buffer.encode_bool(6, this->disabled_by_default); - buffer.encode_uint32(7, static_cast(this->entity_category)); - buffer.encode_uint32(8, this->min_length); - buffer.encode_uint32(9, this->max_length); - buffer.encode_string(10, this->pattern); - buffer.encode_uint32(11, static_cast(this->mode)); + ProtoEncode::encode_bool(pos PROTO_ENCODE_DEBUG_ARG, 6, this->disabled_by_default); + ProtoEncode::encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, 7, static_cast(this->entity_category)); + ProtoEncode::encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, 8, this->min_length); + ProtoEncode::encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, 9, this->max_length); + ProtoEncode::encode_string(pos PROTO_ENCODE_DEBUG_ARG, 10, this->pattern); + ProtoEncode::encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, 11, static_cast(this->mode)); #ifdef USE_DEVICES - buffer.encode_uint32(12, this->device_id); + ProtoEncode::encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, 12, this->device_id); #endif + return pos; } uint32_t ListEntitiesTextResponse::calculate_size() const { uint32_t size = 0; - size += ProtoSize::calc_length(1, this->object_id.size()); + size += 2 + this->object_id.size(); size += 5; - size += ProtoSize::calc_length(1, this->name.size()); + size += 2 + this->name.size(); #ifdef USE_ENTITY_ICON - size += ProtoSize::calc_length(1, this->icon.size()); + size += !this->icon.empty() ? 2 + this->icon.size() : 0; #endif size += ProtoSize::calc_bool(1, this->disabled_by_default); - size += ProtoSize::calc_uint32(1, static_cast(this->entity_category)); + size += this->entity_category ? 2 : 0; 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)); + size += this->mode ? 2 : 0; #ifdef USE_DEVICES size += ProtoSize::calc_uint32(1, this->device_id); #endif return size; } -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); +uint8_t *TextStateResponse::encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const { + uint8_t *__restrict__ pos = buffer.get_pos(); + ProtoEncode::write_tag_and_fixed32(pos PROTO_ENCODE_DEBUG_ARG, 13, this->key); + ProtoEncode::encode_string(pos PROTO_ENCODE_DEBUG_ARG, 2, this->state); + ProtoEncode::encode_bool(pos PROTO_ENCODE_DEBUG_ARG, 3, this->missing_state); #ifdef USE_DEVICES - buffer.encode_uint32(4, this->device_id); + ProtoEncode::encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, 4, this->device_id); #endif + return pos; } uint32_t TextStateResponse::calculate_size() const { uint32_t size = 0; @@ -3121,43 +3262,47 @@ bool TextCommandRequest::decode_32bit(uint32_t field_id, Proto32Bit value) { } #endif #ifdef USE_DATETIME_DATE -void ListEntitiesDateResponse::encode(ProtoWriteBuffer &buffer) const { - buffer.encode_string(1, this->object_id); - buffer.write_tag_and_fixed32(21, this->key); - buffer.encode_string(3, this->name); +uint8_t *ListEntitiesDateResponse::encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const { + uint8_t *__restrict__ pos = buffer.get_pos(); + ProtoEncode::encode_short_string_force(pos PROTO_ENCODE_DEBUG_ARG, 10, this->object_id); + ProtoEncode::write_tag_and_fixed32(pos PROTO_ENCODE_DEBUG_ARG, 21, this->key); + ProtoEncode::encode_short_string_force(pos PROTO_ENCODE_DEBUG_ARG, 26, this->name); #ifdef USE_ENTITY_ICON - buffer.encode_string(5, this->icon); + ProtoEncode::encode_string(pos PROTO_ENCODE_DEBUG_ARG, 5, this->icon); #endif - buffer.encode_bool(6, this->disabled_by_default); - buffer.encode_uint32(7, static_cast(this->entity_category)); + ProtoEncode::encode_bool(pos PROTO_ENCODE_DEBUG_ARG, 6, this->disabled_by_default); + ProtoEncode::encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, 7, static_cast(this->entity_category)); #ifdef USE_DEVICES - buffer.encode_uint32(8, this->device_id); + ProtoEncode::encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, 8, this->device_id); #endif + return pos; } uint32_t ListEntitiesDateResponse::calculate_size() const { uint32_t size = 0; - size += ProtoSize::calc_length(1, this->object_id.size()); + size += 2 + this->object_id.size(); size += 5; - size += ProtoSize::calc_length(1, this->name.size()); + size += 2 + this->name.size(); #ifdef USE_ENTITY_ICON - size += ProtoSize::calc_length(1, this->icon.size()); + size += !this->icon.empty() ? 2 + this->icon.size() : 0; #endif size += ProtoSize::calc_bool(1, this->disabled_by_default); - size += ProtoSize::calc_uint32(1, static_cast(this->entity_category)); + size += this->entity_category ? 2 : 0; #ifdef USE_DEVICES size += ProtoSize::calc_uint32(1, this->device_id); #endif return size; } -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); - buffer.encode_uint32(5, this->day); +uint8_t *DateStateResponse::encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const { + uint8_t *__restrict__ pos = buffer.get_pos(); + ProtoEncode::write_tag_and_fixed32(pos PROTO_ENCODE_DEBUG_ARG, 13, this->key); + ProtoEncode::encode_bool(pos PROTO_ENCODE_DEBUG_ARG, 2, this->missing_state); + ProtoEncode::encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, 3, this->year); + ProtoEncode::encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, 4, this->month); + ProtoEncode::encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, 5, this->day); #ifdef USE_DEVICES - buffer.encode_uint32(6, this->device_id); + ProtoEncode::encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, 6, this->device_id); #endif + return pos; } uint32_t DateStateResponse::calculate_size() const { uint32_t size = 0; @@ -3204,43 +3349,47 @@ bool DateCommandRequest::decode_32bit(uint32_t field_id, Proto32Bit value) { } #endif #ifdef USE_DATETIME_TIME -void ListEntitiesTimeResponse::encode(ProtoWriteBuffer &buffer) const { - buffer.encode_string(1, this->object_id); - buffer.write_tag_and_fixed32(21, this->key); - buffer.encode_string(3, this->name); +uint8_t *ListEntitiesTimeResponse::encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const { + uint8_t *__restrict__ pos = buffer.get_pos(); + ProtoEncode::encode_short_string_force(pos PROTO_ENCODE_DEBUG_ARG, 10, this->object_id); + ProtoEncode::write_tag_and_fixed32(pos PROTO_ENCODE_DEBUG_ARG, 21, this->key); + ProtoEncode::encode_short_string_force(pos PROTO_ENCODE_DEBUG_ARG, 26, this->name); #ifdef USE_ENTITY_ICON - buffer.encode_string(5, this->icon); + ProtoEncode::encode_string(pos PROTO_ENCODE_DEBUG_ARG, 5, this->icon); #endif - buffer.encode_bool(6, this->disabled_by_default); - buffer.encode_uint32(7, static_cast(this->entity_category)); + ProtoEncode::encode_bool(pos PROTO_ENCODE_DEBUG_ARG, 6, this->disabled_by_default); + ProtoEncode::encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, 7, static_cast(this->entity_category)); #ifdef USE_DEVICES - buffer.encode_uint32(8, this->device_id); + ProtoEncode::encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, 8, this->device_id); #endif + return pos; } uint32_t ListEntitiesTimeResponse::calculate_size() const { uint32_t size = 0; - size += ProtoSize::calc_length(1, this->object_id.size()); + size += 2 + this->object_id.size(); size += 5; - size += ProtoSize::calc_length(1, this->name.size()); + size += 2 + this->name.size(); #ifdef USE_ENTITY_ICON - size += ProtoSize::calc_length(1, this->icon.size()); + size += !this->icon.empty() ? 2 + this->icon.size() : 0; #endif size += ProtoSize::calc_bool(1, this->disabled_by_default); - size += ProtoSize::calc_uint32(1, static_cast(this->entity_category)); + size += this->entity_category ? 2 : 0; #ifdef USE_DEVICES size += ProtoSize::calc_uint32(1, this->device_id); #endif return size; } -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); - buffer.encode_uint32(5, this->second); +uint8_t *TimeStateResponse::encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const { + uint8_t *__restrict__ pos = buffer.get_pos(); + ProtoEncode::write_tag_and_fixed32(pos PROTO_ENCODE_DEBUG_ARG, 13, this->key); + ProtoEncode::encode_bool(pos PROTO_ENCODE_DEBUG_ARG, 2, this->missing_state); + ProtoEncode::encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, 3, this->hour); + ProtoEncode::encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, 4, this->minute); + ProtoEncode::encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, 5, this->second); #ifdef USE_DEVICES - buffer.encode_uint32(6, this->device_id); + ProtoEncode::encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, 6, this->device_id); #endif + return pos; } uint32_t TimeStateResponse::calculate_size() const { uint32_t size = 0; @@ -3287,34 +3436,36 @@ bool TimeCommandRequest::decode_32bit(uint32_t field_id, Proto32Bit value) { } #endif #ifdef USE_EVENT -void ListEntitiesEventResponse::encode(ProtoWriteBuffer &buffer) const { - buffer.encode_string(1, this->object_id); - buffer.write_tag_and_fixed32(21, this->key); - buffer.encode_string(3, this->name); +uint8_t *ListEntitiesEventResponse::encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const { + uint8_t *__restrict__ pos = buffer.get_pos(); + ProtoEncode::encode_short_string_force(pos PROTO_ENCODE_DEBUG_ARG, 10, this->object_id); + ProtoEncode::write_tag_and_fixed32(pos PROTO_ENCODE_DEBUG_ARG, 21, this->key); + ProtoEncode::encode_short_string_force(pos PROTO_ENCODE_DEBUG_ARG, 26, this->name); #ifdef USE_ENTITY_ICON - buffer.encode_string(5, this->icon); + ProtoEncode::encode_string(pos PROTO_ENCODE_DEBUG_ARG, 5, this->icon); #endif - buffer.encode_bool(6, this->disabled_by_default); - buffer.encode_uint32(7, static_cast(this->entity_category)); - buffer.encode_string(8, this->device_class); + ProtoEncode::encode_bool(pos PROTO_ENCODE_DEBUG_ARG, 6, this->disabled_by_default); + ProtoEncode::encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, 7, static_cast(this->entity_category)); + ProtoEncode::encode_string(pos PROTO_ENCODE_DEBUG_ARG, 8, this->device_class); for (const char *it : *this->event_types) { - buffer.encode_string(9, it, strlen(it), true); + ProtoEncode::encode_string(pos PROTO_ENCODE_DEBUG_ARG, 9, it, strlen(it), true); } #ifdef USE_DEVICES - buffer.encode_uint32(10, this->device_id); + ProtoEncode::encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, 10, this->device_id); #endif + return pos; } uint32_t ListEntitiesEventResponse::calculate_size() const { uint32_t size = 0; - size += ProtoSize::calc_length(1, this->object_id.size()); + size += 2 + this->object_id.size(); size += 5; - size += ProtoSize::calc_length(1, this->name.size()); + size += 2 + this->name.size(); #ifdef USE_ENTITY_ICON - size += ProtoSize::calc_length(1, this->icon.size()); + size += !this->icon.empty() ? 2 + this->icon.size() : 0; #endif 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 += this->entity_category ? 2 : 0; + size += !this->device_class.empty() ? 2 + this->device_class.size() : 0; if (!this->event_types->empty()) { for (const char *it : *this->event_types) { size += ProtoSize::calc_length_force(1, strlen(it)); @@ -3325,12 +3476,14 @@ uint32_t ListEntitiesEventResponse::calculate_size() const { #endif return size; } -void EventResponse::encode(ProtoWriteBuffer &buffer) const { - buffer.write_tag_and_fixed32(13, this->key); - buffer.encode_string(2, this->event_type); +uint8_t *EventResponse::encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const { + uint8_t *__restrict__ pos = buffer.get_pos(); + ProtoEncode::write_tag_and_fixed32(pos PROTO_ENCODE_DEBUG_ARG, 13, this->key); + ProtoEncode::encode_string(pos PROTO_ENCODE_DEBUG_ARG, 2, this->event_type); #ifdef USE_DEVICES - buffer.encode_uint32(3, this->device_id); + ProtoEncode::encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, 3, this->device_id); #endif + return pos; } uint32_t EventResponse::calculate_size() const { uint32_t size = 0; @@ -3343,34 +3496,36 @@ uint32_t EventResponse::calculate_size() const { } #endif #ifdef USE_VALVE -void ListEntitiesValveResponse::encode(ProtoWriteBuffer &buffer) const { - buffer.encode_string(1, this->object_id); - buffer.write_tag_and_fixed32(21, this->key); - buffer.encode_string(3, this->name); +uint8_t *ListEntitiesValveResponse::encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const { + uint8_t *__restrict__ pos = buffer.get_pos(); + ProtoEncode::encode_short_string_force(pos PROTO_ENCODE_DEBUG_ARG, 10, this->object_id); + ProtoEncode::write_tag_and_fixed32(pos PROTO_ENCODE_DEBUG_ARG, 21, this->key); + ProtoEncode::encode_short_string_force(pos PROTO_ENCODE_DEBUG_ARG, 26, this->name); #ifdef USE_ENTITY_ICON - buffer.encode_string(5, this->icon); + ProtoEncode::encode_string(pos PROTO_ENCODE_DEBUG_ARG, 5, this->icon); #endif - buffer.encode_bool(6, this->disabled_by_default); - buffer.encode_uint32(7, static_cast(this->entity_category)); - buffer.encode_string(8, this->device_class); - buffer.encode_bool(9, this->assumed_state); - buffer.encode_bool(10, this->supports_position); - buffer.encode_bool(11, this->supports_stop); + ProtoEncode::encode_bool(pos PROTO_ENCODE_DEBUG_ARG, 6, this->disabled_by_default); + ProtoEncode::encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, 7, static_cast(this->entity_category)); + ProtoEncode::encode_string(pos PROTO_ENCODE_DEBUG_ARG, 8, this->device_class); + ProtoEncode::encode_bool(pos PROTO_ENCODE_DEBUG_ARG, 9, this->assumed_state); + ProtoEncode::encode_bool(pos PROTO_ENCODE_DEBUG_ARG, 10, this->supports_position); + ProtoEncode::encode_bool(pos PROTO_ENCODE_DEBUG_ARG, 11, this->supports_stop); #ifdef USE_DEVICES - buffer.encode_uint32(12, this->device_id); + ProtoEncode::encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, 12, this->device_id); #endif + return pos; } uint32_t ListEntitiesValveResponse::calculate_size() const { uint32_t size = 0; - size += ProtoSize::calc_length(1, this->object_id.size()); + size += 2 + this->object_id.size(); size += 5; - size += ProtoSize::calc_length(1, this->name.size()); + size += 2 + this->name.size(); #ifdef USE_ENTITY_ICON - size += ProtoSize::calc_length(1, this->icon.size()); + size += !this->icon.empty() ? 2 + this->icon.size() : 0; #endif 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 += this->entity_category ? 2 : 0; + size += !this->device_class.empty() ? 2 + this->device_class.size() : 0; size += ProtoSize::calc_bool(1, this->assumed_state); size += ProtoSize::calc_bool(1, this->supports_position); size += ProtoSize::calc_bool(1, this->supports_stop); @@ -3379,19 +3534,21 @@ uint32_t ListEntitiesValveResponse::calculate_size() const { #endif return size; } -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)); +uint8_t *ValveStateResponse::encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const { + uint8_t *__restrict__ pos = buffer.get_pos(); + ProtoEncode::write_tag_and_fixed32(pos PROTO_ENCODE_DEBUG_ARG, 13, this->key); + ProtoEncode::encode_float(pos PROTO_ENCODE_DEBUG_ARG, 2, this->position); + ProtoEncode::encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, 3, static_cast(this->current_operation)); #ifdef USE_DEVICES - buffer.encode_uint32(4, this->device_id); + ProtoEncode::encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, 4, this->device_id); #endif + return pos; } 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)); + size += this->current_operation ? 2 : 0; #ifdef USE_DEVICES size += ProtoSize::calc_uint32(1, this->device_id); #endif @@ -3430,41 +3587,45 @@ bool ValveCommandRequest::decode_32bit(uint32_t field_id, Proto32Bit value) { } #endif #ifdef USE_DATETIME_DATETIME -void ListEntitiesDateTimeResponse::encode(ProtoWriteBuffer &buffer) const { - buffer.encode_string(1, this->object_id); - buffer.write_tag_and_fixed32(21, this->key); - buffer.encode_string(3, this->name); +uint8_t *ListEntitiesDateTimeResponse::encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const { + uint8_t *__restrict__ pos = buffer.get_pos(); + ProtoEncode::encode_short_string_force(pos PROTO_ENCODE_DEBUG_ARG, 10, this->object_id); + ProtoEncode::write_tag_and_fixed32(pos PROTO_ENCODE_DEBUG_ARG, 21, this->key); + ProtoEncode::encode_short_string_force(pos PROTO_ENCODE_DEBUG_ARG, 26, this->name); #ifdef USE_ENTITY_ICON - buffer.encode_string(5, this->icon); + ProtoEncode::encode_string(pos PROTO_ENCODE_DEBUG_ARG, 5, this->icon); #endif - buffer.encode_bool(6, this->disabled_by_default); - buffer.encode_uint32(7, static_cast(this->entity_category)); + ProtoEncode::encode_bool(pos PROTO_ENCODE_DEBUG_ARG, 6, this->disabled_by_default); + ProtoEncode::encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, 7, static_cast(this->entity_category)); #ifdef USE_DEVICES - buffer.encode_uint32(8, this->device_id); + ProtoEncode::encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, 8, this->device_id); #endif + return pos; } uint32_t ListEntitiesDateTimeResponse::calculate_size() const { uint32_t size = 0; - size += ProtoSize::calc_length(1, this->object_id.size()); + size += 2 + this->object_id.size(); size += 5; - size += ProtoSize::calc_length(1, this->name.size()); + size += 2 + this->name.size(); #ifdef USE_ENTITY_ICON - size += ProtoSize::calc_length(1, this->icon.size()); + size += !this->icon.empty() ? 2 + this->icon.size() : 0; #endif size += ProtoSize::calc_bool(1, this->disabled_by_default); - size += ProtoSize::calc_uint32(1, static_cast(this->entity_category)); + size += this->entity_category ? 2 : 0; #ifdef USE_DEVICES size += ProtoSize::calc_uint32(1, this->device_id); #endif return size; } -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); +uint8_t *DateTimeStateResponse::encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const { + uint8_t *__restrict__ pos = buffer.get_pos(); + ProtoEncode::write_tag_and_fixed32(pos PROTO_ENCODE_DEBUG_ARG, 13, this->key); + ProtoEncode::encode_bool(pos PROTO_ENCODE_DEBUG_ARG, 2, this->missing_state); + ProtoEncode::encode_fixed32(pos PROTO_ENCODE_DEBUG_ARG, 3, this->epoch_seconds); #ifdef USE_DEVICES - buffer.encode_uint32(4, this->device_id); + ProtoEncode::encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, 4, this->device_id); #endif + return pos; } uint32_t DateTimeStateResponse::calculate_size() const { uint32_t size = 0; @@ -3503,50 +3664,54 @@ bool DateTimeCommandRequest::decode_32bit(uint32_t field_id, Proto32Bit value) { } #endif #ifdef USE_UPDATE -void ListEntitiesUpdateResponse::encode(ProtoWriteBuffer &buffer) const { - buffer.encode_string(1, this->object_id); - buffer.write_tag_and_fixed32(21, this->key); - buffer.encode_string(3, this->name); +uint8_t *ListEntitiesUpdateResponse::encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const { + uint8_t *__restrict__ pos = buffer.get_pos(); + ProtoEncode::encode_short_string_force(pos PROTO_ENCODE_DEBUG_ARG, 10, this->object_id); + ProtoEncode::write_tag_and_fixed32(pos PROTO_ENCODE_DEBUG_ARG, 21, this->key); + ProtoEncode::encode_short_string_force(pos PROTO_ENCODE_DEBUG_ARG, 26, this->name); #ifdef USE_ENTITY_ICON - buffer.encode_string(5, this->icon); + ProtoEncode::encode_string(pos PROTO_ENCODE_DEBUG_ARG, 5, this->icon); #endif - buffer.encode_bool(6, this->disabled_by_default); - buffer.encode_uint32(7, static_cast(this->entity_category)); - buffer.encode_string(8, this->device_class); + ProtoEncode::encode_bool(pos PROTO_ENCODE_DEBUG_ARG, 6, this->disabled_by_default); + ProtoEncode::encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, 7, static_cast(this->entity_category)); + ProtoEncode::encode_string(pos PROTO_ENCODE_DEBUG_ARG, 8, this->device_class); #ifdef USE_DEVICES - buffer.encode_uint32(9, this->device_id); + ProtoEncode::encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, 9, this->device_id); #endif + return pos; } uint32_t ListEntitiesUpdateResponse::calculate_size() const { uint32_t size = 0; - size += ProtoSize::calc_length(1, this->object_id.size()); + size += 2 + this->object_id.size(); size += 5; - size += ProtoSize::calc_length(1, this->name.size()); + size += 2 + this->name.size(); #ifdef USE_ENTITY_ICON - size += ProtoSize::calc_length(1, this->icon.size()); + size += !this->icon.empty() ? 2 + this->icon.size() : 0; #endif 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 += this->entity_category ? 2 : 0; + size += !this->device_class.empty() ? 2 + this->device_class.size() : 0; #ifdef USE_DEVICES size += ProtoSize::calc_uint32(1, this->device_id); #endif return size; } -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); - buffer.encode_float(5, this->progress); - buffer.encode_string(6, this->current_version); - buffer.encode_string(7, this->latest_version); - buffer.encode_string(8, this->title); - buffer.encode_string(9, this->release_summary); - buffer.encode_string(10, this->release_url); +uint8_t *UpdateStateResponse::encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const { + uint8_t *__restrict__ pos = buffer.get_pos(); + ProtoEncode::write_tag_and_fixed32(pos PROTO_ENCODE_DEBUG_ARG, 13, this->key); + ProtoEncode::encode_bool(pos PROTO_ENCODE_DEBUG_ARG, 2, this->missing_state); + ProtoEncode::encode_bool(pos PROTO_ENCODE_DEBUG_ARG, 3, this->in_progress); + ProtoEncode::encode_bool(pos PROTO_ENCODE_DEBUG_ARG, 4, this->has_progress); + ProtoEncode::encode_float(pos PROTO_ENCODE_DEBUG_ARG, 5, this->progress); + ProtoEncode::encode_string(pos PROTO_ENCODE_DEBUG_ARG, 6, this->current_version); + ProtoEncode::encode_string(pos PROTO_ENCODE_DEBUG_ARG, 7, this->latest_version); + ProtoEncode::encode_string(pos PROTO_ENCODE_DEBUG_ARG, 8, this->title); + ProtoEncode::encode_string(pos PROTO_ENCODE_DEBUG_ARG, 9, this->release_summary); + ProtoEncode::encode_string(pos PROTO_ENCODE_DEBUG_ARG, 10, this->release_url); #ifdef USE_DEVICES - buffer.encode_uint32(11, this->device_id); + ProtoEncode::encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, 11, this->device_id); #endif + return pos; } uint32_t UpdateStateResponse::calculate_size() const { uint32_t size = 0; @@ -3604,7 +3769,11 @@ 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); } +uint8_t *ZWaveProxyFrame::encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const { + uint8_t *__restrict__ pos = buffer.get_pos(); + ProtoEncode::encode_bytes(pos PROTO_ENCODE_DEBUG_ARG, 1, this->data, this->data_len); + return pos; +} uint32_t ZWaveProxyFrame::calculate_size() const { uint32_t size = 0; size += ProtoSize::calc_length(1, this->data_len); @@ -3632,43 +3801,47 @@ bool ZWaveProxyRequest::decode_length(uint32_t field_id, ProtoLengthDelimited va } return true; } -void ZWaveProxyRequest::encode(ProtoWriteBuffer &buffer) const { - buffer.encode_uint32(1, static_cast(this->type)); - buffer.encode_bytes(2, this->data, this->data_len); +uint8_t *ZWaveProxyRequest::encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const { + uint8_t *__restrict__ pos = buffer.get_pos(); + ProtoEncode::encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, 1, static_cast(this->type)); + ProtoEncode::encode_bytes(pos PROTO_ENCODE_DEBUG_ARG, 2, this->data, this->data_len); + return pos; } uint32_t ZWaveProxyRequest::calculate_size() const { uint32_t size = 0; - size += ProtoSize::calc_uint32(1, static_cast(this->type)); + size += this->type ? 2 : 0; size += ProtoSize::calc_length(1, this->data_len); return size; } #endif #ifdef USE_INFRARED -void ListEntitiesInfraredResponse::encode(ProtoWriteBuffer &buffer) const { - buffer.encode_string(1, this->object_id); - buffer.write_tag_and_fixed32(21, this->key); - buffer.encode_string(3, this->name); +uint8_t *ListEntitiesInfraredResponse::encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const { + uint8_t *__restrict__ pos = buffer.get_pos(); + ProtoEncode::encode_short_string_force(pos PROTO_ENCODE_DEBUG_ARG, 10, this->object_id); + ProtoEncode::write_tag_and_fixed32(pos PROTO_ENCODE_DEBUG_ARG, 21, this->key); + ProtoEncode::encode_short_string_force(pos PROTO_ENCODE_DEBUG_ARG, 26, this->name); #ifdef USE_ENTITY_ICON - buffer.encode_string(4, this->icon); + ProtoEncode::encode_string(pos PROTO_ENCODE_DEBUG_ARG, 4, this->icon); #endif - buffer.encode_bool(5, this->disabled_by_default); - buffer.encode_uint32(6, static_cast(this->entity_category)); + ProtoEncode::encode_bool(pos PROTO_ENCODE_DEBUG_ARG, 5, this->disabled_by_default); + ProtoEncode::encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, 6, static_cast(this->entity_category)); #ifdef USE_DEVICES - buffer.encode_uint32(7, this->device_id); + ProtoEncode::encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, 7, this->device_id); #endif - buffer.encode_uint32(8, this->capabilities); - buffer.encode_uint32(9, this->receiver_frequency); + ProtoEncode::encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, 8, this->capabilities); + ProtoEncode::encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, 9, this->receiver_frequency); + return pos; } uint32_t ListEntitiesInfraredResponse::calculate_size() const { uint32_t size = 0; - size += ProtoSize::calc_length(1, this->object_id.size()); + size += 2 + this->object_id.size(); size += 5; - size += ProtoSize::calc_length(1, this->name.size()); + size += 2 + this->name.size(); #ifdef USE_ENTITY_ICON - size += ProtoSize::calc_length(1, this->icon.size()); + size += !this->icon.empty() ? 2 + this->icon.size() : 0; #endif size += ProtoSize::calc_bool(1, this->disabled_by_default); - size += ProtoSize::calc_uint32(1, static_cast(this->entity_category)); + size += this->entity_category ? 2 : 0; #ifdef USE_DEVICES size += ProtoSize::calc_uint32(1, this->device_id); #endif @@ -3719,14 +3892,16 @@ bool InfraredRFTransmitRawTimingsRequest::decode_32bit(uint32_t field_id, Proto3 } return true; } -void InfraredRFReceiveEvent::encode(ProtoWriteBuffer &buffer) const { +uint8_t *InfraredRFReceiveEvent::encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const { + uint8_t *__restrict__ pos = buffer.get_pos(); #ifdef USE_DEVICES - buffer.encode_uint32(1, this->device_id); + ProtoEncode::encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, 1, this->device_id); #endif - buffer.write_tag_and_fixed32(21, this->key); + ProtoEncode::write_tag_and_fixed32(pos PROTO_ENCODE_DEBUG_ARG, 21, this->key); for (const auto &it : *this->timings) { - buffer.encode_sint32(3, it, true); + ProtoEncode::encode_sint32(pos PROTO_ENCODE_DEBUG_ARG, 3, it, true); } + return pos; } uint32_t InfraredRFReceiveEvent::calculate_size() const { uint32_t size = 0; @@ -3768,9 +3943,11 @@ bool SerialProxyConfigureRequest::decode_varint(uint32_t field_id, proto_varint_ } return true; } -void SerialProxyDataReceived::encode(ProtoWriteBuffer &buffer) const { - buffer.encode_uint32(1, this->instance); - buffer.encode_bytes(2, this->data_ptr_, this->data_len_); +uint8_t *SerialProxyDataReceived::encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const { + uint8_t *__restrict__ pos = buffer.get_pos(); + ProtoEncode::encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, 1, this->instance); + ProtoEncode::encode_bytes(pos PROTO_ENCODE_DEBUG_ARG, 2, this->data_ptr_, this->data_len_); + return pos; } uint32_t SerialProxyDataReceived::calculate_size() const { uint32_t size = 0; @@ -3823,9 +4000,11 @@ bool SerialProxyGetModemPinsRequest::decode_varint(uint32_t field_id, proto_vari } return true; } -void SerialProxyGetModemPinsResponse::encode(ProtoWriteBuffer &buffer) const { - buffer.encode_uint32(1, this->instance); - buffer.encode_uint32(2, this->line_states); +uint8_t *SerialProxyGetModemPinsResponse::encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const { + uint8_t *__restrict__ pos = buffer.get_pos(); + ProtoEncode::encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, 1, this->instance); + ProtoEncode::encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, 2, this->line_states); + return pos; } uint32_t SerialProxyGetModemPinsResponse::calculate_size() const { uint32_t size = 0; @@ -3846,17 +4025,19 @@ bool SerialProxyRequest::decode_varint(uint32_t field_id, proto_varint_value_t v } 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); +uint8_t *SerialProxyRequestResponse::encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const { + uint8_t *__restrict__ pos = buffer.get_pos(); + ProtoEncode::encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, 1, this->instance); + ProtoEncode::encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, 2, static_cast(this->type)); + ProtoEncode::encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, 3, static_cast(this->status)); + ProtoEncode::encode_string(pos PROTO_ENCODE_DEBUG_ARG, 4, this->error_message); + return pos; } 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 += this->type ? 2 : 0; + size += this->status ? 2 : 0; size += ProtoSize::calc_length(1, this->error_message.size()); return size; } @@ -3884,9 +4065,11 @@ bool BluetoothSetConnectionParamsRequest::decode_varint(uint32_t field_id, proto } return true; } -void BluetoothSetConnectionParamsResponse::encode(ProtoWriteBuffer &buffer) const { - buffer.encode_uint64(1, this->address); - buffer.encode_int32(2, this->error); +uint8_t *BluetoothSetConnectionParamsResponse::encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const { + uint8_t *__restrict__ pos = buffer.get_pos(); + ProtoEncode::encode_uint64(pos PROTO_ENCODE_DEBUG_ARG, 1, this->address); + ProtoEncode::encode_int32(pos PROTO_ENCODE_DEBUG_ARG, 2, this->error); + return pos; } uint32_t BluetoothSetConnectionParamsResponse::calculate_size() const { uint32_t size = 0; diff --git a/esphome/components/api/api_pb2.h b/esphome/components/api/api_pb2.h index 14f6c704ae..3b239db36c 100644 --- a/esphome/components/api/api_pb2.h +++ b/esphome/components/api/api_pb2.h @@ -412,7 +412,7 @@ class HelloResponse final : public ProtoMessage { uint32_t api_version_minor{0}; StringRef server_info{}; StringRef name{}; - void encode(ProtoWriteBuffer &buffer) const; + uint8_t *encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const; uint32_t calculate_size() const; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; @@ -477,7 +477,7 @@ class AreaInfo final : public ProtoMessage { public: uint32_t area_id{0}; StringRef name{}; - void encode(ProtoWriteBuffer &buffer) const; + uint8_t *encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const; uint32_t calculate_size() const; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; @@ -492,7 +492,7 @@ class DeviceInfo final : public ProtoMessage { uint32_t device_id{0}; StringRef name{}; uint32_t area_id{0}; - void encode(ProtoWriteBuffer &buffer) const; + uint8_t *encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const; uint32_t calculate_size() const; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; @@ -506,7 +506,7 @@ class SerialProxyInfo final : public ProtoMessage { public: StringRef name{}; enums::SerialProxyPortType port_type{}; - void encode(ProtoWriteBuffer &buffer) const; + uint8_t *encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const; uint32_t calculate_size() const; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; @@ -574,7 +574,7 @@ class DeviceInfoResponse final : public ProtoMessage { #ifdef USE_SERIAL_PROXY std::array serial_proxies{}; #endif - void encode(ProtoWriteBuffer &buffer) const; + uint8_t *encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const; uint32_t calculate_size() const; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; @@ -605,7 +605,7 @@ class ListEntitiesBinarySensorResponse final : public InfoResponseProtoMessage { #endif StringRef device_class{}; bool is_status_binary_sensor{false}; - void encode(ProtoWriteBuffer &buffer) const; + uint8_t *encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const; uint32_t calculate_size() const; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; @@ -622,7 +622,7 @@ class BinarySensorStateResponse final : public StateResponseProtoMessage { #endif bool state{false}; bool missing_state{false}; - void encode(ProtoWriteBuffer &buffer) const; + uint8_t *encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const; uint32_t calculate_size() const; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; @@ -644,7 +644,7 @@ class ListEntitiesCoverResponse final : public InfoResponseProtoMessage { bool supports_tilt{false}; StringRef device_class{}; bool supports_stop{false}; - void encode(ProtoWriteBuffer &buffer) const; + uint8_t *encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const; uint32_t calculate_size() const; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; @@ -662,7 +662,7 @@ class CoverStateResponse final : public StateResponseProtoMessage { float position{0.0f}; float tilt{0.0f}; enums::CoverOperation current_operation{}; - void encode(ProtoWriteBuffer &buffer) const; + uint8_t *encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const; uint32_t calculate_size() const; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; @@ -704,7 +704,7 @@ class ListEntitiesFanResponse final : public InfoResponseProtoMessage { bool supports_direction{false}; int32_t supported_speed_count{0}; const std::vector *supported_preset_modes{}; - void encode(ProtoWriteBuffer &buffer) const; + uint8_t *encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const; uint32_t calculate_size() const; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; @@ -724,7 +724,7 @@ class FanStateResponse final : public StateResponseProtoMessage { enums::FanDirection direction{}; int32_t speed_level{0}; StringRef preset_mode{}; - void encode(ProtoWriteBuffer &buffer) const; + uint8_t *encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const; uint32_t calculate_size() const; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; @@ -771,7 +771,7 @@ class ListEntitiesLightResponse final : public InfoResponseProtoMessage { float min_mireds{0.0f}; float max_mireds{0.0f}; const FixedVector *effects{}; - void encode(ProtoWriteBuffer &buffer) const; + uint8_t *encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const; uint32_t calculate_size() const; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; @@ -798,7 +798,7 @@ class LightStateResponse final : public StateResponseProtoMessage { float cold_white{0.0f}; float warm_white{0.0f}; StringRef effect{}; - void encode(ProtoWriteBuffer &buffer) const; + uint8_t *encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const; uint32_t calculate_size() const; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; @@ -862,7 +862,7 @@ class ListEntitiesSensorResponse final : public InfoResponseProtoMessage { bool force_update{false}; StringRef device_class{}; enums::SensorStateClass state_class{}; - void encode(ProtoWriteBuffer &buffer) const; + uint8_t *encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const; uint32_t calculate_size() const; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; @@ -879,7 +879,7 @@ class SensorStateResponse final : public StateResponseProtoMessage { #endif float state{0.0f}; bool missing_state{false}; - void encode(ProtoWriteBuffer &buffer) const; + uint8_t *encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const; uint32_t calculate_size() const; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; @@ -898,7 +898,7 @@ class ListEntitiesSwitchResponse final : public InfoResponseProtoMessage { #endif bool assumed_state{false}; StringRef device_class{}; - void encode(ProtoWriteBuffer &buffer) const; + uint8_t *encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const; uint32_t calculate_size() const; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; @@ -914,7 +914,7 @@ class SwitchStateResponse final : public StateResponseProtoMessage { const LogString *message_name() const override { return LOG_STR("switch_state_response"); } #endif bool state{false}; - void encode(ProtoWriteBuffer &buffer) const; + uint8_t *encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const; uint32_t calculate_size() const; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; @@ -948,7 +948,7 @@ class ListEntitiesTextSensorResponse final : public InfoResponseProtoMessage { const LogString *message_name() const override { return LOG_STR("list_entities_text_sensor_response"); } #endif StringRef device_class{}; - void encode(ProtoWriteBuffer &buffer) const; + uint8_t *encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const; uint32_t calculate_size() const; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; @@ -965,7 +965,7 @@ class TextSensorStateResponse final : public StateResponseProtoMessage { #endif StringRef state{}; bool missing_state{false}; - void encode(ProtoWriteBuffer &buffer) const; + uint8_t *encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const; uint32_t calculate_size() const; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; @@ -1004,7 +1004,7 @@ class SubscribeLogsResponse final : public ProtoMessage { this->message_ptr_ = data; this->message_len_ = len; } - void encode(ProtoWriteBuffer &buffer) const; + uint8_t *encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const; uint32_t calculate_size() const; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; @@ -1037,7 +1037,7 @@ class NoiseEncryptionSetKeyResponse final : public ProtoMessage { const LogString *message_name() const override { return LOG_STR("noise_encryption_set_key_response"); } #endif bool success{false}; - void encode(ProtoWriteBuffer &buffer) const; + uint8_t *encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const; uint32_t calculate_size() const; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; @@ -1051,7 +1051,7 @@ class HomeassistantServiceMap final : public ProtoMessage { public: StringRef key{}; StringRef value{}; - void encode(ProtoWriteBuffer &buffer) const; + uint8_t *encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const; uint32_t calculate_size() const; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; @@ -1080,7 +1080,7 @@ class HomeassistantActionRequest final : public ProtoMessage { #ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES_JSON StringRef response_template{}; #endif - void encode(ProtoWriteBuffer &buffer) const; + uint8_t *encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const; uint32_t calculate_size() const; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; @@ -1124,7 +1124,7 @@ class SubscribeHomeAssistantStateResponse final : public ProtoMessage { StringRef entity_id{}; StringRef attribute{}; bool once{false}; - void encode(ProtoWriteBuffer &buffer) const; + uint8_t *encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const; uint32_t calculate_size() const; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; @@ -1215,7 +1215,7 @@ class ListEntitiesServicesArgument final : public ProtoMessage { public: StringRef name{}; enums::ServiceArgType type{}; - void encode(ProtoWriteBuffer &buffer) const; + uint8_t *encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const; uint32_t calculate_size() const; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; @@ -1234,7 +1234,7 @@ class ListEntitiesServicesResponse final : public ProtoMessage { uint32_t key{0}; FixedVector args{}; enums::SupportsResponseType supports_response{}; - void encode(ProtoWriteBuffer &buffer) const; + uint8_t *encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const; uint32_t calculate_size() const; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; @@ -1304,7 +1304,7 @@ class ExecuteServiceResponse final : public ProtoMessage { const uint8_t *response_data{nullptr}; uint16_t response_data_len{0}; #endif - void encode(ProtoWriteBuffer &buffer) const; + uint8_t *encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const; uint32_t calculate_size() const; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; @@ -1321,7 +1321,7 @@ class ListEntitiesCameraResponse final : public InfoResponseProtoMessage { #ifdef HAS_PROTO_MESSAGE_DUMP const LogString *message_name() const override { return LOG_STR("list_entities_camera_response"); } #endif - void encode(ProtoWriteBuffer &buffer) const; + uint8_t *encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const; uint32_t calculate_size() const; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; @@ -1343,7 +1343,7 @@ class CameraImageResponse final : public StateResponseProtoMessage { this->data_len_ = len; } bool done{false}; - void encode(ProtoWriteBuffer &buffer) const; + uint8_t *encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const; uint32_t calculate_size() const; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; @@ -1394,7 +1394,7 @@ 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; + uint8_t *encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const; uint32_t calculate_size() const; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; @@ -1422,7 +1422,7 @@ class ClimateStateResponse final : public StateResponseProtoMessage { StringRef custom_preset{}; float current_humidity{0.0f}; float target_humidity{0.0f}; - void encode(ProtoWriteBuffer &buffer) const; + uint8_t *encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const; uint32_t calculate_size() const; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; @@ -1480,7 +1480,7 @@ class ListEntitiesWaterHeaterResponse final : public InfoResponseProtoMessage { float target_temperature_step{0.0f}; const water_heater::WaterHeaterModeMask *supported_modes{}; uint32_t supported_features{0}; - void encode(ProtoWriteBuffer &buffer) const; + uint8_t *encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const; uint32_t calculate_size() const; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; @@ -1501,7 +1501,7 @@ 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; + uint8_t *encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const; uint32_t calculate_size() const; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; @@ -1545,7 +1545,7 @@ class ListEntitiesNumberResponse final : public InfoResponseProtoMessage { StringRef unit_of_measurement{}; enums::NumberMode mode{}; StringRef device_class{}; - void encode(ProtoWriteBuffer &buffer) const; + uint8_t *encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const; uint32_t calculate_size() const; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; @@ -1562,7 +1562,7 @@ class NumberStateResponse final : public StateResponseProtoMessage { #endif float state{0.0f}; bool missing_state{false}; - void encode(ProtoWriteBuffer &buffer) const; + uint8_t *encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const; uint32_t calculate_size() const; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; @@ -1596,7 +1596,7 @@ class ListEntitiesSelectResponse final : public InfoResponseProtoMessage { const LogString *message_name() const override { return LOG_STR("list_entities_select_response"); } #endif const FixedVector *options{}; - void encode(ProtoWriteBuffer &buffer) const; + uint8_t *encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const; uint32_t calculate_size() const; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; @@ -1613,7 +1613,7 @@ class SelectStateResponse final : public StateResponseProtoMessage { #endif StringRef state{}; bool missing_state{false}; - void encode(ProtoWriteBuffer &buffer) const; + uint8_t *encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const; uint32_t calculate_size() const; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; @@ -1650,7 +1650,7 @@ class ListEntitiesSirenResponse final : public InfoResponseProtoMessage { const FixedVector *tones{}; bool supports_duration{false}; bool supports_volume{false}; - void encode(ProtoWriteBuffer &buffer) const; + uint8_t *encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const; uint32_t calculate_size() const; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; @@ -1666,7 +1666,7 @@ class SirenStateResponse final : public StateResponseProtoMessage { const LogString *message_name() const override { return LOG_STR("siren_state_response"); } #endif bool state{false}; - void encode(ProtoWriteBuffer &buffer) const; + uint8_t *encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const; uint32_t calculate_size() const; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; @@ -1711,7 +1711,7 @@ class ListEntitiesLockResponse final : public InfoResponseProtoMessage { bool supports_open{false}; bool requires_code{false}; StringRef code_format{}; - void encode(ProtoWriteBuffer &buffer) const; + uint8_t *encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const; uint32_t calculate_size() const; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; @@ -1727,7 +1727,7 @@ class LockStateResponse final : public StateResponseProtoMessage { const LogString *message_name() const override { return LOG_STR("lock_state_response"); } #endif enums::LockState state{}; - void encode(ProtoWriteBuffer &buffer) const; + uint8_t *encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const; uint32_t calculate_size() const; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; @@ -1764,7 +1764,7 @@ class ListEntitiesButtonResponse final : public InfoResponseProtoMessage { const LogString *message_name() const override { return LOG_STR("list_entities_button_response"); } #endif StringRef device_class{}; - void encode(ProtoWriteBuffer &buffer) const; + uint8_t *encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const; uint32_t calculate_size() const; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; @@ -1796,7 +1796,7 @@ class MediaPlayerSupportedFormat final : public ProtoMessage { uint32_t num_channels{0}; enums::MediaPlayerFormatPurpose purpose{}; uint32_t sample_bytes{0}; - void encode(ProtoWriteBuffer &buffer) const; + uint8_t *encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const; uint32_t calculate_size() const; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; @@ -1814,7 +1814,7 @@ class ListEntitiesMediaPlayerResponse final : public InfoResponseProtoMessage { bool supports_pause{false}; std::vector supported_formats{}; uint32_t feature_flags{0}; - void encode(ProtoWriteBuffer &buffer) const; + uint8_t *encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const; uint32_t calculate_size() const; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; @@ -1832,7 +1832,7 @@ class MediaPlayerStateResponse final : public StateResponseProtoMessage { enums::MediaPlayerState state{}; float volume{0.0f}; bool muted{false}; - void encode(ProtoWriteBuffer &buffer) const; + uint8_t *encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const; uint32_t calculate_size() const; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; @@ -1888,7 +1888,7 @@ class BluetoothLERawAdvertisement final : public ProtoMessage { uint32_t address_type{0}; uint8_t data[62]{}; uint8_t data_len{0}; - void encode(ProtoWriteBuffer &buffer) const; + uint8_t *encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const; uint32_t calculate_size() const; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; @@ -1905,7 +1905,7 @@ class BluetoothLERawAdvertisementsResponse final : public ProtoMessage { #endif std::array advertisements{}; uint16_t advertisements_len{0}; - void encode(ProtoWriteBuffer &buffer) const; + uint8_t *encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const; uint32_t calculate_size() const; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; @@ -1942,7 +1942,7 @@ class BluetoothDeviceConnectionResponse final : public ProtoMessage { bool connected{false}; uint32_t mtu{0}; int32_t error{0}; - void encode(ProtoWriteBuffer &buffer) const; + uint8_t *encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const; uint32_t calculate_size() const; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; @@ -1970,7 +1970,7 @@ class BluetoothGATTDescriptor final : public ProtoMessage { std::array uuid{}; uint32_t handle{0}; uint32_t short_uuid{0}; - void encode(ProtoWriteBuffer &buffer) const; + uint8_t *encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const; uint32_t calculate_size() const; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; @@ -1985,7 +1985,7 @@ class BluetoothGATTCharacteristic final : public ProtoMessage { uint32_t properties{0}; FixedVector descriptors{}; uint32_t short_uuid{0}; - void encode(ProtoWriteBuffer &buffer) const; + uint8_t *encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const; uint32_t calculate_size() const; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; @@ -1999,7 +1999,7 @@ class BluetoothGATTService final : public ProtoMessage { uint32_t handle{0}; FixedVector characteristics{}; uint32_t short_uuid{0}; - void encode(ProtoWriteBuffer &buffer) const; + uint8_t *encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const; uint32_t calculate_size() const; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; @@ -2016,7 +2016,7 @@ class BluetoothGATTGetServicesResponse final : public ProtoMessage { #endif uint64_t address{0}; std::vector services{}; - void encode(ProtoWriteBuffer &buffer) const; + uint8_t *encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const; uint32_t calculate_size() const; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; @@ -2032,7 +2032,7 @@ class BluetoothGATTGetServicesDoneResponse final : public ProtoMessage { 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; + uint8_t *encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const; uint32_t calculate_size() const; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; @@ -2071,7 +2071,7 @@ class BluetoothGATTReadResponse final : public ProtoMessage { this->data_ptr_ = data; this->data_len_ = len; } - void encode(ProtoWriteBuffer &buffer) const; + uint8_t *encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const; uint32_t calculate_size() const; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; @@ -2166,7 +2166,7 @@ class BluetoothGATTNotifyDataResponse final : public ProtoMessage { this->data_ptr_ = data; this->data_len_ = len; } - void encode(ProtoWriteBuffer &buffer) const; + uint8_t *encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const; uint32_t calculate_size() const; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; @@ -2184,7 +2184,7 @@ class BluetoothConnectionsFreeResponse final : public ProtoMessage { uint32_t free{0}; uint32_t limit{0}; std::array allocated{}; - void encode(ProtoWriteBuffer &buffer) const; + uint8_t *encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const; uint32_t calculate_size() const; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; @@ -2202,7 +2202,7 @@ class BluetoothGATTErrorResponse final : public ProtoMessage { uint64_t address{0}; uint32_t handle{0}; int32_t error{0}; - void encode(ProtoWriteBuffer &buffer) const; + uint8_t *encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const; uint32_t calculate_size() const; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; @@ -2219,7 +2219,7 @@ class BluetoothGATTWriteResponse final : public ProtoMessage { #endif uint64_t address{0}; uint32_t handle{0}; - void encode(ProtoWriteBuffer &buffer) const; + uint8_t *encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const; uint32_t calculate_size() const; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; @@ -2236,7 +2236,7 @@ class BluetoothGATTNotifyResponse final : public ProtoMessage { #endif uint64_t address{0}; uint32_t handle{0}; - void encode(ProtoWriteBuffer &buffer) const; + uint8_t *encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const; uint32_t calculate_size() const; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; @@ -2254,7 +2254,7 @@ class BluetoothDevicePairingResponse final : public ProtoMessage { uint64_t address{0}; bool paired{false}; int32_t error{0}; - void encode(ProtoWriteBuffer &buffer) const; + uint8_t *encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const; uint32_t calculate_size() const; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; @@ -2272,7 +2272,7 @@ class BluetoothDeviceUnpairingResponse final : public ProtoMessage { uint64_t address{0}; bool success{false}; int32_t error{0}; - void encode(ProtoWriteBuffer &buffer) const; + uint8_t *encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const; uint32_t calculate_size() const; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; @@ -2290,7 +2290,7 @@ class BluetoothDeviceClearCacheResponse final : public ProtoMessage { uint64_t address{0}; bool success{false}; int32_t error{0}; - void encode(ProtoWriteBuffer &buffer) const; + uint8_t *encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const; uint32_t calculate_size() const; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; @@ -2308,7 +2308,7 @@ class BluetoothScannerStateResponse final : public ProtoMessage { enums::BluetoothScannerState state{}; enums::BluetoothScannerMode mode{}; enums::BluetoothScannerMode configured_mode{}; - void encode(ProtoWriteBuffer &buffer) const; + uint8_t *encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const; uint32_t calculate_size() const; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; @@ -2354,7 +2354,7 @@ class VoiceAssistantAudioSettings final : public ProtoMessage { uint32_t noise_suppression_level{0}; uint32_t auto_gain{0}; float volume_multiplier{0.0f}; - void encode(ProtoWriteBuffer &buffer) const; + uint8_t *encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const; uint32_t calculate_size() const; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; @@ -2374,7 +2374,7 @@ class VoiceAssistantRequest final : public ProtoMessage { uint32_t flags{0}; VoiceAssistantAudioSettings audio_settings{}; StringRef wake_word_phrase{}; - void encode(ProtoWriteBuffer &buffer) const; + uint8_t *encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const; uint32_t calculate_size() const; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; @@ -2436,7 +2436,7 @@ class VoiceAssistantAudio final : public ProtoDecodableMessage { const uint8_t *data{nullptr}; uint16_t data_len{0}; bool end{false}; - void encode(ProtoWriteBuffer &buffer) const; + uint8_t *encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const; uint32_t calculate_size() const; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; @@ -2494,7 +2494,7 @@ class VoiceAssistantAnnounceFinished final : public ProtoMessage { const LogString *message_name() const override { return LOG_STR("voice_assistant_announce_finished"); } #endif bool success{false}; - void encode(ProtoWriteBuffer &buffer) const; + uint8_t *encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const; uint32_t calculate_size() const; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; @@ -2507,7 +2507,7 @@ class VoiceAssistantWakeWord final : public ProtoMessage { StringRef id{}; StringRef wake_word{}; std::vector trained_languages{}; - void encode(ProtoWriteBuffer &buffer) const; + uint8_t *encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const; uint32_t calculate_size() const; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; @@ -2557,7 +2557,7 @@ class VoiceAssistantConfigurationResponse final : public ProtoMessage { std::vector available_wake_words{}; const std::vector *active_wake_words{}; uint32_t max_active_wake_words{0}; - void encode(ProtoWriteBuffer &buffer) const; + uint8_t *encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const; uint32_t calculate_size() const; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; @@ -2592,7 +2592,7 @@ class ListEntitiesAlarmControlPanelResponse final : public InfoResponseProtoMess uint32_t supported_features{0}; bool requires_code{false}; bool requires_code_to_arm{false}; - void encode(ProtoWriteBuffer &buffer) const; + uint8_t *encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const; uint32_t calculate_size() const; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; @@ -2608,7 +2608,7 @@ class AlarmControlPanelStateResponse final : public StateResponseProtoMessage { const LogString *message_name() const override { return LOG_STR("alarm_control_panel_state_response"); } #endif enums::AlarmControlPanelState state{}; - void encode(ProtoWriteBuffer &buffer) const; + uint8_t *encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const; uint32_t calculate_size() const; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; @@ -2647,7 +2647,7 @@ class ListEntitiesTextResponse final : public InfoResponseProtoMessage { uint32_t max_length{0}; StringRef pattern{}; enums::TextMode mode{}; - void encode(ProtoWriteBuffer &buffer) const; + uint8_t *encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const; uint32_t calculate_size() const; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; @@ -2664,7 +2664,7 @@ class TextStateResponse final : public StateResponseProtoMessage { #endif StringRef state{}; bool missing_state{false}; - void encode(ProtoWriteBuffer &buffer) const; + uint8_t *encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const; uint32_t calculate_size() const; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; @@ -2698,7 +2698,7 @@ class ListEntitiesDateResponse final : public InfoResponseProtoMessage { #ifdef HAS_PROTO_MESSAGE_DUMP const LogString *message_name() const override { return LOG_STR("list_entities_date_response"); } #endif - void encode(ProtoWriteBuffer &buffer) const; + uint8_t *encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const; uint32_t calculate_size() const; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; @@ -2717,7 +2717,7 @@ class DateStateResponse final : public StateResponseProtoMessage { uint32_t year{0}; uint32_t month{0}; uint32_t day{0}; - void encode(ProtoWriteBuffer &buffer) const; + uint8_t *encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const; uint32_t calculate_size() const; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; @@ -2752,7 +2752,7 @@ class ListEntitiesTimeResponse final : public InfoResponseProtoMessage { #ifdef HAS_PROTO_MESSAGE_DUMP const LogString *message_name() const override { return LOG_STR("list_entities_time_response"); } #endif - void encode(ProtoWriteBuffer &buffer) const; + uint8_t *encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const; uint32_t calculate_size() const; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; @@ -2771,7 +2771,7 @@ class TimeStateResponse final : public StateResponseProtoMessage { uint32_t hour{0}; uint32_t minute{0}; uint32_t second{0}; - void encode(ProtoWriteBuffer &buffer) const; + uint8_t *encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const; uint32_t calculate_size() const; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; @@ -2808,7 +2808,7 @@ class ListEntitiesEventResponse final : public InfoResponseProtoMessage { #endif StringRef device_class{}; const FixedVector *event_types{}; - void encode(ProtoWriteBuffer &buffer) const; + uint8_t *encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const; uint32_t calculate_size() const; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; @@ -2824,7 +2824,7 @@ class EventResponse final : public StateResponseProtoMessage { const LogString *message_name() const override { return LOG_STR("event_response"); } #endif StringRef event_type{}; - void encode(ProtoWriteBuffer &buffer) const; + uint8_t *encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const; uint32_t calculate_size() const; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; @@ -2845,7 +2845,7 @@ class ListEntitiesValveResponse final : public InfoResponseProtoMessage { bool assumed_state{false}; bool supports_position{false}; bool supports_stop{false}; - void encode(ProtoWriteBuffer &buffer) const; + uint8_t *encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const; uint32_t calculate_size() const; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; @@ -2862,7 +2862,7 @@ class ValveStateResponse final : public StateResponseProtoMessage { #endif float position{0.0f}; enums::ValveOperation current_operation{}; - void encode(ProtoWriteBuffer &buffer) const; + uint8_t *encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const; uint32_t calculate_size() const; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; @@ -2897,7 +2897,7 @@ class ListEntitiesDateTimeResponse final : public InfoResponseProtoMessage { #ifdef HAS_PROTO_MESSAGE_DUMP const LogString *message_name() const override { return LOG_STR("list_entities_date_time_response"); } #endif - void encode(ProtoWriteBuffer &buffer) const; + uint8_t *encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const; uint32_t calculate_size() const; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; @@ -2914,7 +2914,7 @@ class DateTimeStateResponse final : public StateResponseProtoMessage { #endif bool missing_state{false}; uint32_t epoch_seconds{0}; - void encode(ProtoWriteBuffer &buffer) const; + uint8_t *encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const; uint32_t calculate_size() const; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; @@ -2948,7 +2948,7 @@ class ListEntitiesUpdateResponse final : public InfoResponseProtoMessage { const LogString *message_name() const override { return LOG_STR("list_entities_update_response"); } #endif StringRef device_class{}; - void encode(ProtoWriteBuffer &buffer) const; + uint8_t *encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const; uint32_t calculate_size() const; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; @@ -2972,7 +2972,7 @@ class UpdateStateResponse final : public StateResponseProtoMessage { StringRef title{}; StringRef release_summary{}; StringRef release_url{}; - void encode(ProtoWriteBuffer &buffer) const; + uint8_t *encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const; uint32_t calculate_size() const; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; @@ -3007,7 +3007,7 @@ class ZWaveProxyFrame final : public ProtoDecodableMessage { #endif const uint8_t *data{nullptr}; uint16_t data_len{0}; - void encode(ProtoWriteBuffer &buffer) const; + uint8_t *encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const; uint32_t calculate_size() const; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; @@ -3026,7 +3026,7 @@ class ZWaveProxyRequest final : public ProtoDecodableMessage { enums::ZWaveProxyRequestType type{}; const uint8_t *data{nullptr}; uint16_t data_len{0}; - void encode(ProtoWriteBuffer &buffer) const; + uint8_t *encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const; uint32_t calculate_size() const; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; @@ -3047,7 +3047,7 @@ class ListEntitiesInfraredResponse final : public InfoResponseProtoMessage { #endif uint32_t capabilities{0}; uint32_t receiver_frequency{0}; - void encode(ProtoWriteBuffer &buffer) const; + uint8_t *encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const; uint32_t calculate_size() const; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; @@ -3094,7 +3094,7 @@ class InfraredRFReceiveEvent final : public ProtoMessage { #endif uint32_t key{0}; const std::vector *timings{}; - void encode(ProtoWriteBuffer &buffer) const; + uint8_t *encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const; uint32_t calculate_size() const; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; @@ -3138,7 +3138,7 @@ class SerialProxyDataReceived final : public ProtoMessage { this->data_ptr_ = data; this->data_len_ = len; } - void encode(ProtoWriteBuffer &buffer) const; + uint8_t *encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const; uint32_t calculate_size() const; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; @@ -3204,7 +3204,7 @@ class SerialProxyGetModemPinsResponse final : public ProtoMessage { #endif uint32_t instance{0}; uint32_t line_states{0}; - void encode(ProtoWriteBuffer &buffer) const; + uint8_t *encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const; uint32_t calculate_size() const; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; @@ -3239,7 +3239,7 @@ class SerialProxyRequestResponse final : public ProtoMessage { enums::SerialProxyRequestType type{}; enums::SerialProxyStatus status{}; StringRef error_message{}; - void encode(ProtoWriteBuffer &buffer) const; + uint8_t *encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const; uint32_t calculate_size() const; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; @@ -3277,7 +3277,7 @@ class BluetoothSetConnectionParamsResponse final : public ProtoMessage { #endif uint64_t address{0}; int32_t error{0}; - void encode(ProtoWriteBuffer &buffer) const; + uint8_t *encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const; uint32_t calculate_size() const; #ifdef HAS_PROTO_MESSAGE_DUMP const char *dump_to(DumpBuffer &out) const override; diff --git a/esphome/components/api/proto.cpp b/esphome/components/api/proto.cpp index 4f5b3f0918..236e4a474a 100644 --- a/esphome/components/api/proto.cpp +++ b/esphome/components/api/proto.cpp @@ -145,14 +145,15 @@ uint32_t ProtoDecodableMessage::count_repeated_field(const uint8_t *buffer, size // [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 &)) { + uint8_t *(*encode_fn)(const void *, + ProtoWriteBuffer &PROTO_ENCODE_DEBUG_PARAM)) { 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); + this->pos_ = encode_fn(value, *this PROTO_ENCODE_DEBUG_INIT(this->buffer_)); uint32_t body_size = static_cast(this->pos_ - body_start); if (body_size < 128) [[likely]] { // Common case: 1-byte varint, just backpatch @@ -173,22 +174,27 @@ void ProtoWriteBuffer::encode_sub_message(uint32_t field_id, const void *value, // 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 &)) { + uint8_t *(*encode_fn)(const void *, + ProtoWriteBuffer &PROTO_ENCODE_DEBUG_PARAM)) { 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); + this->pos_ = encode_fn(value, *this PROTO_ENCODE_DEBUG_INIT(this->buffer_)); 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); + this->pos_ = encode_fn(value, *this PROTO_ENCODE_DEBUG_INIT(this->buffer_)); #endif } #ifdef ESPHOME_DEBUG_API +void proto_check_bounds_failed(const uint8_t *pos, size_t bytes, const uint8_t *end, const char *caller) { + ESP_LOGE(TAG, "Proto encode bounds check failed in %s: need %zu bytes, %td available", caller, bytes, end - pos); + abort(); +} 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, @@ -201,6 +207,7 @@ void ProtoWriteBuffer::debug_check_encode_size_(uint32_t field_id, uint32_t expe expected, actual); abort(); } + #endif void ProtoDecodableMessage::decode(const uint8_t *buffer, size_t length) { @@ -257,7 +264,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); } diff --git a/esphome/components/api/proto.h b/esphome/components/api/proto.h index b629018a91..ff7c5232b6 100644 --- a/esphome/components/api/proto.h +++ b/esphome/components/api/proto.h @@ -25,6 +25,19 @@ constexpr uint8_t WIRE_TYPE_LENGTH_DELIMITED = 2; // string, bytes, embedded me constexpr uint8_t WIRE_TYPE_FIXED32 = 5; // fixed32, sfixed32, float constexpr uint8_t WIRE_TYPE_MASK = 0b111; // Mask to extract wire type from tag +// Reinterpret float bits as uint32_t without floating-point comparison. +// Used by both encode_float() and calc_float() to ensure identical zero checks. +// Uses union type-punning which is a GCC/Clang extension (not standard C++), +// but bit_cast/memcpy don't optimize to a no-op on xtensa-gcc (ESP8266). +inline uint32_t float_to_raw(float value) { + union { + float f; + uint32_t u; + } v; + v.f = value; + return v.u; +} + // Helper functions for ZigZag encoding/decoding inline constexpr uint32_t encode_zigzag32(int32_t value) { return (static_cast(value) << 1) ^ (static_cast(value >> 31)); @@ -195,6 +208,26 @@ class Proto32Bit { // NOTE: Proto64Bit class removed - wire type 1 (64-bit fixed) not supported +// Debug bounds checking for proto encode functions. +// In debug mode (ESPHOME_DEBUG_API), an extra end-of-buffer pointer is threaded +// through the entire encode chain. In production, these expand to nothing. +#ifdef ESPHOME_DEBUG_API +#define PROTO_ENCODE_DEBUG_PARAM , uint8_t *proto_debug_end_ +#define PROTO_ENCODE_DEBUG_ARG , proto_debug_end_ +#define PROTO_ENCODE_DEBUG_INIT(buf) , (buf)->data() + (buf)->size() +#define PROTO_ENCODE_CHECK_BOUNDS(pos, n) \ + do { \ + if ((pos) + (n) > proto_debug_end_) \ + proto_check_bounds_failed(pos, n, proto_debug_end_, __builtin_FUNCTION()); \ + } while (0) +void proto_check_bounds_failed(const uint8_t *pos, size_t bytes, const uint8_t *end, const char *caller); +#else +#define PROTO_ENCODE_DEBUG_PARAM +#define PROTO_ENCODE_DEBUG_ARG +#define PROTO_ENCODE_DEBUG_INIT(buf) +#define PROTO_ENCODE_CHECK_BOUNDS(pos, n) +#endif + class ProtoWriteBuffer { public: ProtoWriteBuffer(APIBuffer *buffer) : buffer_(buffer), pos_(buffer->data() + buffer->size()) {} @@ -207,15 +240,6 @@ class ProtoWriteBuffer { } 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). * @@ -229,123 +253,6 @@ class ProtoWriteBuffer { * Following https://protobuf.dev/programming-guides/encoding/#structure */ 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) - return; - - this->encode_field_raw(field_id, 2); // type 2: Length-delimited string - this->encode_varint_raw(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); - } - void encode_string(uint32_t field_id, const StringRef &ref, bool force = false) { - this->encode_string(field_id, ref.c_str(), ref.size(), force); - } - void encode_bytes(uint32_t field_id, const uint8_t *data, size_t len, bool force = false) { - this->encode_string(field_id, reinterpret_cast(data), len, force); - } - void encode_uint32(uint32_t field_id, uint32_t value, bool force = false) { - if (value == 0 && !force) - return; - this->encode_field_raw(field_id, 0); // type 0: Varint - uint32 - this->encode_varint_raw(value); - } - void encode_uint64(uint32_t field_id, uint64_t value, bool force = false) { - if (value == 0 && !force) - return; - this->encode_field_raw(field_id, 0); // type 0: Varint - uint64 - 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->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->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 - // 32-bit microcontrollers where 64-bit operations are expensive. If 64-bit support - // is needed in the future, the necessary encoding/decoding functions must be added. - void encode_float(uint32_t field_id, float value, bool force = false) { - if (value == 0.0f && !force) - return; - - union { - float value; - uint32_t raw; - } val{}; - val.value = value; - this->encode_fixed32(field_id, val.raw); - } - void encode_int32(uint32_t field_id, int32_t value, bool force = false) { - if (value < 0) { - // negative int32 is always 10 byte long - this->encode_int64(field_id, value, force); - return; - } - this->encode_uint32(field_id, static_cast(value), force); - } - void encode_int64(uint32_t field_id, int64_t value, bool force = false) { - this->encode_uint64(field_id, static_cast(value), force); - } - void encode_sint32(uint32_t field_id, int32_t value, bool force = false) { - this->encode_uint32(field_id, encode_zigzag32(value), force); - } - void encode_sint64(uint32_t field_id, int64_t value, bool force = false) { - this->encode_uint64(field_id, encode_zigzag64(value), force); - } - /// Encode a packed repeated sint32 field (zero-copy from vector) - void encode_packed_sint32(uint32_t field_id, const std::vector &values); /// 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); @@ -353,12 +260,17 @@ class ProtoWriteBuffer { /// 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); + // NOLINTBEGIN(readability-identifier-naming) // 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 &)); + void encode_sub_message(uint32_t field_id, const void *value, + uint8_t *(*encode_fn)(const void *, ProtoWriteBuffer &PROTO_ENCODE_DEBUG_PARAM)); // 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 &)); + uint8_t *(*encode_fn)(const void *, ProtoWriteBuffer &PROTO_ENCODE_DEBUG_PARAM)); + // NOLINTEND(readability-identifier-naming) APIBuffer *get_buffer() const { return buffer_; } + uint8_t *get_pos() const { return pos_; } + void set_pos(uint8_t *pos) { pos_ = pos; } protected: // Slow path for encode_varint_raw values >= 128, outlined to keep fast path small @@ -375,6 +287,220 @@ class ProtoWriteBuffer { uint8_t *pos_; }; +// Varint encoding thresholds — used by both proto_encode_* free functions and ProtoSize. +constexpr uint32_t VARINT_MAX_1_BYTE = 1 << 7; // 128 +constexpr uint32_t VARINT_MAX_2_BYTE = 1 << 14; // 16384 + +/// Static encode helpers for generated encode() functions. +/// Generated code hoists buffer.pos_ into a local uint8_t *__restrict__ pos, +/// then calls these methods which take pos by reference. No struct, no overhead. +/// For sub-messages, pos is synced back to buffer before the call and reloaded after. +class ProtoEncode { + public: + /// Write a multi-byte varint directly through a pos pointer. + template + static inline void encode_varint_raw_loop(uint8_t *__restrict__ &pos PROTO_ENCODE_DEBUG_PARAM, T value) { + do { + PROTO_ENCODE_CHECK_BOUNDS(pos, 1); + *pos++ = static_cast(value | 0x80); + value >>= 7; + } while (value > 0x7F); + PROTO_ENCODE_CHECK_BOUNDS(pos, 1); + *pos++ = static_cast(value); + } + static inline void ESPHOME_ALWAYS_INLINE encode_varint_raw(uint8_t *__restrict__ &pos PROTO_ENCODE_DEBUG_PARAM, + uint32_t value) { + if (value < VARINT_MAX_1_BYTE) [[likely]] { + PROTO_ENCODE_CHECK_BOUNDS(pos, 1); + *pos++ = static_cast(value); + return; + } + encode_varint_raw_loop(pos PROTO_ENCODE_DEBUG_ARG, value); + } + /// Encode a varint that is expected to be 1-2 bytes (e.g. zigzag RSSI, small lengths). + static inline void ESPHOME_ALWAYS_INLINE encode_varint_raw_short(uint8_t *__restrict__ &pos PROTO_ENCODE_DEBUG_PARAM, + uint32_t value) { + if (value < VARINT_MAX_1_BYTE) [[likely]] { + PROTO_ENCODE_CHECK_BOUNDS(pos, 1); + *pos++ = static_cast(value); + return; + } + if (value < VARINT_MAX_2_BYTE) [[likely]] { + PROTO_ENCODE_CHECK_BOUNDS(pos, 2); + *pos++ = static_cast(value | 0x80); + *pos++ = static_cast(value >> 7); + return; + } + encode_varint_raw_loop(pos PROTO_ENCODE_DEBUG_ARG, value); + } + static inline void ESPHOME_ALWAYS_INLINE encode_varint_raw_64(uint8_t *__restrict__ &pos PROTO_ENCODE_DEBUG_PARAM, + uint64_t value) { + if (value < VARINT_MAX_1_BYTE) [[likely]] { + PROTO_ENCODE_CHECK_BOUNDS(pos, 1); + *pos++ = static_cast(value); + return; + } + encode_varint_raw_loop(pos PROTO_ENCODE_DEBUG_ARG, value); + } + static inline void ESPHOME_ALWAYS_INLINE encode_field_raw(uint8_t *__restrict__ &pos PROTO_ENCODE_DEBUG_PARAM, + uint32_t field_id, uint32_t type) { + encode_varint_raw(pos PROTO_ENCODE_DEBUG_ARG, (field_id << 3) | type); + } + /// Write a single precomputed tag byte. Tag must be < 128. + static inline void ESPHOME_ALWAYS_INLINE write_raw_byte(uint8_t *__restrict__ &pos PROTO_ENCODE_DEBUG_PARAM, + uint8_t b) { + PROTO_ENCODE_CHECK_BOUNDS(pos, 1); + *pos++ = b; + } + /// Write raw bytes to the buffer (no tag, no length prefix). + static inline void ESPHOME_ALWAYS_INLINE encode_raw(uint8_t *__restrict__ &pos PROTO_ENCODE_DEBUG_PARAM, + const void *data, size_t len) { + PROTO_ENCODE_CHECK_BOUNDS(pos, len); + std::memcpy(pos, data, len); + pos += len; + } + /// Encode tag + 1-byte length + raw string data. For strings with max_data_length < 128. + /// Tag must be a single-byte varint (< 128). Always encodes (no zero check). + static inline void encode_short_string_force(uint8_t *__restrict__ &pos PROTO_ENCODE_DEBUG_PARAM, uint8_t tag, + const StringRef &ref) { +#ifdef ESPHOME_DEBUG_API + assert(ref.size() < 128 && "encode_short_string_force: string exceeds max_data_length < 128"); +#endif + PROTO_ENCODE_CHECK_BOUNDS(pos, 2 + ref.size()); + pos[0] = tag; + pos[1] = static_cast(ref.size()); + std::memcpy(pos + 2, ref.c_str(), ref.size()); + pos += 2 + ref.size(); + } + /// Write a precomputed tag byte + 32-bit value in one operation. + static inline void ESPHOME_ALWAYS_INLINE write_tag_and_fixed32(uint8_t *__restrict__ &pos PROTO_ENCODE_DEBUG_PARAM, + uint8_t tag, uint32_t value) { + PROTO_ENCODE_CHECK_BOUNDS(pos, 5); + pos[0] = tag; +#if __BYTE_ORDER__ == __ORDER_LITTLE_ENDIAN__ + std::memcpy(pos + 1, &value, 4); +#else + pos[1] = static_cast(value & 0xFF); + pos[2] = static_cast((value >> 8) & 0xFF); + pos[3] = static_cast((value >> 16) & 0xFF); + pos[4] = static_cast((value >> 24) & 0xFF); +#endif + pos += 5; + } + static inline void encode_string(uint8_t *__restrict__ &pos PROTO_ENCODE_DEBUG_PARAM, uint32_t field_id, + const char *string, size_t len, bool force = false) { + if (len == 0 && !force) + return; + encode_field_raw(pos PROTO_ENCODE_DEBUG_ARG, field_id, 2); // type 2: Length-delimited string + if (len < VARINT_MAX_1_BYTE) [[likely]] { + PROTO_ENCODE_CHECK_BOUNDS(pos, 1 + len); + *pos++ = static_cast(len); + } else { + encode_varint_raw_loop(pos PROTO_ENCODE_DEBUG_ARG, len); + PROTO_ENCODE_CHECK_BOUNDS(pos, len); + } + std::memcpy(pos, string, len); + pos += len; + } + static inline void encode_string(uint8_t *__restrict__ &pos PROTO_ENCODE_DEBUG_PARAM, uint32_t field_id, + const std::string &value, bool force = false) { + encode_string(pos PROTO_ENCODE_DEBUG_ARG, field_id, value.data(), value.size(), force); + } + static inline void encode_string(uint8_t *__restrict__ &pos PROTO_ENCODE_DEBUG_PARAM, uint32_t field_id, + const StringRef &ref, bool force = false) { + encode_string(pos PROTO_ENCODE_DEBUG_ARG, field_id, ref.c_str(), ref.size(), force); + } + static inline void encode_bytes(uint8_t *__restrict__ &pos PROTO_ENCODE_DEBUG_PARAM, uint32_t field_id, + const uint8_t *data, size_t len, bool force = false) { + encode_string(pos PROTO_ENCODE_DEBUG_ARG, field_id, reinterpret_cast(data), len, force); + } + static inline void encode_uint32(uint8_t *__restrict__ &pos PROTO_ENCODE_DEBUG_PARAM, uint32_t field_id, + uint32_t value, bool force = false) { + if (value == 0 && !force) + return; + encode_field_raw(pos PROTO_ENCODE_DEBUG_ARG, field_id, 0); + encode_varint_raw(pos PROTO_ENCODE_DEBUG_ARG, value); + } + static inline void encode_uint64(uint8_t *__restrict__ &pos PROTO_ENCODE_DEBUG_PARAM, uint32_t field_id, + uint64_t value, bool force = false) { + if (value == 0 && !force) + return; + encode_field_raw(pos PROTO_ENCODE_DEBUG_ARG, field_id, 0); + encode_varint_raw_64(pos PROTO_ENCODE_DEBUG_ARG, value); + } + static inline void encode_bool(uint8_t *__restrict__ &pos PROTO_ENCODE_DEBUG_PARAM, uint32_t field_id, bool value, + bool force = false) { + if (!value && !force) + return; + encode_field_raw(pos PROTO_ENCODE_DEBUG_ARG, field_id, 0); + PROTO_ENCODE_CHECK_BOUNDS(pos, 1); + *pos++ = value ? 0x01 : 0x00; + } + static inline void encode_fixed32(uint8_t *__restrict__ &pos PROTO_ENCODE_DEBUG_PARAM, uint32_t field_id, + uint32_t value, bool force = false) { + if (value == 0 && !force) + return; + encode_field_raw(pos PROTO_ENCODE_DEBUG_ARG, field_id, 5); + PROTO_ENCODE_CHECK_BOUNDS(pos, 4); +#if __BYTE_ORDER__ == __ORDER_LITTLE_ENDIAN__ + std::memcpy(pos, &value, 4); + pos += 4; +#else + *pos++ = (value >> 0) & 0xFF; + *pos++ = (value >> 8) & 0xFF; + *pos++ = (value >> 16) & 0xFF; + *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 + // 32-bit microcontrollers where 64-bit operations are expensive. If 64-bit support + // is needed in the future, the necessary encoding/decoding functions must be added. + static inline void encode_float(uint8_t *__restrict__ &pos PROTO_ENCODE_DEBUG_PARAM, uint32_t field_id, float value, + bool force = false) { + uint32_t raw = float_to_raw(value); + if (raw == 0 && !force) + return; + encode_fixed32(pos PROTO_ENCODE_DEBUG_ARG, field_id, raw); + } + static inline void encode_int32(uint8_t *__restrict__ &pos PROTO_ENCODE_DEBUG_PARAM, uint32_t field_id, int32_t value, + bool force = false) { + if (value < 0) { + // negative int32 is always 10 byte long + encode_uint64(pos PROTO_ENCODE_DEBUG_ARG, field_id, static_cast(value), force); + return; + } + encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, field_id, static_cast(value), force); + } + static inline void encode_int64(uint8_t *__restrict__ &pos PROTO_ENCODE_DEBUG_PARAM, uint32_t field_id, int64_t value, + bool force = false) { + encode_uint64(pos PROTO_ENCODE_DEBUG_ARG, field_id, static_cast(value), force); + } + static inline void encode_sint32(uint8_t *__restrict__ &pos PROTO_ENCODE_DEBUG_PARAM, uint32_t field_id, + int32_t value, bool force = false) { + encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, field_id, encode_zigzag32(value), force); + } + static inline void encode_sint64(uint8_t *__restrict__ &pos PROTO_ENCODE_DEBUG_PARAM, uint32_t field_id, + int64_t value, bool force = false) { + encode_uint64(pos PROTO_ENCODE_DEBUG_ARG, field_id, encode_zigzag64(value), force); + } + /// Sub-message encoding: sync pos to buffer, delegate, get pos from return value. + template + static inline void encode_sub_message(uint8_t *__restrict__ &pos PROTO_ENCODE_DEBUG_PARAM, ProtoWriteBuffer &buffer, + uint32_t field_id, const T &value) { + buffer.set_pos(pos); + buffer.encode_sub_message(field_id, value); + pos = buffer.get_pos(); + } + template + static inline void encode_optional_sub_message(uint8_t *__restrict__ &pos PROTO_ENCODE_DEBUG_PARAM, + ProtoWriteBuffer &buffer, uint32_t field_id, const T &value) { + buffer.set_pos(pos); + buffer.encode_optional_sub_message(field_id, value); + pos = buffer.get_pos(); + } +}; + #ifdef HAS_PROTO_MESSAGE_DUMP /** * Fixed-size buffer for message dumps - avoids heap allocation. @@ -470,7 +596,7 @@ class ProtoMessage { // 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 {} + uint8_t *encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const { return buffer.get_pos(); } uint32_t calculate_size() const { return 0; } #ifdef HAS_PROTO_MESSAGE_DUMP virtual const char *dump_to(DumpBuffer &out) const = 0; @@ -512,9 +638,10 @@ class ProtoDecodableMessage : public ProtoMessage { class ProtoSize { public: - // 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 + // Varint encoding thresholds — use namespace-level constants for 1/2 byte, + // class-level for 3/4 byte (only used within ProtoSize). + static constexpr uint32_t VARINT_THRESHOLD_1_BYTE = VARINT_MAX_1_BYTE; + static constexpr uint32_t VARINT_THRESHOLD_2_BYTE = VARINT_MAX_2_BYTE; static constexpr uint32_t VARINT_THRESHOLD_3_BYTE = 1 << 21; // 2097152 static constexpr uint32_t VARINT_THRESHOLD_4_BYTE = 1 << 28; // 268435456 @@ -531,6 +658,17 @@ class ProtoSize { return varint_wide(value); return varint_slow(value); } + /// Size of a varint expected to be 1-2 bytes (e.g. zigzag RSSI, small lengths). + /// Inlines both checks; falls back to slow path for 3+ bytes. + static constexpr inline uint32_t ESPHOME_ALWAYS_INLINE varint_short(uint32_t value) { + if (value < VARINT_THRESHOLD_1_BYTE) [[likely]] + return 1; + if (value < VARINT_THRESHOLD_2_BYTE) [[likely]] + return 2; + 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 @@ -635,8 +773,8 @@ class ProtoSize { } 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; + static uint32_t calc_float(uint32_t field_id_size, float value) { + return float_to_raw(value) != 0 ? field_id_size + 4 : 0; } static constexpr uint32_t calc_fixed32(uint32_t field_id_size, uint32_t value) { return value ? field_id_size + 4 : 0; @@ -645,10 +783,10 @@ class ProtoSize { return value ? field_id_size + 4 : 0; } static constexpr uint32_t calc_sint32(uint32_t field_id_size, int32_t value) { - return value ? field_id_size + varint(encode_zigzag32(value)) : 0; + return value ? field_id_size + varint_short(encode_zigzag32(value)) : 0; } 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)); + return field_id_size + varint_short(encode_zigzag32(value)); } static constexpr uint32_t calc_int64(uint32_t field_id_size, int64_t value) { return value ? field_id_size + varint(value) : 0; @@ -691,28 +829,9 @@ class ProtoSize { // 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()) - return; - - // Calculate packed size - size_t packed_size = 0; - for (int value : values) { - packed_size += ProtoSize::varint(encode_zigzag32(value)); - } - - // Write tag (LENGTH_DELIMITED) + length + all zigzag-encoded values - this->encode_field_raw(field_id, WIRE_TYPE_LENGTH_DELIMITED); - this->encode_varint_raw(packed_size); - for (int value : values) { - this->encode_varint_raw(encode_zigzag32(value)); - } -} - // 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); +template uint8_t *proto_encode_msg(const void *msg, ProtoWriteBuffer &buf PROTO_ENCODE_DEBUG_PARAM) { + return static_cast(msg)->encode(buf PROTO_ENCODE_DEBUG_ARG); } // Thin template wrapper; delegates to non-template core in proto.cpp. diff --git a/esphome/components/as3935/sensor.py b/esphome/components/as3935/sensor.py index 79bc7af4a9..9b43155563 100644 --- a/esphome/components/as3935/sensor.py +++ b/esphome/components/as3935/sensor.py @@ -6,6 +6,7 @@ from esphome.const import ( CONF_LIGHTNING_ENERGY, ICON_FLASH, ICON_SIGNAL_DISTANCE_VARIANT, + STATE_CLASS_MEASUREMENT, UNIT_KILOMETER, ) @@ -20,13 +21,14 @@ CONFIG_SCHEMA = cv.Schema( unit_of_measurement=UNIT_KILOMETER, icon=ICON_SIGNAL_DISTANCE_VARIANT, accuracy_decimals=1, + state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional(CONF_LIGHTNING_ENERGY): sensor.sensor_schema( icon=ICON_FLASH, accuracy_decimals=1, ), } -).extend(cv.COMPONENT_SCHEMA) +) async def to_code(config): diff --git a/esphome/components/as5600/sensor/__init__.py b/esphome/components/as5600/sensor/__init__.py index e84733a484..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,7 +19,6 @@ DEPENDENCIES = ["as5600"] AS5600Sensor = as5600_ns.class_("AS5600Sensor", sensor.Sensor, cg.PollingComponent) -CONF_RAW_ANGLE = "raw_angle" CONF_RAW_POSITION = "raw_position" CONF_SLOW_FILTER = "slow_filter" CONF_FAST_FILTER = "fast_filter" @@ -89,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 1c0f4bad2c..4e549d24d5 100644 --- a/esphome/components/as5600/sensor/as5600_sensor.cpp +++ b/esphome/components/as5600/sensor/as5600_sensor.cpp @@ -25,27 +25,10 @@ static const uint8_t REGISTER_MAGNITUDE = 0x1B; // 16 bytes / R 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 d471be49b5..77593f4b12 100644 --- a/esphome/components/as5600/sensor/as5600_sensor.h +++ b/esphome/components/as5600/sensor/as5600_sensor.h @@ -15,9 +15,6 @@ class AS5600Sensor : public PollingComponent, public Parented, void update() override; void dump_config() 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; } @@ -28,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/at581x/__init__.py b/esphome/components/at581x/__init__.py index 0780814ea6..4923491f0c 100644 --- a/esphome/components/at581x/__init__.py +++ b/esphome/components/at581x/__init__.py @@ -173,49 +173,41 @@ async def at581x_settings_to_code(config, action_id, template_arg, args): cg.add(var.set_hw_frontend_reset(template_)) if freq := config.get(CONF_FREQUENCY): - template_ = await cg.templatable(freq, args, float) - template_ = int(template_ / 1000000) + if cg.is_template(freq): + template_ = await cg.templatable(freq, args, cg.int32) + else: + template_ = int(freq / 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_)) if selfcheck := config.get(CONF_POWERON_SELFCHECK_TIME): - template_ = await cg.templatable(selfcheck, args, float) - if isinstance(template_, cv.TimePeriod): - template_ = template_.total_milliseconds - template_ = int(template_) + template_ = await cg.templatable(selfcheck, args, cg.int32) cg.add(var.set_poweron_selfcheck_time(template_)) if protect := config.get(CONF_PROTECT_TIME): - template_ = await cg.templatable(protect, args, float) - if isinstance(template_, cv.TimePeriod): - template_ = template_.total_milliseconds - template_ = int(template_) + template_ = await cg.templatable(protect, args, cg.int32) cg.add(var.set_protect_time(template_)) if trig_base := config.get(CONF_TRIGGER_BASE): - template_ = await cg.templatable(trig_base, args, float) - if isinstance(template_, cv.TimePeriod): - template_ = template_.total_milliseconds - template_ = int(template_) + template_ = await cg.templatable(trig_base, args, cg.int32) cg.add(var.set_trigger_base(template_)) if trig_keep := config.get(CONF_TRIGGER_KEEP): - template_ = await cg.templatable(trig_keep, args, float) - if isinstance(template_, cv.TimePeriod): - template_ = template_.total_milliseconds - template_ = int(template_) + template_ = await cg.templatable(trig_keep, args, cg.int32) 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_)) if power := config.get(CONF_POWER_CONSUMPTION): - template_ = await cg.templatable(power, args, float) - template_ = int(template_ * 1000000) + if cg.is_template(power): + template_ = await cg.templatable(power, args, cg.int32) + else: + template_ = int(power * 1000000) cg.add(var.set_power_consumption(template_)) return var diff --git a/esphome/components/atm90e26/sensor.py b/esphome/components/atm90e26/sensor.py index 4522e94846..5941cb35b4 100644 --- a/esphome/components/atm90e26/sensor.py +++ b/esphome/components/atm90e26/sensor.py @@ -14,6 +14,7 @@ from esphome.const import ( CONF_VOLTAGE, DEVICE_CLASS_CURRENT, DEVICE_CLASS_ENERGY, + DEVICE_CLASS_FREQUENCY, DEVICE_CLASS_POWER, DEVICE_CLASS_POWER_FACTOR, DEVICE_CLASS_REACTIVE_POWER, @@ -103,6 +104,7 @@ CONFIG_SCHEMA = ( unit_of_measurement=UNIT_HERTZ, icon=ICON_CURRENT_AC, accuracy_decimals=1, + device_class=DEVICE_CLASS_FREQUENCY, state_class=STATE_CLASS_MEASUREMENT, ), cv.Required(CONF_LINE_FREQUENCY): cv.enum(LINE_FREQS, upper=True), diff --git a/esphome/components/atm90e32/atm90e32.cpp b/esphome/components/atm90e32/atm90e32.cpp index ee7fe5ce75..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) { 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/atm90e32/sensor.py b/esphome/components/atm90e32/sensor.py index a510095217..7e5d85c57a 100644 --- a/esphome/components/atm90e32/sensor.py +++ b/esphome/components/atm90e32/sensor.py @@ -20,6 +20,7 @@ from esphome.const import ( DEVICE_CLASS_APPARENT_POWER, DEVICE_CLASS_CURRENT, DEVICE_CLASS_ENERGY, + DEVICE_CLASS_FREQUENCY, DEVICE_CLASS_POWER, DEVICE_CLASS_POWER_FACTOR, DEVICE_CLASS_REACTIVE_POWER, @@ -131,7 +132,6 @@ ATM90E32_PHASE_SCHEMA = cv.Schema( cv.Optional(CONF_PHASE_ANGLE): sensor.sensor_schema( unit_of_measurement=UNIT_DEGREES, accuracy_decimals=2, - device_class=DEVICE_CLASS_POWER, state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional(CONF_HARMONIC_POWER): sensor.sensor_schema( @@ -166,6 +166,7 @@ CONFIG_SCHEMA = ( unit_of_measurement=UNIT_HERTZ, icon=ICON_CURRENT_AC, accuracy_decimals=1, + device_class=DEVICE_CLASS_FREQUENCY, state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional(CONF_CHIP_TEMPERATURE): sensor.sensor_schema( diff --git a/esphome/components/audio/__init__.py b/esphome/components/audio/__init__.py index acc3b5d351..8f2102de6a 100644 --- a/esphome/components/audio/__init__.py +++ b/esphome/components/audio/__init__.py @@ -210,6 +210,7 @@ async def to_code(config): 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: diff --git a/esphome/components/audio/audio_decoder.cpp b/esphome/components/audio/audio_decoder.cpp index bc05bc0006..baa4c41c06 100644 --- a/esphome/components/audio/audio_decoder.cpp +++ b/esphome/components/audio/audio_decoder.cpp @@ -84,13 +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(); - // CRC check slows down decoding by 15-20% on an ESP32-S3. FLAC sources in ESPHome are either from an http source - // or built into the firmware, so the data integrity is already verified by the time it gets to the decoder, - // making the CRC check unnecessary. - this->flac_decoder_->set_crc_check_enabled(false); + 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 @@ -268,59 +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_buffer_->data(), this->input_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)); } - - size_t bytes_consumed = this->flac_decoder_->get_bytes_index(); 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()); - 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_buffer_->data(), this->input_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_buffer_->consume(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; diff --git a/esphome/components/audio/audio_decoder.h b/esphome/components/audio/audio_decoder.h index 726baa289e..6e3a228a68 100644 --- a/esphome/components/audio/audio_decoder.h +++ b/esphome/components/audio/audio_decoder.h @@ -16,14 +16,16 @@ #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 @@ -119,7 +121,7 @@ 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_(); 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 index 120f871d2f..fbb5ecd88d 100644 --- a/esphome/components/audio_file/media_source/audio_file_media_source.cpp +++ b/esphome/components/audio_file/media_source/audio_file_media_source.cpp @@ -4,6 +4,7 @@ #include "esphome/components/audio/audio_decoder.h" +#include #include namespace esphome::audio_file { @@ -249,7 +250,7 @@ void AudioFileMediaSource::decode_task(void *params) { audio::AudioStreamInfo stream_info = decoder->get_audio_stream_info().value(); - ESP_LOGD(TAG, "Bits per sample: %d, Channels: %d, Sample rate: %d", stream_info.get_bits_per_sample(), + 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) { diff --git a/esphome/components/binary_sensor/__init__.py b/esphome/components/binary_sensor/__init__.py index 4705f1675d..d8cdaa5d58 100644 --- a/esphome/components/binary_sensor/__init__.py +++ b/esphome/components/binary_sensor/__init__.py @@ -124,9 +124,10 @@ ClickTrigger = binary_sensor_ns.class_("ClickTrigger", automation.Trigger.templa 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") BinarySensorPublishAction = binary_sensor_ns.class_( @@ -255,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): @@ -283,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 @@ -388,7 +390,7 @@ def validate_multi_click_timing(value): new_state = v_.get(CONF_STATE, not state) if new_state == state: raise cv.Invalid( - f"Timings must have alternating state. Indices {i} and {i + 1} have the same state {state}" + f"Timings must have alternating state. Indices {i - 1} and {i} have the same state {state}" ) if max_length is not None and max_length < min_length: raise cv.Invalid( @@ -483,7 +485,9 @@ _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" @@ -560,7 +564,9 @@ async def _build_binary_sensor_automations(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) diff --git a/esphome/components/binary_sensor/automation.cpp b/esphome/components/binary_sensor/automation.cpp index 7e43d42357..eb68abce3b 100644 --- a/esphome/components/binary_sensor/automation.cpp +++ b/esphome/components/binary_sensor/automation.cpp @@ -13,7 +13,7 @@ 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 MultiClickTrigger::on_state_(bool state) { +void MultiClickTriggerBase::on_state_(bool state) { // Handle duplicate events if (state == this->last_state_) { return; @@ -32,7 +32,7 @@ void MultiClickTrigger::on_state_(bool state) { 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) { + 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); @@ -50,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; } @@ -61,7 +61,7 @@ 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(MULTICLICK_IS_NOT_VALID_ID); this->schedule_is_valid_(evt.min_length); @@ -74,7 +74,7 @@ void MultiClickTrigger::on_state_(bool state) { *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(MULTICLICK_COOLDOWN_ID, this->invalid_cooldown_, [this]() { @@ -86,7 +86,7 @@ void MultiClickTrigger::schedule_cooldown_() { 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; @@ -97,19 +97,19 @@ void MultiClickTrigger::schedule_is_valid_(uint32_t min_length) { this->is_valid_ = true; }); } -void MultiClickTrigger::schedule_is_not_valid_(uint32_t max_length) { +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(MULTICLICK_TRIGGER_ID); diff --git a/esphome/components/binary_sensor/automation.h b/esphome/components/binary_sensor/automation.h index f30f9d3279..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,10 +90,10 @@ 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); @@ -104,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); @@ -113,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 8ace7eafd1..7596975a68 100644 --- a/esphome/components/binary_sensor/binary_sensor.cpp +++ b/esphome/components/binary_sensor/binary_sensor.cpp @@ -32,13 +32,6 @@ 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 diff --git a/esphome/components/binary_sensor/binary_sensor.h b/esphome/components/binary_sensor/binary_sensor.h index 6ae5d04bcb..28c156763a 100644 --- a/esphome/components/binary_sensor/binary_sensor.h +++ b/esphome/components/binary_sensor/binary_sensor.h @@ -32,7 +32,10 @@ void log_binary_sensor(const char *tag, const char *prefix, const char *type, Bi */ 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. * @@ -54,16 +57,24 @@ class BinarySensor : public StatefulEntityBase { // ========== 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 @@ -73,7 +84,7 @@ class BinarySensor : public StatefulEntityBase { 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 5d525e967d..914060ce13 100644 --- a/esphome/components/binary_sensor/filter.cpp +++ b/esphome/components/binary_sensor/filter.cpp @@ -76,14 +76,11 @@ 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 { @@ -94,34 +91,26 @@ optional AutorepeatFilter::new_value(bool value) { } } -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()) { +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_.size()) { + 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->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)) {} diff --git a/esphome/components/binary_sensor/filter.h b/esphome/components/binary_sensor/filter.h index 0813847ca2..37c6bf0092 100644 --- a/esphome/components/binary_sensor/filter.h +++ b/esphome/components/binary_sensor/filter.h @@ -3,6 +3,8 @@ #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" @@ -86,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); diff --git a/esphome/components/bl0940/number/__init__.py b/esphome/components/bl0940/number/__init__.py index a640c2ae08..92ab2837b3 100644 --- a/esphome/components/bl0940/number/__init__.py +++ b/esphome/components/bl0940/number/__init__.py @@ -89,6 +89,6 @@ async def to_code(config): ) await cg.register_component(var, conf) - if restore_value := config.get(CONF_RESTORE_VALUE): + if restore_value := conf.get(CONF_RESTORE_VALUE): cg.add(var.set_restore_value(restore_value)) cg.add(getattr(bl0940, setter_method)(var)) 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/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/bluetooth_proxy/bluetooth_proxy.cpp b/esphome/components/bluetooth_proxy/bluetooth_proxy.cpp index 87206996b2..c69163b1f7 100644 --- a/esphome/components/bluetooth_proxy/bluetooth_proxy.cpp +++ b/esphome/components/bluetooth_proxy/bluetooth_proxy.cpp @@ -30,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) { @@ -101,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_); - +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() { @@ -130,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; } diff --git a/esphome/components/bluetooth_proxy/bluetooth_proxy.h b/esphome/components/bluetooth_proxy/bluetooth_proxy.h index f1b723e719..6680ab0e84 100644 --- a/esphome/components/bluetooth_proxy/bluetooth_proxy.h +++ b/esphome/components/bluetooth_proxy/bluetooth_proxy.h @@ -65,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) { @@ -150,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); @@ -166,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 269acfea44..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 { @@ -146,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/bme68x_bsec2/bme68x_bsec2.cpp b/esphome/components/bme68x_bsec2/bme68x_bsec2.cpp index cf516f6ca6..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_); @@ -114,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(); @@ -130,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; } @@ -212,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; } @@ -235,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"); @@ -259,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,28 +283,19 @@ void BME68xBSEC2Component::run_() { if (this->bsec_settings_.trigger_measurement && this->bsec_settings_.op_mode != BME68X_SLEEP_MODE) { 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 %uus", meas_dur); + 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; @@ -309,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; } @@ -373,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) { @@ -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 1ed72eee03..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, @@ -97,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_(); @@ -108,39 +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}; - 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 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}; @@ -155,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/bmp581_base/bmp581_base.cpp b/esphome/components/bmp581_base/bmp581_base.cpp index c9d250545b..7a627eee03 100644 --- a/esphome/components/bmp581_base/bmp581_base.cpp +++ b/esphome/components/bmp581_base/bmp581_base.cpp @@ -126,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; diff --git a/esphome/components/bmp581_base/bmp581_base.h b/esphome/components/bmp581_base/bmp581_base.h index c3920512e0..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 { diff --git a/esphome/components/bmp581_spi/sensor.py b/esphome/components/bmp581_spi/sensor.py index 75f60b2460..db0d0cd529 100644 --- a/esphome/components/bmp581_spi/sensor.py +++ b/esphome/components/bmp581_spi/sensor.py @@ -31,7 +31,7 @@ BMP581SPIComponent = bmp581_ns.class_( 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") + raise cv.Invalid("BMP581 only supports SPI mode 0 or mode 3") return config diff --git a/esphome/components/button/button.cpp b/esphome/components/button/button.cpp index b1c491805e..2a2a645132 100644 --- a/esphome/components/button/button.cpp +++ b/esphome/components/button/button.cpp @@ -16,7 +16,7 @@ void log_button(const char *tag, const char *prefix, const char *type, Button *o } void Button::press() { - ESP_LOGD(TAG, "'%s' Pressed.", this->get_name().c_str()); + ESP_LOGV(TAG, "'%s' Pressed.", this->get_name().c_str()); this->press_action(); this->press_callback_.call(); } diff --git a/esphome/components/camera_encoder/__init__.py b/esphome/components/camera_encoder/__init__.py index a0c59a517a..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.5") + 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/canbus/__init__.py b/esphome/components/canbus/__init__.py index c94c8647a9..7d3bf78f49 100644 --- a/esphome/components/canbus/__init__.py +++ b/esphome/components/canbus/__init__.py @@ -161,7 +161,7 @@ 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/cd74hc4067/__init__.py b/esphome/components/cd74hc4067/__init__.py index 9b69576b43..af6866df78 100644 --- a/esphome/components/cd74hc4067/__init__.py +++ b/esphome/components/cd74hc4067/__init__.py @@ -9,9 +9,7 @@ MULTI_CONF = True cd74hc4067_ns = cg.esphome_ns.namespace("cd74hc4067") -CD74HC4067Component = cd74hc4067_ns.class_( - "CD74HC4067Component", cg.Component, cg.PollingComponent -) +CD74HC4067Component = cd74hc4067_ns.class_("CD74HC4067Component", cg.Component) CONF_PIN_S0 = "pin_s0" CONF_PIN_S1 = "pin_s1" diff --git a/esphome/components/ch422g/ch422g.cpp b/esphome/components/ch422g/ch422g.cpp index 5f5e848c76..fc856cd563 100644 --- a/esphome/components/ch422g/ch422g.cpp +++ b/esphome/components/ch422g/ch422g.cpp @@ -124,12 +124,6 @@ bool CH422GComponent::write_outputs_() { float CH422GComponent::get_setup_priority() const { return setup_priority::IO; } -#ifdef USE_LOOP_PRIORITY -// 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 -#endif - 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 1b96568209..6e6bdad64a 100644 --- a/esphome/components/ch422g/ch422g.h +++ b/esphome/components/ch422g/ch422g.h @@ -23,9 +23,6 @@ class CH422GComponent : public Component, public i2c::I2CDevice { void pin_mode(uint8_t pin, gpio::Flags flags); float get_setup_priority() const override; -#ifdef USE_LOOP_PRIORITY - float get_loop_priority() const override; -#endif void dump_config() override; protected: diff --git a/esphome/components/ch423/ch423.cpp b/esphome/components/ch423/ch423.cpp index 805d8df877..8424d130b4 100644 --- a/esphome/components/ch423/ch423.cpp +++ b/esphome/components/ch423/ch423.cpp @@ -129,12 +129,6 @@ bool CH423Component::write_outputs_() { float CH423Component::get_setup_priority() const { return setup_priority::IO; } -#ifdef USE_LOOP_PRIORITY -// Run our loop() method very early in the loop, so that we cache read values -// before other components call our digital_read() method. -float CH423Component::get_loop_priority() const { return 9.0f; } // Just after WIFI -#endif - 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_; } diff --git a/esphome/components/ch423/ch423.h b/esphome/components/ch423/ch423.h index d85648a8f9..d384971a72 100644 --- a/esphome/components/ch423/ch423.h +++ b/esphome/components/ch423/ch423.h @@ -22,9 +22,6 @@ class CH423Component : public Component, public i2c::I2CDevice { void pin_mode(uint8_t pin, gpio::Flags flags); float get_setup_priority() const override; -#ifdef USE_LOOP_PRIORITY - float get_loop_priority() const override; -#endif void dump_config() override; protected: diff --git a/esphome/components/climate/__init__.py b/esphome/components/climate/__init__.py index 8cf5fa9b0c..13dd7aa007 100644 --- a/esphome/components/climate/__init__.py +++ b/esphome/components/climate/__init__.py @@ -400,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 ) ) diff --git a/esphome/components/const/__init__.py b/esphome/components/const/__init__.py index 0eb37e3029..846d3fd883 100644 --- a/esphome/components/const/__init__.py +++ b/esphome/components/const/__init__.py @@ -32,4 +32,6 @@ 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/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/cs5460a/sensor.py b/esphome/components/cs5460a/sensor.py index d2383bd01b..0c6ae0d821 100644 --- a/esphome/components/cs5460a/sensor.py +++ b/esphome/components/cs5460a/sensor.py @@ -12,6 +12,7 @@ from esphome.const import ( DEVICE_CLASS_CURRENT, DEVICE_CLASS_POWER, DEVICE_CLASS_VOLTAGE, + STATE_CLASS_MEASUREMENT, UNIT_AMPERE, UNIT_VOLT, UNIT_WATT, @@ -82,16 +83,19 @@ CONFIG_SCHEMA = cv.All( unit_of_measurement=UNIT_VOLT, accuracy_decimals=0, device_class=DEVICE_CLASS_VOLTAGE, + state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional(CONF_CURRENT): sensor.sensor_schema( unit_of_measurement=UNIT_AMPERE, accuracy_decimals=1, device_class=DEVICE_CLASS_CURRENT, + state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional(CONF_POWER): sensor.sensor_schema( unit_of_measurement=UNIT_WATT, accuracy_decimals=0, device_class=DEVICE_CLASS_POWER, + state_class=STATE_CLASS_MEASUREMENT, ), } ) diff --git a/esphome/components/cse7766/sensor.py b/esphome/components/cse7766/sensor.py index 6572a914aa..94ed66d7cc 100644 --- a/esphome/components/cse7766/sensor.py +++ b/esphome/components/cse7766/sensor.py @@ -32,52 +32,56 @@ DEPENDENCIES = ["uart"] cse7766_ns = cg.esphome_ns.namespace("cse7766") CSE7766Component = cse7766_ns.class_("CSE7766Component", cg.Component, uart.UARTDevice) -CONFIG_SCHEMA = cv.Schema( - { - cv.GenerateID(): cv.declare_id(CSE7766Component), - cv.Optional(CONF_VOLTAGE): sensor.sensor_schema( - unit_of_measurement=UNIT_VOLT, - accuracy_decimals=1, - device_class=DEVICE_CLASS_VOLTAGE, - state_class=STATE_CLASS_MEASUREMENT, - ), - cv.Optional(CONF_CURRENT): sensor.sensor_schema( - unit_of_measurement=UNIT_AMPERE, - accuracy_decimals=2, - device_class=DEVICE_CLASS_CURRENT, - state_class=STATE_CLASS_MEASUREMENT, - ), - cv.Optional(CONF_POWER): sensor.sensor_schema( - unit_of_measurement=UNIT_WATT, - accuracy_decimals=1, - device_class=DEVICE_CLASS_POWER, - state_class=STATE_CLASS_MEASUREMENT, - ), - cv.Optional(CONF_ENERGY): sensor.sensor_schema( - unit_of_measurement=UNIT_WATT_HOURS, - accuracy_decimals=3, - device_class=DEVICE_CLASS_ENERGY, - state_class=STATE_CLASS_TOTAL_INCREASING, - ), - cv.Optional(CONF_APPARENT_POWER): sensor.sensor_schema( - unit_of_measurement=UNIT_VOLT_AMPS, - accuracy_decimals=1, - device_class=DEVICE_CLASS_APPARENT_POWER, - state_class=STATE_CLASS_MEASUREMENT, - ), - cv.Optional(CONF_REACTIVE_POWER): sensor.sensor_schema( - unit_of_measurement=UNIT_VOLT_AMPS_REACTIVE, - accuracy_decimals=1, - device_class=DEVICE_CLASS_REACTIVE_POWER, - state_class=STATE_CLASS_MEASUREMENT, - ), - cv.Optional(CONF_POWER_FACTOR): sensor.sensor_schema( - accuracy_decimals=2, - device_class=DEVICE_CLASS_POWER_FACTOR, - state_class=STATE_CLASS_MEASUREMENT, - ), - } -).extend(uart.UART_DEVICE_SCHEMA) +CONFIG_SCHEMA = ( + cv.Schema( + { + cv.GenerateID(): cv.declare_id(CSE7766Component), + cv.Optional(CONF_VOLTAGE): sensor.sensor_schema( + unit_of_measurement=UNIT_VOLT, + accuracy_decimals=1, + device_class=DEVICE_CLASS_VOLTAGE, + state_class=STATE_CLASS_MEASUREMENT, + ), + cv.Optional(CONF_CURRENT): sensor.sensor_schema( + unit_of_measurement=UNIT_AMPERE, + accuracy_decimals=2, + device_class=DEVICE_CLASS_CURRENT, + state_class=STATE_CLASS_MEASUREMENT, + ), + cv.Optional(CONF_POWER): sensor.sensor_schema( + unit_of_measurement=UNIT_WATT, + accuracy_decimals=1, + device_class=DEVICE_CLASS_POWER, + state_class=STATE_CLASS_MEASUREMENT, + ), + cv.Optional(CONF_ENERGY): sensor.sensor_schema( + unit_of_measurement=UNIT_WATT_HOURS, + accuracy_decimals=3, + device_class=DEVICE_CLASS_ENERGY, + state_class=STATE_CLASS_TOTAL_INCREASING, + ), + cv.Optional(CONF_APPARENT_POWER): sensor.sensor_schema( + unit_of_measurement=UNIT_VOLT_AMPS, + accuracy_decimals=1, + device_class=DEVICE_CLASS_APPARENT_POWER, + state_class=STATE_CLASS_MEASUREMENT, + ), + cv.Optional(CONF_REACTIVE_POWER): sensor.sensor_schema( + unit_of_measurement=UNIT_VOLT_AMPS_REACTIVE, + accuracy_decimals=1, + device_class=DEVICE_CLASS_REACTIVE_POWER, + state_class=STATE_CLASS_MEASUREMENT, + ), + cv.Optional(CONF_POWER_FACTOR): sensor.sensor_schema( + accuracy_decimals=2, + device_class=DEVICE_CLASS_POWER_FACTOR, + state_class=STATE_CLASS_MEASUREMENT, + ), + } + ) + .extend(uart.UART_DEVICE_SCHEMA) + .extend(cv.COMPONENT_SCHEMA) +) FINAL_VALIDATE_SCHEMA = uart.final_validate_device_schema( "cse7766", baud_rate=4800, parity="EVEN", require_rx=True ) diff --git a/esphome/components/cst226/binary_sensor/__init__.py b/esphome/components/cst226/binary_sensor/__init__.py index d95f0d2b4d..324d794772 100644 --- a/esphome/components/cst226/binary_sensor/__init__.py +++ b/esphome/components/cst226/binary_sensor/__init__.py @@ -15,10 +15,14 @@ CST226Button = cst226_ns.class_( cg.Parented.template(CST226Touchscreen), ) -CONFIG_SCHEMA = binary_sensor.binary_sensor_schema(CST226Button).extend( - { - cv.GenerateID(CONF_CST226_ID): cv.use_id(CST226Touchscreen), - } +CONFIG_SCHEMA = ( + binary_sensor.binary_sensor_schema(CST226Button) + .extend( + { + cv.GenerateID(CONF_CST226_ID): cv.use_id(CST226Touchscreen), + } + ) + .extend(cv.COMPONENT_SCHEMA) ) diff --git a/esphome/components/debug/debug_zephyr.cpp b/esphome/components/debug/debug_zephyr.cpp index bf87b7ae3d..d1580dae80 100644 --- a/esphome/components/debug/debug_zephyr.cpp +++ b/esphome/components/debug/debug_zephyr.cpp @@ -91,6 +91,49 @@ void DebugComponent::log_partition_info_() { 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): @@ -354,7 +397,9 @@ size_t DebugComponent::get_device_info_(std::span }; 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/debug/sensor.py b/esphome/components/debug/sensor.py index 0a716d666e..a018ce5c3b 100644 --- a/esphome/components/debug/sensor.py +++ b/esphome/components/debug/sensor.py @@ -8,12 +8,14 @@ from esphome.const import ( CONF_FRAGMENTATION, CONF_FREE, CONF_LOOP_TIME, + DEVICE_CLASS_FREQUENCY, ENTITY_CATEGORY_DIAGNOSTIC, ICON_COUNTER, ICON_TIMER, PLATFORM_BK72XX, PLATFORM_LN882X, PLATFORM_RTL87XX, + STATE_CLASS_MEASUREMENT, UNIT_BYTES, UNIT_HERTZ, UNIT_MILLISECOND, @@ -38,12 +40,14 @@ CONFIG_SCHEMA = { icon=ICON_COUNTER, accuracy_decimals=0, entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional(CONF_BLOCK): sensor.sensor_schema( unit_of_measurement=UNIT_BYTES, icon=ICON_COUNTER, accuracy_decimals=0, entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional(CONF_FRAGMENTATION): cv.All( cv.Any( @@ -59,6 +63,7 @@ CONFIG_SCHEMA = { icon=ICON_COUNTER, accuracy_decimals=1, entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + state_class=STATE_CLASS_MEASUREMENT, ), ), cv.Optional(CONF_MIN_FREE): cv.All( @@ -72,6 +77,7 @@ CONFIG_SCHEMA = { icon=ICON_COUNTER, accuracy_decimals=0, entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + state_class=STATE_CLASS_MEASUREMENT, ), ), cv.Optional(CONF_LOOP_TIME): sensor.sensor_schema( @@ -79,6 +85,7 @@ CONFIG_SCHEMA = { icon=ICON_TIMER, accuracy_decimals=0, entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional(CONF_PSRAM): cv.All( cv.only_on_esp32, @@ -88,6 +95,7 @@ CONFIG_SCHEMA = { icon=ICON_COUNTER, accuracy_decimals=0, entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + state_class=STATE_CLASS_MEASUREMENT, ), ), cv.Optional(CONF_CPU_FREQUENCY): cv.All( @@ -95,7 +103,9 @@ CONFIG_SCHEMA = { unit_of_measurement=UNIT_HERTZ, icon="mdi:speedometer", accuracy_decimals=0, + device_class=DEVICE_CLASS_FREQUENCY, entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + state_class=STATE_CLASS_MEASUREMENT, ), ), } diff --git a/esphome/components/deep_sleep/__init__.py b/esphome/components/deep_sleep/__init__.py index 4098fd3fb8..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, diff --git a/esphome/components/deep_sleep/deep_sleep_component.cpp b/esphome/components/deep_sleep/deep_sleep_component.cpp index 0511518419..3dd1b70930 100644 --- a/esphome/components/deep_sleep/deep_sleep_component.cpp +++ b/esphome/components/deep_sleep/deep_sleep_component.cpp @@ -40,12 +40,6 @@ void DeepSleepComponent::loop() { this->begin_sleep(); } -#ifdef USE_LOOP_PRIORITY -float DeepSleepComponent::get_loop_priority() const { - return -100.0f; // run after everything else is ready -} -#endif - 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 14713d51a1..9090f91876 100644 --- a/esphome/components/deep_sleep/deep_sleep_component.h +++ b/esphome/components/deep_sleep/deep_sleep_component.h @@ -113,9 +113,6 @@ class DeepSleepComponent : public Component { void setup() override; void dump_config() override; void loop() override; -#ifdef USE_LOOP_PRIORITY - float get_loop_priority() const override; -#endif float get_setup_priority() const override; /// Helper to enter deep sleep mode diff --git a/esphome/components/dfplayer/__init__.py b/esphome/components/dfplayer/__init__.py index 9df108c9c0..adc1913791 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( @@ -130,7 +122,7 @@ async def dfplayer_previous_to_code(config, action_id, template_arg, args): async def dfplayer_play_mp3_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_FILE], args, float) + template_ = await cg.templatable(config[CONF_FILE], args, cg.uint16) cg.add(var.set_file(template_)) return var @@ -151,10 +143,10 @@ async def dfplayer_play_mp3_to_code(config, action_id, template_arg, args): async def dfplayer_play_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_FILE], args, float) + template_ = await cg.templatable(config[CONF_FILE], args, cg.uint16) cg.add(var.set_file(template_)) if CONF_LOOP in config: - template_ = await cg.templatable(config[CONF_LOOP], args, float) + template_ = await cg.templatable(config[CONF_LOOP], args, cg.bool_) cg.add(var.set_loop(template_)) return var @@ -175,13 +167,13 @@ async def dfplayer_play_to_code(config, action_id, template_arg, args): async def dfplayer_play_folder_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_FOLDER], args, float) + template_ = await cg.templatable(config[CONF_FOLDER], args, cg.uint16) cg.add(var.set_folder(template_)) if CONF_FILE in config: - template_ = await cg.templatable(config[CONF_FILE], args, float) + template_ = await cg.templatable(config[CONF_FILE], args, cg.uint16) cg.add(var.set_file(template_)) if CONF_LOOP in config: - template_ = await cg.templatable(config[CONF_LOOP], args, float) + template_ = await cg.templatable(config[CONF_LOOP], args, cg.bool_) cg.add(var.set_loop(template_)) return var @@ -221,7 +213,7 @@ async def dfplayer_set_device_to_code(config, action_id, template_arg, args): async def dfplayer_set_volume_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_VOLUME], args, float) + template_ = await cg.templatable(config[CONF_VOLUME], args, cg.uint8) cg.add(var.set_volume(template_)) return var diff --git a/esphome/components/dfplayer/dfplayer.h b/esphome/components/dfplayer/dfplayer.h index 2c4ee03470..0d240566c3 100644 --- a/esphome/components/dfplayer/dfplayer.h +++ b/esphome/components/dfplayer/dfplayer.h @@ -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 ba77e56abb..0becaf3543 100644 --- a/esphome/components/dfrobot_sen0395/__init__.py +++ b/esphome/components/dfrobot_sen0395/__init__.py @@ -97,7 +97,7 @@ def range_segment_list(input): ) largest_distance = -1 - for distance in input: + for i, distance in enumerate(input): if isinstance(distance, cv.Lambda): continue m = cv.distance(distance) @@ -112,7 +112,7 @@ def range_segment_list(input): ) largest_distance = m # Replace distance object with meters float - input[input.index(distance)] = m + input[i] = m return input diff --git a/esphome/components/dht/dht.cpp b/esphome/components/dht/dht.cpp index fef247f168..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) @@ -73,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); @@ -107,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 @@ -156,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; } @@ -177,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; } @@ -234,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 4671ee6f27..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; } @@ -55,13 +53,12 @@ class DHT : public PollingComponent { 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/display/__init__.py b/esphome/components/display/__init__.py index 6367f88acc..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, @@ -16,7 +19,9 @@ from esphome.const import ( SCHEDULER_DONT_RUN, ) 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) diff --git a/esphome/components/display/display.h b/esphome/components/display/display.h index e40f6ec963..6e38300d0e 100644 --- a/esphome/components/display/display.h +++ b/esphome/components/display/display.h @@ -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 c9a0c7ee93..9125c43f0c 100644 --- a/esphome/components/display_menu_base/__init__.py +++ b/esphome/components/display_menu_base/__init__.py @@ -119,7 +119,7 @@ DisplayMenuOnPrevTrigger = display_menu_base_ns.class_( def validate_format(format): - if re.search(r"^%([+-])*(\d+)*(\.\d+)*[fg]$", format) is None: + if re.search(r"^%[+-]*(\d+)?(\.\d+)?[fg]$", format) is None: raise cv.Invalid( f"{CONF_FORMAT}: has to specify a printf-like format string specifying exactly one f or g type conversion, '{format}' provided" ) diff --git a/esphome/components/dlms_meter/dlms_meter.cpp b/esphome/components/dlms_meter/dlms_meter.cpp index 052a0f4d01..b732e71d24 100644 --- a/esphome/components/dlms_meter/dlms_meter.cpp +++ b/esphome/components/dlms_meter/dlms_meter.cpp @@ -1,5 +1,7 @@ #include "dlms_meter.h" +#include + #if defined(USE_ESP8266_FRAMEWORK_ARDUINO) #include #elif defined(USE_ESP32) @@ -21,7 +23,7 @@ void DlmsMeterComponent::dump_config() { ESP_LOGCONFIG(TAG, "DLMS Meter:\n" " Provider: %s\n" - " Read Timeout: %u ms", + " 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, ) diff --git a/esphome/components/dsmr/__init__.py b/esphome/components/dsmr/__init__.py index 7d76856f28..dd7f2b9f56 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( @@ -44,7 +46,9 @@ CONFIG_SCHEMA = cv.All( CONF_RECEIVE_TIMEOUT, default="200ms" ): cv.positive_time_period_milliseconds, } - ).extend(uart.UART_DEVICE_SCHEMA), + ) + .extend(uart.UART_DEVICE_SCHEMA) + .extend(cv.COMPONENT_SCHEMA), ) @@ -64,6 +68,7 @@ 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.1.0") diff --git a/esphome/components/dsmr/sensor.py b/esphome/components/dsmr/sensor.py index 863af42d1b..c49614eaa9 100644 --- a/esphome/components/dsmr/sensor.py +++ b/esphome/components/dsmr/sensor.py @@ -122,42 +122,52 @@ CONFIG_SCHEMA = cv.Schema( cv.Optional("total_imported_energy"): sensor.sensor_schema( unit_of_measurement=UNIT_KILOVOLT_AMPS_REACTIVE_HOURS, accuracy_decimals=3, + state_class=STATE_CLASS_TOTAL_INCREASING, ), cv.Optional("reactive_energy_delivered_tariff1"): sensor.sensor_schema( unit_of_measurement=UNIT_KILOVOLT_AMPS_REACTIVE_HOURS, accuracy_decimals=3, + state_class=STATE_CLASS_TOTAL_INCREASING, ), cv.Optional("reactive_energy_delivered_tariff2"): sensor.sensor_schema( unit_of_measurement=UNIT_KILOVOLT_AMPS_REACTIVE_HOURS, accuracy_decimals=3, + state_class=STATE_CLASS_TOTAL_INCREASING, ), cv.Optional("reactive_energy_delivered_tariff3"): sensor.sensor_schema( unit_of_measurement=UNIT_KILOVOLT_AMPS_REACTIVE_HOURS, accuracy_decimals=3, + state_class=STATE_CLASS_TOTAL_INCREASING, ), cv.Optional("reactive_energy_delivered_tariff4"): sensor.sensor_schema( unit_of_measurement=UNIT_KILOVOLT_AMPS_REACTIVE_HOURS, accuracy_decimals=3, + state_class=STATE_CLASS_TOTAL_INCREASING, ), cv.Optional("total_exported_energy"): sensor.sensor_schema( unit_of_measurement=UNIT_KILOVOLT_AMPS_REACTIVE_HOURS, accuracy_decimals=3, + state_class=STATE_CLASS_TOTAL_INCREASING, ), cv.Optional("reactive_energy_returned_tariff1"): sensor.sensor_schema( unit_of_measurement=UNIT_KILOVOLT_AMPS_REACTIVE_HOURS, accuracy_decimals=3, + state_class=STATE_CLASS_TOTAL_INCREASING, ), cv.Optional("reactive_energy_returned_tariff2"): sensor.sensor_schema( unit_of_measurement=UNIT_KILOVOLT_AMPS_REACTIVE_HOURS, accuracy_decimals=3, + state_class=STATE_CLASS_TOTAL_INCREASING, ), cv.Optional("reactive_energy_returned_tariff3"): sensor.sensor_schema( unit_of_measurement=UNIT_KILOVOLT_AMPS_REACTIVE_HOURS, accuracy_decimals=3, + state_class=STATE_CLASS_TOTAL_INCREASING, ), cv.Optional("reactive_energy_returned_tariff4"): sensor.sensor_schema( unit_of_measurement=UNIT_KILOVOLT_AMPS_REACTIVE_HOURS, accuracy_decimals=3, + state_class=STATE_CLASS_TOTAL_INCREASING, ), cv.Optional("power_delivered"): sensor.sensor_schema( unit_of_measurement=UNIT_KILOWATT, diff --git a/esphome/components/e131/__init__.py b/esphome/components/e131/__init__.py index 301812e314..a1a8e0aec5 100644 --- a/esphome/components/e131/__init__.py +++ b/esphome/components/e131/__init__.py @@ -29,7 +29,7 @@ CONFIG_SCHEMA = cv.Schema( cv.GenerateID(): cv.declare_id(E131Component), cv.Optional(CONF_METHOD, default="MULTICAST"): cv.one_of(*METHODS, upper=True), } -) +).extend(cv.COMPONENT_SCHEMA) async def to_code(config): diff --git a/esphome/components/ens160_base/__init__.py b/esphome/components/ens160_base/__init__.py index 3b6ad8a4ee..46c53c3b10 100644 --- a/esphome/components/ens160_base/__init__.py +++ b/esphome/components/ens160_base/__init__.py @@ -24,7 +24,6 @@ CODEOWNERS = ["@vincentscode", "@latonita"] ens160_ns = cg.esphome_ns.namespace("ens160_base") CONF_AQI = "aqi" -UNIT_INDEX = "index" CONFIG_SCHEMA_BASE = cv.Schema( { diff --git a/esphome/components/epaper_spi/display.py b/esphome/components/epaper_spi/display.py index 2657071f45..658f9e2c4a 100644 --- a/esphome/components/epaper_spi/display.py +++ b/esphome/components/epaper_spi/display.py @@ -175,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]) @@ -201,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 ae1923a916..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_()); @@ -280,11 +297,11 @@ bool 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 a743985518..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; @@ -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_(); @@ -119,6 +126,7 @@ class EPaperBase : public Display, 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); /** @@ -171,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/esp32/__init__.py b/esphome/components/esp32/__init__.py index 91eb913e3d..0d8a221524 100644 --- a/esphome/components/esp32/__init__.py +++ b/esphome/components/esp32/__init__.py @@ -48,7 +48,7 @@ from esphome.coroutine import CoroPriority, coroutine_with_priority import esphome.final_validate as fv 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 esphome.writer import clean_build, clean_cmake_cache from .boards import BOARDS, STANDARD_BOARDS from .const import ( # noqa @@ -97,8 +97,12 @@ CONF_ENABLE_LWIP_ASSERT = "enable_lwip_assert" CONF_EXECUTE_FROM_PSRAM = "execute_from_psram" CONF_MINIMUM_CHIP_REVISION = "minimum_chip_revision" CONF_RELEASE = "release" +CONF_SIGNED_OTA_VERIFICATION = "signed_ota_verification" +CONF_SIGNING_KEY = "signing_key" +CONF_SIGNING_SCHEME = "signing_scheme" CONF_SRAM1_AS_IRAM = "sram1_as_iram" CONF_SUBTYPE = "subtype" +CONF_VERIFICATION_KEY = "verification_key" ARDUINO_FRAMEWORK_NAME = "framework-arduinoespressif32" ARDUINO_FRAMEWORK_PKG = f"pioarduino/{ARDUINO_FRAMEWORK_NAME}" @@ -120,6 +124,27 @@ ASSERTION_LEVELS = { "SILENT": "CONFIG_COMPILER_OPTIMIZATION_ASSERTIONS_SILENT", } +SIGNING_SCHEMES = { + "rsa3072": "CONFIG_SECURE_SIGNED_APPS_RSA_SCHEME", + "ecdsa256": "CONFIG_SECURE_SIGNED_APPS_ECDSA_V2_SCHEME", +} + +# Chip variants that only support one signing scheme for Secure Boot V2. +# Based on SOC_SECURE_BOOT_V2_RSA / SOC_SECURE_BOOT_V2_ECC in soc_caps.h. +# Variants not listed in either set support both RSA and ECDSA +# (e.g. C5, C6, H2, P4). New variants should be added to the +# appropriate set if they only support one scheme. +SIGNED_OTA_RSA_ONLY_VARIANTS = { + VARIANT_ESP32, + VARIANT_ESP32S2, + VARIANT_ESP32S3, + VARIANT_ESP32C3, +} +SIGNED_OTA_ECC_ONLY_VARIANTS = { + VARIANT_ESP32C2, + VARIANT_ESP32C61, +} + COMPILER_OPTIMIZATIONS = { "DEBUG": "CONFIG_COMPILER_OPTIMIZATION_DEBUG", "NONE": "CONFIG_COMPILER_OPTIMIZATION_NONE", @@ -690,9 +715,15 @@ ARDUINO_IDF_VERSION_LOOKUP = { ESP_IDF_FRAMEWORK_VERSION_LOOKUP = { "recommended": cv.Version(5, 5, 3, "1"), "latest": cv.Version(5, 5, 3, "1"), - "dev": cv.Version(5, 5, 3, "1"), + "dev": cv.Version(5, 5, 4), } ESP_IDF_PLATFORM_VERSION_LOOKUP = { + 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), @@ -956,6 +987,47 @@ def final_validate(config): ) # disable the rollback feature anyway since it can't be used. advanced[CONF_ENABLE_OTA_ROLLBACK] = False + if signed_ota := advanced.get(CONF_SIGNED_OTA_VERIFICATION): + scheme = signed_ota[CONF_SIGNING_SCHEME] + variant = config[CONF_VARIANT] + scheme_variant_conflicts = { + "ecdsa256": (SIGNED_OTA_RSA_ONLY_VARIANTS, "rsa3072"), + "rsa3072": (SIGNED_OTA_ECC_ONLY_VARIANTS, "ecdsa256"), + } + if (conflict := scheme_variant_conflicts.get(scheme)) and variant in conflict[ + 0 + ]: + errs.append( + cv.Invalid( + f"Signing scheme '{scheme}' is not supported on " + f"{VARIANT_FRIENDLY[variant]}. Use '{conflict[1]}' instead.", + path=[ + CONF_FRAMEWORK, + CONF_ADVANCED, + CONF_SIGNED_OTA_VERIFICATION, + CONF_SIGNING_SCHEME, + ], + ) + ) + if CONF_OTA not in full_config: + _LOGGER.warning( + "Signed OTA verification is enabled but no OTA component is configured. " + "The initial firmware will be signed but OTA updates won't be possible " + "until an OTA component is added." + ) + if CONF_SIGNING_KEY in signed_ota: + _LOGGER.info( + "Signed OTA verification is enabled. Keep your signing key safe! " + "If you lose the signing key, you will NOT be able to OTA update " + "devices running firmware signed with this key. " + "Without the key, you'll need to reflash via serial." + ) + else: + _LOGGER.info( + "Signed OTA verification is configured with a public verification key. " + "Binaries will NOT be signed automatically during build. " + "You must sign them externally before flashing." + ) if errs: raise cv.MultipleInvalid(errs) @@ -1167,6 +1239,18 @@ FRAMEWORK_SCHEMA = cv.Schema( min=8192, max=32768 ), cv.Optional(CONF_ENABLE_OTA_ROLLBACK, default=True): cv.boolean, + cv.Optional(CONF_SIGNED_OTA_VERIFICATION): cv.All( + cv.Schema( + { + cv.Optional(CONF_SIGNING_KEY): cv.file_, + cv.Optional(CONF_VERIFICATION_KEY): cv.file_, + cv.Optional( + CONF_SIGNING_SCHEME, default="rsa3072" + ): cv.one_of(*SIGNING_SCHEMES, lower=True), + } + ), + cv.has_exactly_one_key(CONF_SIGNING_KEY, CONF_VERIFICATION_KEY), + ), cv.Optional( CONF_USE_FULL_CERTIFICATE_BUNDLE, default=False ): cv.boolean, @@ -1872,6 +1956,32 @@ async def to_code(config): add_idf_sdkconfig_option("CONFIG_BOOTLOADER_APP_ROLLBACK_ENABLE", True) cg.add_define("USE_OTA_ROLLBACK") + # Enable signed app verification without hardware secure boot + if signed_ota := advanced.get(CONF_SIGNED_OTA_VERIFICATION): + add_idf_sdkconfig_option("CONFIG_SECURE_SIGNED_APPS_NO_SECURE_BOOT", True) + add_idf_sdkconfig_option("CONFIG_SECURE_SIGNED_ON_UPDATE_NO_SECURE_BOOT", True) + + scheme = signed_ota[CONF_SIGNING_SCHEME] + for key, flag in SIGNING_SCHEMES.items(): + add_idf_sdkconfig_option(flag, scheme == key) + + if CONF_SIGNING_KEY in signed_ota: + # Private key mode — auto-sign binaries during build + add_idf_sdkconfig_option("CONFIG_SECURE_BOOT_BUILD_SIGNED_BINARIES", True) + add_idf_sdkconfig_option( + "CONFIG_SECURE_BOOT_SIGNING_KEY", + str(signed_ota[CONF_SIGNING_KEY].resolve()), + ) + else: + # Public key mode — verification only, external signing required + add_idf_sdkconfig_option("CONFIG_SECURE_BOOT_BUILD_SIGNED_BINARIES", False) + add_idf_sdkconfig_option( + "CONFIG_SECURE_BOOT_VERIFICATION_KEY", + str(signed_ota[CONF_VERIFICATION_KEY].resolve()), + ) + + cg.add_define("USE_OTA_SIGNED_VERIFICATION") + cg.add_define("ESPHOME_LOOP_TASK_STACK_SIZE", advanced[CONF_LOOP_TASK_STACK_SIZE]) cg.add_define( @@ -2189,6 +2299,7 @@ def _write_sdkconfig(): if write_file_if_changed(internal_path, contents): # internal changed, update real one write_file_if_changed(sdk_path, contents) + clean_build(clear_pio_cache=False) def _write_idf_component_yml(): diff --git a/esphome/components/esp32/gpio_esp32_c6.py b/esphome/components/esp32/gpio_esp32_c6.py index 993606d9de..bd7bb9e220 100644 --- a/esphome/components/esp32/gpio_esp32_c6.py +++ b/esphome/components/esp32/gpio_esp32_c6.py @@ -26,8 +26,8 @@ _LOGGER = logging.getLogger(__name__) 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 < 0 or value > 30: + raise cv.Invalid(f"Invalid pin number: {value} (must be 0-30)") 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]})" @@ -47,8 +47,8 @@ def esp32_c6_validate_supports(value: dict[str, Any]) -> dict[str, Any]: mode = value[CONF_MODE] is_input = mode[CONF_INPUT] - if num < 0 or num > 23: - raise cv.Invalid(f"Invalid pin number: {num} (must be 0-23)") + if num < 0 or num > 30: + raise cv.Invalid(f"Invalid pin number: {num} (must be 0-30)") if is_input: # All ESP32 pins support input mode pass diff --git a/esphome/components/esp32/post_build.py.script b/esphome/components/esp32/post_build.py.script index 5ef5860687..8d13214259 100644 --- a/esphome/components/esp32/post_build.py.script +++ b/esphome/components/esp32/post_build.py.script @@ -8,6 +8,99 @@ import shutil # noqa: E402 from glob import glob # noqa: E402 +def _parse_sdkconfig(sdkconfig_path): + """Parse sdkconfig file and return a dict of CONFIG_ options.""" + options = {} + try: + for line in sdkconfig_path.read_text().splitlines(): + line = line.strip() + if line and not line.startswith("#") and "=" in line: + key, _, value = line.partition("=") + # Strip surrounding quotes from string values + if value.startswith('"') and value.endswith('"'): + value = value[1:-1] + options[key] = value + except FileNotFoundError: + pass + return options + + +def sign_firmware(source, target, env): + """ + Sign the firmware binary using espsecure.py if signed OTA verification is enabled. + Reads signing configuration from sdkconfig. + """ + build_dir = pathlib.Path(env.subst("$BUILD_DIR")) + project_dir = pathlib.Path(env.subst("$PROJECT_DIR")) + pioenv = env.subst("$PIOENV") + sdkconfig = _parse_sdkconfig(project_dir / f"sdkconfig.{pioenv}") + + if sdkconfig.get("CONFIG_SECURE_SIGNED_APPS_NO_SECURE_BOOT") != "y": + return + + if sdkconfig.get("CONFIG_SECURE_BOOT_BUILD_SIGNED_BINARIES") != "y": + print("Signed OTA verification enabled but build-time signing disabled.") + print("You must sign the firmware externally before flashing.") + return + + signing_key = sdkconfig.get("CONFIG_SECURE_BOOT_SIGNING_KEY") + if not signing_key: + print("Error: CONFIG_SECURE_BOOT_SIGNING_KEY not set in sdkconfig") + env.Exit(1) + return + + signing_key_path = pathlib.Path(signing_key) + if not signing_key_path.exists(): + print(f"Error: Signing key not found: {signing_key_path}") + env.Exit(1) + return + + # ESPHome only exposes RSA3072 and ECDSA256 (both Secure Boot V2 schemes), + # so the espsecure signature version is always 2. + sign_version = "2" + + firmware_name = os.path.basename(env.subst("$PROGNAME")) + ".bin" + firmware_path = build_dir / firmware_name + + if not firmware_path.exists(): + print(f"Error: Firmware binary not found: {firmware_path}") + env.Exit(1) + return + + python_exe = f'"{env.subst("$PYTHONEXE")}"' + unsigned_path = firmware_path.with_suffix(".unsigned.bin") + + # Keep a copy of the unsigned binary + shutil.copyfile(str(firmware_path), str(unsigned_path)) + + cmd = [ + python_exe, + "-m", + "espsecure", + "sign-data", + "--version", + sign_version, + "--keyfile", + str(signing_key_path), + "--output", + str(firmware_path), + str(unsigned_path), + ] + + print(f"Signing firmware with key: {signing_key_path.name}") + result = env.Execute( + env.VerboseAction(" ".join(cmd), "Signing firmware with espsecure") + ) + + if result == 0: + print("Successfully signed firmware") + else: + print(f"Error: espsecure sign_data failed with code {result}") + # Restore unsigned binary on failure + shutil.copyfile(str(unsigned_path), str(firmware_path)) + env.Exit(1) + + def merge_factory_bin(source, target, env): """ Merges all flash sections into a single .factory.bin using esptool. @@ -124,7 +217,8 @@ def esp32_copy_ota_bin(source, target, env): print(f"Copied firmware to {new_file_name}") -# Run merge first, then ota copy second +# Run signing first, then merge, then ota copy +env.AddPostAction("$BUILD_DIR/${PROGNAME}.bin", sign_firmware) # noqa: F821 env.AddPostAction("$BUILD_DIR/${PROGNAME}.bin", merge_factory_bin) # noqa: F821 env.AddPostAction("$BUILD_DIR/${PROGNAME}.bin", esp32_copy_ota_bin) # noqa: F821 diff --git a/esphome/components/esp32/preferences.cpp b/esphome/components/esp32/preferences.cpp index e88ace3e6b..bc0a34ebe8 100644 --- a/esphome/components/esp32/preferences.cpp +++ b/esphome/components/esp32/preferences.cpp @@ -129,11 +129,15 @@ bool ESP32Preferences::sync() { } s_pending_save.clear(); - 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); + 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 diff --git a/esphome/components/esp32_ble/__init__.py b/esphome/components/esp32_ble/__init__.py index 43208eb87e..79d05049bf 100644 --- a/esphome/components/esp32_ble/__init__.py +++ b/esphome/components/esp32_ble/__init__.py @@ -7,7 +7,6 @@ from typing import Any 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 @@ -134,10 +133,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( @@ -145,7 +172,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( @@ -153,7 +186,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( @@ -161,7 +200,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( @@ -169,7 +214,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: @@ -225,10 +276,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) @@ -326,14 +373,14 @@ def bt_uuid(value): value = in_value.upper() if len(value) == len(bt_uuid16_format): - pattern = re.compile("^[A-F|0-9]{4,}$") + pattern = re.compile("^[A-F0-9]{4,}$") if not pattern.match(value): raise cv.Invalid( f"Invalid hexadecimal value for 16 bit UUID format: '{in_value}'" ) return value if len(value) == len(bt_uuid32_format): - pattern = re.compile("^[A-F|0-9]{8,}$") + pattern = re.compile("^[A-F0-9]{8,}$") if not pattern.match(value): raise cv.Invalid( f"Invalid hexadecimal value for 32 bit UUID format: '{in_value}'" @@ -341,7 +388,7 @@ def bt_uuid(value): return value if len(value) == len(bt_uuid128_format): pattern = re.compile( - "^[A-F|0-9]{8,}-[A-F|0-9]{4,}-[A-F|0-9]{4,}-[A-F|0-9]{4,}-[A-F|0-9]{12,}$" + "^[A-F0-9]{8,}-[A-F0-9]{4,}-[A-F0-9]{4,}-[A-F0-9]{4,}-[A-F0-9]{12,}$" ) if not pattern.match(value): raise cv.Invalid( @@ -544,11 +591,6 @@ async def to_code(config): cg.add(var.set_name(name)) await cg.register_component(var, config) - # BLE uses the socket wake_loop_threadsafe() mechanism to wake the main loop from BLE tasks - # This enables low-latency (~12μs) BLE event processing instead of waiting for - # select() timeout (0-16ms). The wake socket is shared across all components. - socket.require_wake_loop_threadsafe() - # Define max connections for use in C++ code (e.g., ble_server.h) max_connections = config.get(CONF_MAX_CONNECTIONS, DEFAULT_MAX_CONNECTIONS) cg.add_define("USE_ESP32_BLE_MAX_CONNECTIONS", max_connections) diff --git a/esphome/components/esp32_ble/ble.cpp b/esphome/components/esp32_ble/ble.cpp index 317f8fd11b..0280439731 100644 --- a/esphome/components/esp32_ble/ble.cpp +++ b/esphome/components/esp32_ble/ble.cpp @@ -399,8 +399,17 @@ void ESP32BLE::loop() { return; } +#ifdef USE_ESP32_BLE_ADVERTISING + if (this->advertising_ != nullptr) { + this->advertising_->loop(); + } +#endif + BLEEvent *ble_event = this->ble_events_.pop(); - while (ble_event != nullptr) { + if (ble_event == nullptr) + return; + + do { switch (ble_event->type_) { #if defined(USE_ESP32_BLE_SERVER) && defined(ESPHOME_ESP32_BLE_GATTS_EVENT_HANDLER_COUNT) case BLEEvent::GATTS: { @@ -408,9 +417,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 @@ -420,9 +427,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 @@ -431,10 +436,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; @@ -478,9 +480,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; @@ -497,15 +497,11 @@ void ESP32BLE::loop() { } // Return the event to the pool this->ble_event_pool_.release(ble_event); - ble_event = this->ble_events_.pop(); - } -#ifdef USE_ESP32_BLE_ADVERTISING - if (this->advertising_ != nullptr) { - this->advertising_->loop(); - } -#endif + } while ((ble_event = this->ble_events_.pop()) != nullptr); - // Log dropped events periodically + // Log dropped events - only reachable when events were processed. + // Drops only occur when the queue is full, and only this loop drains it, + // so if pop() returned nullptr above we can skip this check (saves a memw). uint16_t dropped = this->ble_events_.get_and_reset_dropped_count(); if (dropped > 0) { ESP_LOGW(TAG, "Dropped %u BLE events due to buffer overflow", dropped); @@ -518,9 +514,7 @@ void ESP32BLE::loop_handle_state_transition_not_active_() { 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(); - } + this->ble_status_event_callbacks_.call(); #endif if (!ble_dismantle_()) { @@ -605,9 +599,7 @@ void ESP32BLE::gap_event_handler(esp_gap_ble_cb_event_t event, esp_ble_gap_cb_pa GAP_SECURITY_EVENTS: enqueue_ble_event(event, param); // Wake up main loop to process security event immediately -#if defined(USE_SOCKET_SELECT_SUPPORT) && defined(USE_WAKE_LOOP_THREADSAFE) App.wake_loop_threadsafe(); -#endif return; // Ignore these GAP events as they are not relevant for our use case @@ -628,9 +620,7 @@ void ESP32BLE::gatts_event_handler(esp_gatts_cb_event_t event, esp_gatt_if_t gat esp_ble_gatts_cb_param_t *param) { enqueue_ble_event(event, gatts_if, param); // Wake up main loop to process GATT event immediately -#if defined(USE_SOCKET_SELECT_SUPPORT) && defined(USE_WAKE_LOOP_THREADSAFE) App.wake_loop_threadsafe(); -#endif } #endif @@ -639,9 +629,7 @@ void ESP32BLE::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gat esp_ble_gattc_cb_param_t *param) { enqueue_ble_event(event, gattc_if, param); // Wake up main loop to process GATT event immediately -#if defined(USE_SOCKET_SELECT_SUPPORT) && defined(USE_WAKE_LOOP_THREADSAFE) App.wake_loop_threadsafe(); -#endif } #endif diff --git a/esphome/components/esp32_ble/ble.h b/esphome/components/esp32_ble/ble.h index 82b2789461..de8c8c2343 100644 --- a/esphome/components/esp32_ble/ble.h +++ b/esphome/components/esp32_ble/ble.h @@ -87,37 +87,6 @@ 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; } @@ -154,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; } @@ -202,21 +177,27 @@ 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) diff --git a/esphome/components/esp32_ble_beacon/__init__.py b/esphome/components/esp32_ble_beacon/__init__.py index 04c783980d..8052c13596 100644 --- a/esphome/components/esp32_ble_beacon/__init__.py +++ b/esphome/components/esp32_ble_beacon/__init__.py @@ -10,12 +10,7 @@ AUTO_LOAD = ["esp32_ble"] DEPENDENCIES = ["esp32"] 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), -) +ESP32BLEBeacon = esp32_ble_beacon_ns.class_("ESP32BLEBeacon", cg.Component) CONF_MAJOR = "major" CONF_MINOR = "minor" CONF_MIN_INTERVAL = "min_interval" diff --git a/esphome/components/esp32_ble_beacon/esp32_ble_beacon.h b/esphome/components/esp32_ble_beacon/esp32_ble_beacon.h index 7a0424f3aa..44a7133454 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: explicit ESP32BLEBeacon(const std::array &uuid) : uuid_(uuid) {} @@ -51,7 +51,7 @@ class ESP32BLEBeacon : public Component, public GAPEventHandler, public Parented #ifndef CONFIG_ESP_HOSTED_ENABLE_BT_BLUEDROID void set_tx_power(esp_power_level_t val) { this->tx_power_ = val; } #endif - void gap_event_handler(esp_gap_ble_cb_event_t event, esp_ble_gap_cb_param_t *param) override; + void gap_event_handler(esp_gap_ble_cb_event_t event, esp_ble_gap_cb_param_t *param); protected: void on_advertise_(); diff --git a/esphome/components/esp32_ble_client/ble_client_base.cpp b/esphome/components/esp32_ble_client/ble_client_base.cpp index 9d6e079d92..7f0f2c624d 100644 --- a/esphome/components/esp32_ble_client/ble_client_base.cpp +++ b/esphome/components/esp32_ble_client/ble_client_base.cpp @@ -350,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: { diff --git a/esphome/components/esp32_ble_server/__init__.py b/esphome/components/esp32_ble_server/__init__.py index 827ddba955..7bf3092a4e 100644 --- a/esphome/components/esp32_ble_server/__init__.py +++ b/esphome/components/esp32_ble_server/__init__.py @@ -72,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( @@ -308,24 +307,30 @@ def final_validate_config(config): # 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 - char_config = [ + matches = [ char_conf for service_conf in config[CONF_SERVICES] for char_conf in service_conf[CONF_CHARACTERISTICS] if char_conf[CONF_ID] == char_id - ][0] + ] + if not matches: + continue + char_config = matches[0] if not char_config[CONF_NOTIFY]: raise cv.Invalid( f"Characteristic {char_config[CONF_UUID]} has notify actions and the {CONF_NOTIFY} property is not set" ) for char_id in CORE.data.get(DOMAIN, {}).get(KEY_SET_VALUE, set()): # Look for the characteristic in the configuration - char_config = [ + matches = [ char_conf for service_conf in config[CONF_SERVICES] for char_conf in service_conf[CONF_CHARACTERISTICS] if char_conf[CONF_ID] == char_id - ][0] + ] + if not matches: + continue + char_config = matches[0] if isinstance(char_config.get(CONF_VALUE, {}).get(CONF_DATA), cv.Lambda): raise cv.Invalid( f"Characteristic {char_config[CONF_UUID]} has both a set_value action and a templated value" diff --git a/esphome/components/esp32_ble_server/ble_server.h b/esphome/components/esp32_ble_server/ble_server.h index 1b419d2ee4..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; @@ -53,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) { 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 c5e8f3178d..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") diff --git a/esphome/components/esp32_ble_tracker/esp32_ble_tracker.cpp b/esphome/components/esp32_ble_tracker/esp32_ble_tracker.cpp index 6dce70f839..c7f2319d69 100644 --- a/esphome/components/esp32_ble_tracker/esp32_ble_tracker.cpp +++ b/esphome/components/esp32_ble_tracker/esp32_ble_tracker.cpp @@ -88,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 @@ -243,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_) @@ -849,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 f50ed107b6..43405b02b7 100644 --- a/esphome/components/esp32_ble_tracker/esp32_ble_tracker.h +++ b/esphome/components/esp32_ble_tracker/esp32_ble_tracker.h @@ -291,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 @@ -325,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; @@ -436,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 afab849a7c..5165956806 100644 --- a/esphome/components/esp32_camera/__init__.py +++ b/esphome/components/esp32_camera/__init__.py @@ -2,7 +2,7 @@ import logging from esphome import automation, pins import esphome.codegen as cg -from esphome.components import i2c, socket +from esphome.components import i2c from esphome.components.esp32 import add_idf_component, add_idf_sdkconfig_option from esphome.components.psram import DOMAIN as psram_domain import esphome.config_validation as cv @@ -29,7 +29,7 @@ from esphome.types import ConfigType _LOGGER = logging.getLogger(__name__) -AUTO_LOAD = ["camera", "socket"] +AUTO_LOAD = ["camera"] DEPENDENCIES = ["esp32"] esp32_camera_ns = cg.esphome_ns.namespace("esp32_camera") @@ -370,7 +370,6 @@ SETTERS = { async def to_code(config): cg.add_define("USE_CAMERA") - socket.require_wake_loop_threadsafe() var = cg.new_Pvariable(config[CONF_ID]) await setup_entity(var, config, "camera") await cg.register_component(var, config) @@ -400,7 +399,7 @@ async def to_code(config): 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.5") + 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 085feb8c8a..a7546476d8 100644 --- a/esphome/components/esp32_camera/esp32_camera.cpp +++ b/esphome/components/esp32_camera/esp32_camera.cpp @@ -521,11 +521,9 @@ void ESP32Camera::framebuffer_task(void *pv) { camera_fb_t *framebuffer = esp_camera_fb_get(); xQueueSend(that->framebuffer_get_queue_, &framebuffer, portMAX_DELAY); // Only wake the main loop if there's a pending request to consume the frame -#if defined(USE_SOCKET_SELECT_SUPPORT) && defined(USE_WAKE_LOOP_THREADSAFE) if (that->has_requested_image_()) { App.wake_loop_threadsafe(); } -#endif // return is no-op for config with 1 fb xQueueReceive(that->framebuffer_return_queue_, &framebuffer, portMAX_DELAY); esp_camera_fb_return(framebuffer); diff --git a/esphome/components/esp32_hosted/update/esp32_hosted_update.cpp b/esphome/components/esp32_hosted/update/esp32_hosted_update.cpp index dcd6e643c2..af35d32888 100644 --- a/esphome/components/esp32_hosted/update/esp32_hosted_update.cpp +++ b/esphome/components/esp32_hosted/update/esp32_hosted_update.cpp @@ -448,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_touch/esp32_touch.cpp b/esphome/components/esp32_touch/esp32_touch.cpp index 0d331b29d6..e44bc807e9 100644 --- a/esphome/components/esp32_touch/esp32_touch.cpp +++ b/esphome/components/esp32_touch/esp32_touch.cpp @@ -217,7 +217,7 @@ void ESP32TouchComponent::setup() { 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 %d failed: %s", i, esp_err_to_name(err)); + ESP_LOGW(TAG, "Oneshot scan %" PRIu32 " failed: %s", i, esp_err_to_name(err)); } } diff --git a/esphome/components/esp8266/__init__.py b/esphome/components/esp8266/__init__.py index 16043b6d69..fcd3499b15 100644 --- a/esphome/components/esp8266/__init__.py +++ b/esphome/components/esp8266/__init__.py @@ -1,5 +1,6 @@ import logging from pathlib import Path +import re import esphome.codegen as cg import esphome.config_validation as cv @@ -18,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 ( @@ -40,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 @@ -181,6 +213,7 @@ 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, @@ -200,17 +233,25 @@ async def to_code(config): cg.add_define("ESPHOME_BOARD", config[CONF_BOARD]) cg.add_define("ESPHOME_VARIANT", "ESP8266") cg.add_define(ThreadModel.SINGLE) + cg.add_define("USE_ESP8266_CRASH_HANDLER") - cg.add_platformio_option( - "extra_scripts", - [ - "pre:testing_mode.py", - "pre:exclude_updater.py", - "pre:exclude_waveform.py", - "pre:remove_float_scanf.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") diff --git a/esphome/components/esp8266/crash_handler.cpp b/esphome/components/esp8266/crash_handler.cpp new file mode 100644 index 0000000000..91b0cf9082 --- /dev/null +++ b/esphome/components/esp8266/crash_handler.cpp @@ -0,0 +1,235 @@ +#ifdef USE_ESP8266 + +#include "esphome/core/defines.h" +#ifdef USE_ESP8266_CRASH_HANDLER + +#include "crash_handler.h" +#include "esphome/core/log.h" + +#include + +extern "C" { +#include + +// Global reset info struct populated by SDK/Arduino core at boot +extern struct rst_info resetInfo; +} + +// Xtensa windowed-ABI: bits[31:30] encode call type (CALL0=00, CALL4=01, +// CALL8=10, CALL12=11). Mask and force bit 30 to recover the real address. +static constexpr uint32_t XTENSA_ADDR_MASK = 0x3FFFFFFF; +static constexpr uint32_t XTENSA_CODE_BASE = 0x40000000; + +// ESP8266 memory map boundaries for code regions +static constexpr uint32_t IRAM_START = 0x40100000; +static constexpr uint32_t IRAM_END = 0x40108000; // 32KB + +// Linker symbols for the actual firmware IROM section. +// Using these instead of a conservative upper bound (0x40400000) prevents +// false positives from stale stack values beyond the actual flash mapping. +extern "C" { +// NOLINTBEGIN(bugprone-reserved-identifier,readability-identifier-naming,readability-redundant-declaration) +extern void _irom0_text_start(void); +extern void _irom0_text_end(void); +// NOLINTEND(bugprone-reserved-identifier,readability-identifier-naming,readability-redundant-declaration) +} + +// Check if a value looks like a code address in IRAM or flash-mapped IROM. +// IRAM_ATTR as safety net — normally inlined into custom_crash_callback, but +// ensures correctness if the compiler ever chooses not to inline. +static inline bool IRAM_ATTR is_code_addr(uint32_t val) { + uint32_t addr = (val & XTENSA_ADDR_MASK) | XTENSA_CODE_BASE; + return (addr >= IRAM_START && addr < IRAM_END) || + (addr >= (uint32_t) _irom0_text_start && addr < (uint32_t) _irom0_text_end); +} + +// Recover the actual code address from a windowed-ABI return address on the stack. +static inline uint32_t IRAM_ATTR recover_code_addr(uint32_t val) { return (val & XTENSA_ADDR_MASK) | XTENSA_CODE_BASE; } + +// RTC user memory layout for crash backtrace data. +// User-accessible RTC memory: blocks 64-191 (each block = 4 bytes). +// We use blocks 174-191 (last 18 blocks, 72 bytes) to minimize conflicts. +// Store 16 raw candidates, filter to real return addresses at log time. +static constexpr uint8_t RTC_CRASH_BASE = 174; +static constexpr size_t MAX_BACKTRACE = 16; + +// Magic word packs sentinel, version, and count into one uint32_t: +// bits[31:16] = sentinel +// bits[15:8] = version +// bits[7:0] = backtrace count +static constexpr uint8_t CRASH_SENTINEL_BITS = 16; +static constexpr uint8_t CRASH_VERSION_BITS = 8; + +static constexpr uint16_t CRASH_SENTINEL_VALUE = 0xDEAD; +static constexpr uint8_t CRASH_VERSION_VALUE = 1; + +static constexpr uint32_t CRASH_SENTINEL = static_cast(CRASH_SENTINEL_VALUE) << CRASH_SENTINEL_BITS; +static constexpr uint32_t CRASH_VERSION = static_cast(CRASH_VERSION_VALUE) << CRASH_VERSION_BITS; +static constexpr uint32_t CRASH_SENTINEL_MASK = static_cast(0xFFFF) << CRASH_SENTINEL_BITS; +static constexpr uint32_t CRASH_VERSION_MASK = static_cast(0xFF) << CRASH_VERSION_BITS; +static constexpr uint32_t CRASH_COUNT_MASK = 0xFF; + +// Struct layout: 18 RTC blocks (72 bytes): +// [0] = magic (sentinel | version | count) +// [1..16] = up to 16 code addresses from stack scanning +// [17] = epc1 at crash time (to skip duplicates at log time) +struct RtcCrashData { + uint32_t magic; + uint32_t backtrace[MAX_BACKTRACE]; + uint32_t epc1; // Fault PC, used to filter duplicates +}; +static_assert(sizeof(RtcCrashData) == 72, "RtcCrashData must fit in 18 RTC blocks"); + +namespace esphome::esp8266 { + +static const char *const TAG = "esp8266"; + +static inline bool is_crash_reason(uint32_t reason) { + return reason == REASON_WDT_RST || reason == REASON_EXCEPTION_RST || reason == REASON_SOFT_WDT_RST; +} + +bool crash_handler_has_data() { return is_crash_reason(resetInfo.reason); } + +// Xtensa exception cause names for the LX106 core (ESP8266). +// Only includes causes that can actually occur on the LX106 — it has no MMU, +// no TLB, no PIF, and no privilege levels, so causes 12-18 and 24-26 are +// impossible and omitted. The numeric cause is always logged as fallback. +// Uses if-else with LOG_STR to avoid CSWTCH jump tables (RAM on ESP8266). +static const LogString *get_exception_cause(uint32_t cause) { + if (cause == 0) + return LOG_STR("IllegalInst"); + if (cause == 2) + return LOG_STR("InstFetchErr"); + if (cause == 3) + return LOG_STR("LoadStoreErr"); + if (cause == 4) + return LOG_STR("Level1Int"); + if (cause == 6) + return LOG_STR("DivByZero"); + if (cause == 9) + return LOG_STR("Alignment"); + if (cause == 20) + return LOG_STR("InstFetchProhibit"); + if (cause == 28) + return LOG_STR("LoadProhibit"); + if (cause == 29) + return LOG_STR("StoreProhibit"); + return nullptr; +} + +static const LogString *get_reset_reason(uint32_t reason) { + if (reason == REASON_WDT_RST) + return LOG_STR("Hardware WDT"); + if (reason == REASON_EXCEPTION_RST) + return LOG_STR("Exception"); + if (reason == REASON_SOFT_WDT_RST) + return LOG_STR("Soft WDT"); + return LOG_STR("Unknown"); +} + +// Read backtrace from RTC user memory into caller-provided buffer. +// Returns the number of valid backtrace entries (0 if no data found). +static uint8_t read_rtc_backtrace(uint32_t *backtrace, size_t max_entries) { + RtcCrashData rtc_data; + if (!system_rtc_mem_read(RTC_CRASH_BASE, &rtc_data, sizeof(rtc_data))) + return 0; + uint32_t magic = rtc_data.magic; + if ((magic & CRASH_SENTINEL_MASK) != CRASH_SENTINEL || (magic & CRASH_VERSION_MASK) != CRASH_VERSION) + return 0; + uint8_t raw_count = magic & CRASH_COUNT_MASK; + if (raw_count > MAX_BACKTRACE) + raw_count = MAX_BACKTRACE; + // Skip any that match epc1 (already reported as the fault PC). + // Note: we cannot verify CALL instructions at addr-3 on ESP8266 because + // reading from IROM causes LoadStoreError due to flash cache conflicts + // (the reading code and target can share a direct-mapped cache line). + // The linker-symbol IROM bounds already eliminate most false positives. + uint8_t out = 0; + for (uint8_t i = 0; i < raw_count && out < max_entries; i++) { + uint32_t addr = rtc_data.backtrace[i]; + if (addr != rtc_data.epc1) + backtrace[out++] = addr; + } + return out; +} + +// 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 (!is_crash_reason(resetInfo.reason)) + return; + + // Read and filter backtrace from RTC into stack-local buffer (no persistent RAM cost). + // Both resetInfo and RTC data survive until the next reset, so this can be + // called multiple times (logger init + API subscribe) with the same result. + uint32_t backtrace[MAX_BACKTRACE]; + uint8_t bt_count = read_rtc_backtrace(backtrace, MAX_BACKTRACE); + + ESP_LOGE(TAG, "*** CRASH DETECTED ON PREVIOUS BOOT ***"); + // GCC's ROM divide routine triggers IllegalInstruction (exccause=0) at specific + // ROM addresses instead of IntegerDivideByZero (exccause=6). Patch to match + // the Arduino core's postmortem handler behavior. + static constexpr uint32_t EXCCAUSE_ILLEGAL_INSTRUCTION = 0; + static constexpr uint32_t EXCCAUSE_INTEGER_DIVIDE_BY_ZERO = 6; + static constexpr uint32_t ROM_DIV_ZERO_ADDR_1 = 0x4000dce5; + static constexpr uint32_t ROM_DIV_ZERO_ADDR_2 = 0x4000dd3d; + uint32_t exccause = resetInfo.exccause; + if (exccause == EXCCAUSE_ILLEGAL_INSTRUCTION && + (resetInfo.epc1 == ROM_DIV_ZERO_ADDR_1 || resetInfo.epc1 == ROM_DIV_ZERO_ADDR_2)) { + exccause = EXCCAUSE_INTEGER_DIVIDE_BY_ZERO; + } + const LogString *cause = get_exception_cause(exccause); + if (cause != nullptr) { + ESP_LOGE(TAG, " Reason: %s - %s (exccause=%" PRIu32 ")", LOG_STR_ARG(get_reset_reason(resetInfo.reason)), + LOG_STR_ARG(cause), exccause); + } else { + ESP_LOGE(TAG, " Reason: %s (exccause=%" PRIu32 ")", LOG_STR_ARG(get_reset_reason(resetInfo.reason)), exccause); + } + ESP_LOGE(TAG, " PC: 0x%08" PRIX32, resetInfo.epc1); + if (resetInfo.reason == REASON_EXCEPTION_RST) { + ESP_LOGE(TAG, " EXCVADDR: 0x%08" PRIX32, resetInfo.excvaddr); + } + for (uint8_t i = 0; i < bt_count; i++) { + ESP_LOGE(TAG, " BT%d: 0x%08" PRIX32, i, backtrace[i]); + } +} + +} // namespace esphome::esp8266 + +// --- Custom crash callback --- +// Overrides the weak custom_crash_callback() from Arduino core's +// core_esp8266_postmortem.cpp. Called during exception handling before +// the device restarts. We scan the full stack for code addresses and store +// them in RTC user memory (which survives software reset). +extern "C" void IRAM_ATTR custom_crash_callback(struct rst_info *rst_info, uint32_t stack, uint32_t stack_end) { + // No zero-init — only magic, epc1, and backtrace[0..count-1] are read. + // Saves the IRAM cost of a 72-byte zero-init loop. + RtcCrashData data; // NOLINT(cppcoreguidelines-pro-type-member-init) + uint8_t count = 0; + + // Stack pointer from the Xtensa exception frame is always 4-byte aligned. + auto *scan = (uint32_t *) stack; // NOLINT(performance-no-int-to-ptr) + auto *end = (uint32_t *) stack_end; // NOLINT(performance-no-int-to-ptr) + uint32_t epc1 = rst_info->epc1; + + for (; scan < end && count < MAX_BACKTRACE; scan++) { + uint32_t val = *scan; + if (is_code_addr(val)) { + uint32_t addr = recover_code_addr(val); + // Skip epc1 — already reported as the fault PC + if (addr != epc1) + data.backtrace[count++] = addr; + } + } + + data.epc1 = epc1; + data.magic = CRASH_SENTINEL | CRASH_VERSION | count; + + system_rtc_mem_write(RTC_CRASH_BASE, &data, sizeof(data)); +} + +#endif // USE_ESP8266_CRASH_HANDLER +#endif // USE_ESP8266 diff --git a/esphome/components/esp8266/crash_handler.h b/esphome/components/esp8266/crash_handler.h new file mode 100644 index 0000000000..ea3683b834 --- /dev/null +++ b/esphome/components/esp8266/crash_handler.h @@ -0,0 +1,20 @@ +#pragma once + +#ifdef USE_ESP8266 + +#include "esphome/core/defines.h" + +#ifdef USE_ESP8266_CRASH_HANDLER + +namespace esphome::esp8266 { + +/// Log crash data if a crash was detected on previous boot. +void crash_handler_log(); + +/// Returns true if the previous boot was a crash (exception, WDT, or soft WDT). +bool crash_handler_has_data(); + +} // namespace esphome::esp8266 + +#endif // USE_ESP8266_CRASH_HANDLER +#endif // USE_ESP8266 diff --git a/esphome/components/esp8266/gpio.py b/esphome/components/esp8266/gpio.py index 43508afaf9..64be4a6495 100644 --- a/esphome/components/esp8266/gpio.py +++ b/esphome/components/esp8266/gpio.py @@ -155,7 +155,7 @@ ESP8266_PIN_SCHEMA = cv.All( @dataclass class PinInitialState: - mode = 255 + mode: int = 255 level: int = 255 diff --git a/esphome/components/esp8266/preferences.cpp b/esphome/components/esp8266/preferences.cpp index 906fed2b29..f444f03555 100644 --- a/esphome/components/esp8266/preferences.cpp +++ b/esphome/components/esp8266/preferences.cpp @@ -19,12 +19,13 @@ static constexpr uint32_t ESP_RTC_USER_MEM_START = 0x60001200; static constexpr uint32_t ESP_RTC_USER_MEM_SIZE_WORDS = 128; static constexpr uint32_t ESP_RTC_USER_MEM_SIZE_BYTES = ESP_RTC_USER_MEM_SIZE_WORDS * 4; -// RTC memory layout for preferences: -// - Eboot region: RTC words 0-31 (reserved, mapped from preference offset 96-127) -// - Normal region: RTC words 32-127 (mapped from preference offset 0-95) +// RTC memory layout: +// - Eboot region: RTC words 0-31 (reserved, mapped from preference offset 78-109) +// - Normal region: RTC words 32-109 (mapped from preference offset 0-77) +// - Crash handler: RTC words 110-127 (reserved for crash_handler.cpp backtrace data) static constexpr uint32_t RTC_EBOOT_REGION_WORDS = 32; // Words 0-31 reserved for eboot -static constexpr uint32_t RTC_NORMAL_REGION_WORDS = 96; // Words 32-127 for normal prefs -static constexpr uint32_t PREF_TOTAL_WORDS = RTC_EBOOT_REGION_WORDS + RTC_NORMAL_REGION_WORDS; // 128 +static constexpr uint32_t RTC_NORMAL_REGION_WORDS = 78; // Words 32-109 for normal prefs +static constexpr uint32_t PREF_TOTAL_WORDS = RTC_EBOOT_REGION_WORDS + RTC_NORMAL_REGION_WORDS; // 110 // Maximum preference size in words (limited by uint8_t length_words field) static constexpr uint32_t MAX_PREFERENCE_WORDS = 255; diff --git a/esphome/components/esphome/ota/ota_esphome.cpp b/esphome/components/esphome/ota/ota_esphome.cpp index 972d2b2b8d..af9b8ee19a 100644 --- a/esphome/components/esphome/ota/ota_esphome.cpp +++ b/esphome/components/esphome/ota/ota_esphome.cpp @@ -262,7 +262,7 @@ void ESPHomeOTAComponent::handle_data_() { /// 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(); + /// wakeable_delay() in read(); /// write() always returns immediately ota::OTAResponseTypes error_code = ota::OTA_RESPONSE_ERROR_UNKNOWN; bool update_started = false; diff --git a/esphome/components/espnow/__init__.py b/esphome/components/espnow/__init__.py index d1a85ae8fd..a9624734d0 100644 --- a/esphome/components/espnow/__init__.py +++ b/esphome/components/espnow/__init__.py @@ -1,6 +1,6 @@ from esphome import automation, core import esphome.codegen as cg -from esphome.components import socket, wifi +from esphome.components import wifi from esphome.components.udp import CONF_ON_RECEIVE import esphome.config_validation as cv from esphome.const import ( @@ -17,7 +17,7 @@ from esphome.core import HexInt from esphome.types import ConfigType CODEOWNERS = ["@jesserockz"] -AUTO_LOAD = ["socket"] + byte_vector = cg.std_vector.template(cg.uint8) peer_address_t = cg.std_ns.class_("array").template(cg.uint8, 6) @@ -124,14 +124,11 @@ async def to_code(config): var = cg.new_Pvariable(config[CONF_ID]) await cg.register_component(var, config) - # 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() - cg.add_define("USE_ESPNOW") 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, []): @@ -161,15 +158,15 @@ def validate_peer(value): def _validate_raw_data(value): if isinstance(value, str): - if len(value) >= MAX_ESPNOW_PACKET_SIZE: + if len(value) > MAX_ESPNOW_PACKET_SIZE: raise cv.Invalid( - f"'{CONF_DATA}' must be less than {MAX_ESPNOW_PACKET_SIZE} characters long, got {len(value)}" + f"'{CONF_DATA}' must be at most {MAX_ESPNOW_PACKET_SIZE} characters long, got {len(value)}" ) return value if isinstance(value, list): if len(value) > MAX_ESPNOW_PACKET_SIZE: raise cv.Invalid( - f"'{CONF_DATA}' must be less than {MAX_ESPNOW_PACKET_SIZE} bytes long, got {len(value)}" + f"'{CONF_DATA}' must be at most {MAX_ESPNOW_PACKET_SIZE} bytes long, got {len(value)}" ) return cv.Schema([cv.hex_uint8_t])(value) raise cv.Invalid( @@ -248,7 +245,7 @@ async def send_action( data = config.get(CONF_DATA, []) if isinstance(data, str): - data = [cg.RawExpression(f"'{c}'") for c in data] + data = list(data.encode()) templ = await cg.templatable(data, args, byte_vector, byte_vector) cg.add(var.set_data(templ)) diff --git a/esphome/components/espnow/espnow_component.cpp b/esphome/components/espnow/espnow_component.cpp index 78916891f4..282287ca83 100644 --- a/esphome/components/espnow/espnow_component.cpp +++ b/esphome/components/espnow/espnow_component.cpp @@ -4,6 +4,8 @@ #include "espnow_err.h" +#include + #include "esphome/core/application.h" #include "esphome/core/defines.h" #include "esphome/core/helpers.h" @@ -90,10 +92,8 @@ void on_send_report(const uint8_t *mac_addr, esp_now_send_status_t status) // 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) + // Wake main loop immediately to process ESP-NOW send event App.wake_loop_threadsafe(); -#endif } void on_data_received(const esp_now_recv_info_t *info, const uint8_t *data, int size) { @@ -113,10 +113,8 @@ void on_data_received(const esp_now_recv_info_t *info, const uint8_t *data, int // 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) + // Wake main loop immediately to process ESP-NOW receive event App.wake_loop_threadsafe(); -#endif } ESPNowComponent::ESPNowComponent() { global_esp_now = this; } @@ -266,7 +264,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/ethernet/__init__.py b/esphome/components/ethernet/__init__.py index 17459cabb6..d9f51c677e 100644 --- a/esphome/components/ethernet/__init__.py +++ b/esphome/components/ethernet/__init__.py @@ -104,6 +104,8 @@ CONF_CLK_MODE = "clk_mode" CONF_POWER_PIN = "power_pin" CONF_PHY_REGISTERS = "phy_registers" +CONF_INTERFACE = "interface" + CONF_CLOCK_SPEED = "clock_speed" EthernetType = ethernet_ns.enum("EthernetType") @@ -191,6 +193,13 @@ CLK_MODES_DEPRECATED = { "GPIO17_OUT": ("CLK_OUT", 17), } +spi_host_device_t = cg.global_ns.enum("spi_host_device_t") + +SPI_INTERFACE_MAP = { + "spi2": spi_host_device_t.SPI2_HOST, + "spi3": spi_host_device_t.SPI3_HOST, +} + MANUAL_IP_SCHEMA = cv.Schema( { cv.Required(CONF_STATIC_IP): cv.ipv4address, @@ -225,6 +234,24 @@ def _is_framework_spi_polling_mode_supported() -> bool: return False +def _validate_spi_interface(config: ConfigType) -> ConfigType: + """Set default SPI interface or validate user choice against the variant.""" + if not CORE.is_esp32: + return config + from esphome.components.esp32 import VARIANT_ESP32, get_esp32_variant + from esphome.components.spi import get_hw_interface_list + + has_spi3 = "spi3" in sum(get_hw_interface_list(), []) + if CONF_INTERFACE not in config: + # Only classic ESP32 defaults to spi3; all others default to spi2 + config[CONF_INTERFACE] = ( + "spi3" if get_esp32_variant() == VARIANT_ESP32 else "spi2" + ) + elif config[CONF_INTERFACE] == "spi3" and not has_spi3: + raise cv.Invalid("Interface 'spi3' is not available on this variant.") + return config + + def _validate(config): if CONF_USE_ADDRESS not in config: if CONF_MANUAL_IP in config: @@ -368,6 +395,10 @@ SPI_SCHEMA = cv.All( 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(), lower=True), + ), # Set default value (SPI_ETHERNET_DEFAULT_POLLING_INTERVAL) at _validate() cv.Optional(CONF_POLLING_INTERVAL): cv.All( cv.only_on_esp32, @@ -378,6 +409,7 @@ SPI_SCHEMA = cv.All( ), ), cv.only_on([Platform.ESP32, Platform.RP2040]), + _validate_spi_interface, ) CONFIG_SCHEMA = cv.All( @@ -408,37 +440,18 @@ def _final_validate_spi(config): return # SPI interface validation is ESP32-only if config[CONF_TYPE] not in SPI_ETHERNET_TYPES: return - from esphome.components.esp32 import ( - VARIANT_ESP32C3, - VARIANT_ESP32C5, - VARIANT_ESP32C6, - VARIANT_ESP32C61, - VARIANT_ESP32S2, - VARIANT_ESP32S3, - get_esp32_variant, - ) from esphome.components.spi import CONF_INTERFACE_INDEX, get_spi_interface if spi_configs := fv.full_config.get().get(CONF_SPI): - variant = get_esp32_variant() - if variant in ( - VARIANT_ESP32C3, - VARIANT_ESP32C5, - VARIANT_ESP32C6, - VARIANT_ESP32C61, - VARIANT_ESP32S2, - VARIANT_ESP32S3, - ): - spi_host = "SPI2_HOST" - else: - spi_host = "SPI3_HOST" + # get_spi_interface() returns strings like "SPI2_HOST" + spi_host = f"{config[CONF_INTERFACE].upper()}_HOST" for spi_conf in spi_configs: if (index := spi_conf.get(CONF_INTERFACE_INDEX)) is not None: interface = get_spi_interface(index) if interface == spi_host: raise cv.Invalid( - f"`spi` component is using interface '{interface}'. " - f"To use {config[CONF_TYPE]}, you must change the `interface` on the `spi` component.", + f"The `ethernet` and `spi` components are both using interface '{interface}'. " + f"To use {config[CONF_TYPE]}, change the `interface` on either `ethernet:` or `spi:`." ) @@ -528,6 +541,8 @@ async def _to_code_esp32(var: cg.Pvariable, config: ConfigType) -> None: cg.add(var.set_clock_speed(config[CONF_CLOCK_SPEED])) cg.add_define("USE_ETHERNET_SPI") + + cg.add(var.set_interface(SPI_INTERFACE_MAP[config[CONF_INTERFACE]])) add_idf_sdkconfig_option("CONFIG_ETH_USE_SPI_ETHERNET", 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 diff --git a/esphome/components/ethernet/ethernet_component.h b/esphome/components/ethernet/ethernet_component.h index c6e37d01ea..b760ba2af7 100644 --- a/esphome/components/ethernet/ethernet_component.h +++ b/esphome/components/ethernet/ethernet_component.h @@ -11,6 +11,9 @@ #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" @@ -135,6 +138,7 @@ class EthernetComponent final : public Component { void set_interrupt_pin(uint8_t interrupt_pin); void set_reset_pin(uint8_t reset_pin); void set_clock_speed(int clock_speed); + void set_interface(spi_host_device_t interface); #ifdef USE_ETHERNET_SPI_POLLING_SUPPORT void set_polling_interval(uint32_t polling_interval); #endif @@ -201,6 +205,7 @@ class EthernetComponent final : public Component { int reset_pin_{-1}; int phy_addr_spi_{-1}; int clock_speed_; + spi_host_device_t interface_{SPI3_HOST}; #ifdef USE_ETHERNET_SPI_POLLING_SUPPORT uint32_t polling_interval_{0}; #endif diff --git a/esphome/components/ethernet/ethernet_component_esp32.cpp b/esphome/components/ethernet/ethernet_component_esp32.cpp index a170239e03..d4585bf100 100644 --- a/esphome/components/ethernet/ethernet_component_esp32.cpp +++ b/esphome/components/ethernet/ethernet_component_esp32.cpp @@ -158,12 +158,7 @@ void EthernetComponent::setup() { .intr_flags = 0, }; -#if defined(USE_ESP32_VARIANT_ESP32C3) || defined(USE_ESP32_VARIANT_ESP32C5) || defined(USE_ESP32_VARIANT_ESP32C6) || \ - defined(USE_ESP32_VARIANT_ESP32C61) || defined(USE_ESP32_VARIANT_ESP32S2) || defined(USE_ESP32_VARIANT_ESP32S3) - auto host = SPI2_HOST; -#else - auto host = SPI3_HOST; -#endif + auto host = this->interface_; err = spi_bus_initialize(host, &buscfg, SPI_DMA_CH_AUTO); ESPHL_ERROR_CHECK(err, "SPI bus initialize error"); @@ -458,6 +453,11 @@ void EthernetComponent::dump_config() { " MOSI Pin: %u\n" " CS Pin: %u", this->clk_pin_, this->miso_pin_, this->mosi_pin_, this->cs_pin_); + const char *spi_interface = "spi3"; + if (this->interface_ == SPI2_HOST) { + spi_interface = "spi2"; + } + ESP_LOGCONFIG(TAG, " Interface: %s", spi_interface); #ifdef USE_ETHERNET_SPI_POLLING_SUPPORT if (this->polling_interval_ != 0) { ESP_LOGCONFIG(TAG, " Polling Interval: %" PRIu32 " ms", this->polling_interval_); @@ -760,6 +760,7 @@ 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 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/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 1538e303f1..3de796dd25 100644 --- a/esphome/components/ezo_pmp/__init__.py +++ b/esphome/components/ezo_pmp/__init__.py @@ -286,7 +286,7 @@ async def ezo_pmp_change_i2c_address_to_code(config, action_id, template_arg, ar 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 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 34f89d73b6..41ee627c4b 100644 --- a/esphome/components/factory_reset/factory_reset.h +++ b/esphome/components/factory_reset/factory_reset.h @@ -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/fingerprint_grow/__init__.py b/esphome/components/fingerprint_grow/__init__.py index 2637097be8..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( diff --git a/esphome/components/fingerprint_grow/fingerprint_grow.h b/esphome/components/fingerprint_grow/fingerprint_grow.h index 63839534f6..947c701c98 100644 --- a/esphome/components/fingerprint_grow/fingerprint_grow.h +++ b/esphome/components/fingerprint_grow/fingerprint_grow.h @@ -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 c8813bf1bc..a1339a4bc1 100644 --- a/esphome/components/font/__init__.py +++ b/esphome/components/font/__init__.py @@ -238,7 +238,7 @@ def validate_font_config(config): return config -FONT_EXTENSIONS = (".ttf", ".woff", ".otf", "bdf", ".pcf") +FONT_EXTENSIONS = (".ttf", ".woff", ".otf", ".bdf", ".pcf") def validate_truetype_file(value): diff --git a/esphome/components/gcja5/sensor.py b/esphome/components/gcja5/sensor.py index ec26447ccb..e4de7721c6 100644 --- a/esphome/components/gcja5/sensor.py +++ b/esphome/components/gcja5/sensor.py @@ -24,73 +24,77 @@ DEPENDENCIES = ["uart"] gcja5_ns = cg.esphome_ns.namespace("gcja5") -GCJA5Component = gcja5_ns.class_("GCJA5Component", cg.PollingComponent, uart.UARTDevice) +GCJA5Component = gcja5_ns.class_("GCJA5Component", cg.Component, uart.UARTDevice) CONF_PMC_0_3 = "pmc_0_3" CONF_PMC_5_0 = "pmc_5_0" -CONFIG_SCHEMA = cv.Schema( - { - cv.GenerateID(): cv.declare_id(GCJA5Component), - cv.Optional(CONF_PM_1_0): sensor.sensor_schema( - unit_of_measurement=UNIT_MICROGRAMS_PER_CUBIC_METER, - icon=ICON_CHEMICAL_WEAPON, - accuracy_decimals=2, - device_class=DEVICE_CLASS_PM1, - state_class=STATE_CLASS_MEASUREMENT, - ), - cv.Optional(CONF_PM_2_5): sensor.sensor_schema( - unit_of_measurement=UNIT_MICROGRAMS_PER_CUBIC_METER, - icon=ICON_CHEMICAL_WEAPON, - accuracy_decimals=2, - device_class=DEVICE_CLASS_PM25, - state_class=STATE_CLASS_MEASUREMENT, - ), - cv.Optional(CONF_PM_10_0): sensor.sensor_schema( - unit_of_measurement=UNIT_MICROGRAMS_PER_CUBIC_METER, - icon=ICON_CHEMICAL_WEAPON, - accuracy_decimals=2, - device_class=DEVICE_CLASS_PM10, - state_class=STATE_CLASS_MEASUREMENT, - ), - cv.Optional(CONF_PMC_0_3): sensor.sensor_schema( - unit_of_measurement=UNIT_MICROGRAMS_PER_CUBIC_METER, - icon=ICON_COUNTER, - accuracy_decimals=0, - state_class=STATE_CLASS_MEASUREMENT, - ), - cv.Optional(CONF_PMC_0_5): sensor.sensor_schema( - unit_of_measurement=UNIT_MICROGRAMS_PER_CUBIC_METER, - icon=ICON_COUNTER, - accuracy_decimals=0, - state_class=STATE_CLASS_MEASUREMENT, - ), - cv.Optional(CONF_PMC_1_0): sensor.sensor_schema( - unit_of_measurement=UNIT_MICROGRAMS_PER_CUBIC_METER, - icon=ICON_COUNTER, - accuracy_decimals=0, - state_class=STATE_CLASS_MEASUREMENT, - ), - cv.Optional(CONF_PMC_2_5): sensor.sensor_schema( - unit_of_measurement=UNIT_MICROGRAMS_PER_CUBIC_METER, - icon=ICON_COUNTER, - accuracy_decimals=0, - state_class=STATE_CLASS_MEASUREMENT, - ), - cv.Optional(CONF_PMC_5_0): sensor.sensor_schema( - unit_of_measurement=UNIT_MICROGRAMS_PER_CUBIC_METER, - icon=ICON_COUNTER, - accuracy_decimals=0, - state_class=STATE_CLASS_MEASUREMENT, - ), - cv.Optional(CONF_PMC_10_0): sensor.sensor_schema( - unit_of_measurement=UNIT_MICROGRAMS_PER_CUBIC_METER, - icon=ICON_COUNTER, - accuracy_decimals=0, - state_class=STATE_CLASS_MEASUREMENT, - ), - } -).extend(uart.UART_DEVICE_SCHEMA) +CONFIG_SCHEMA = ( + cv.Schema( + { + cv.GenerateID(): cv.declare_id(GCJA5Component), + cv.Optional(CONF_PM_1_0): sensor.sensor_schema( + unit_of_measurement=UNIT_MICROGRAMS_PER_CUBIC_METER, + icon=ICON_CHEMICAL_WEAPON, + accuracy_decimals=2, + device_class=DEVICE_CLASS_PM1, + state_class=STATE_CLASS_MEASUREMENT, + ), + cv.Optional(CONF_PM_2_5): sensor.sensor_schema( + unit_of_measurement=UNIT_MICROGRAMS_PER_CUBIC_METER, + icon=ICON_CHEMICAL_WEAPON, + accuracy_decimals=2, + device_class=DEVICE_CLASS_PM25, + state_class=STATE_CLASS_MEASUREMENT, + ), + cv.Optional(CONF_PM_10_0): sensor.sensor_schema( + unit_of_measurement=UNIT_MICROGRAMS_PER_CUBIC_METER, + icon=ICON_CHEMICAL_WEAPON, + accuracy_decimals=2, + device_class=DEVICE_CLASS_PM10, + state_class=STATE_CLASS_MEASUREMENT, + ), + cv.Optional(CONF_PMC_0_3): sensor.sensor_schema( + unit_of_measurement=UNIT_MICROGRAMS_PER_CUBIC_METER, + icon=ICON_COUNTER, + accuracy_decimals=0, + state_class=STATE_CLASS_MEASUREMENT, + ), + cv.Optional(CONF_PMC_0_5): sensor.sensor_schema( + unit_of_measurement=UNIT_MICROGRAMS_PER_CUBIC_METER, + icon=ICON_COUNTER, + accuracy_decimals=0, + state_class=STATE_CLASS_MEASUREMENT, + ), + cv.Optional(CONF_PMC_1_0): sensor.sensor_schema( + unit_of_measurement=UNIT_MICROGRAMS_PER_CUBIC_METER, + icon=ICON_COUNTER, + accuracy_decimals=0, + state_class=STATE_CLASS_MEASUREMENT, + ), + cv.Optional(CONF_PMC_2_5): sensor.sensor_schema( + unit_of_measurement=UNIT_MICROGRAMS_PER_CUBIC_METER, + icon=ICON_COUNTER, + accuracy_decimals=0, + state_class=STATE_CLASS_MEASUREMENT, + ), + cv.Optional(CONF_PMC_5_0): sensor.sensor_schema( + unit_of_measurement=UNIT_MICROGRAMS_PER_CUBIC_METER, + icon=ICON_COUNTER, + accuracy_decimals=0, + state_class=STATE_CLASS_MEASUREMENT, + ), + cv.Optional(CONF_PMC_10_0): sensor.sensor_schema( + unit_of_measurement=UNIT_MICROGRAMS_PER_CUBIC_METER, + icon=ICON_COUNTER, + accuracy_decimals=0, + state_class=STATE_CLASS_MEASUREMENT, + ), + } + ) + .extend(uart.UART_DEVICE_SCHEMA) + .extend(cv.COMPONENT_SCHEMA) +) FINAL_VALIDATE_SCHEMA = uart.final_validate_device_schema( "gcja5", baud_rate=9600, require_rx=True, parity="EVEN" ) diff --git a/esphome/components/gpio_expander/cached_gpio.h b/esphome/components/gpio_expander/cached_gpio.h index eeff98cb6e..ddb9e63686 100644 --- a/esphome/components/gpio_expander/cached_gpio.h +++ b/esphome/components/gpio_expander/cached_gpio.h @@ -28,7 +28,10 @@ namespace esphome::gpio_expander { template 256), uint16_t, uint8_t>::type> class CachedGpioExpander { public: - /// @brief Read the state of the given pin. This will invalidate the cache for the given pin number. + /// @brief Read the state of the given pin. + /// By default, each read invalidates the pin's cache entry so the next read + /// of the same pin triggers a fresh hardware read. When invalidate_on_read + /// is disabled, the cache stays valid until explicitly cleared via reset_pin_cache_(). /// @param pin Pin number to read /// @return Pin state bool digital_read(P pin) { @@ -36,14 +39,17 @@ class CachedGpioExpander { const T pin_mask = (1 << (pin % BANK_SIZE)); // Check if specific pin cache is valid if (this->read_cache_valid_[bank] & pin_mask) { - // Invalidate pin - this->read_cache_valid_[bank] &= ~pin_mask; + if (this->invalidate_on_read_) { + // Invalidate pin so next read triggers hardware read + this->read_cache_valid_[bank] &= ~pin_mask; + } } else { // Read whole bank from hardware if (!this->digital_read_hw(pin)) return false; // Mark bank cache as valid except the pin that is being returned now - this->read_cache_valid_[bank] = std::numeric_limits::max() & ~pin_mask; + // (when not invalidating on read, mark all pins including this one as valid) + this->read_cache_valid_[bank] = std::numeric_limits::max() & ~(this->invalidate_on_read_ ? pin_mask : 0); } return this->digital_read_cache(pin); } @@ -71,12 +77,18 @@ class CachedGpioExpander { /// @brief Invalidate cache. This function should be called in component loop(). void reset_pin_cache_() { memset(this->read_cache_valid_, 0x00, CACHE_SIZE_BYTES); } + /// @brief Control whether digital_read() invalidates the pin's cache entry after reading. + /// When enabled (default), each read self-invalidates so the next read triggers a hardware read. + /// When disabled, cache stays valid until reset_pin_cache_() is explicitly called. + void set_invalidate_on_read_(bool invalidate) { this->invalidate_on_read_ = invalidate; } + static constexpr uint16_t BITS_PER_BYTE = 8; static constexpr uint16_t BANK_SIZE = sizeof(T) * BITS_PER_BYTE; static constexpr size_t BANKS = N / BANK_SIZE; static constexpr size_t CACHE_SIZE_BYTES = BANKS * sizeof(T); T read_cache_valid_[BANKS]{0}; + bool invalidate_on_read_{true}; }; } // namespace esphome::gpio_expander diff --git a/esphome/components/growatt_solar/sensor.py b/esphome/components/growatt_solar/sensor.py index 19f3adfd0e..7458b88b72 100644 --- a/esphome/components/growatt_solar/sensor.py +++ b/esphome/components/growatt_solar/sensor.py @@ -12,7 +12,9 @@ from esphome.const import ( CONF_VOLTAGE, DEVICE_CLASS_CURRENT, DEVICE_CLASS_ENERGY, + DEVICE_CLASS_FREQUENCY, DEVICE_CLASS_POWER, + DEVICE_CLASS_TEMPERATURE, DEVICE_CLASS_VOLTAGE, ICON_CURRENT_AC, STATE_CLASS_MEASUREMENT, @@ -53,6 +55,7 @@ PHASE_SENSORS = { unit_of_measurement=UNIT_VOLT, accuracy_decimals=1, device_class=DEVICE_CLASS_VOLTAGE, + state_class=STATE_CLASS_MEASUREMENT, ), CONF_CURRENT: sensor.sensor_schema( unit_of_measurement=UNIT_AMPERE, @@ -72,6 +75,7 @@ PV_SENSORS = { unit_of_measurement=UNIT_VOLT, accuracy_decimals=1, device_class=DEVICE_CLASS_VOLTAGE, + state_class=STATE_CLASS_MEASUREMENT, ), CONF_CURRENT: sensor.sensor_schema( unit_of_measurement=UNIT_AMPERE, @@ -118,6 +122,7 @@ CONFIG_SCHEMA = ( unit_of_measurement=UNIT_HERTZ, icon=ICON_CURRENT_AC, accuracy_decimals=2, + device_class=DEVICE_CLASS_FREQUENCY, state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional(CONF_ACTIVE_POWER): sensor.sensor_schema( @@ -147,6 +152,7 @@ CONFIG_SCHEMA = ( cv.Optional(CONF_INVERTER_MODULE_TEMP): sensor.sensor_schema( unit_of_measurement=UNIT_CELSIUS, accuracy_decimals=1, + device_class=DEVICE_CLASS_TEMPERATURE, state_class=STATE_CLASS_MEASUREMENT, ), } diff --git a/esphome/components/haier/climate.py b/esphome/components/haier/climate.py index caaaa18dd6..9c2c999f25 100644 --- a/esphome/components/haier/climate.py +++ b/esphome/components/haier/climate.py @@ -22,7 +22,6 @@ from esphome.const import ( CONF_SUPPORTED_SWING_MODES, CONF_TARGET_TEMPERATURE, CONF_TEMPERATURE_STEP, - CONF_TRIGGER_ID, CONF_VISUAL, CONF_WIFI, ) @@ -122,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: @@ -203,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) @@ -264,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({}), } ), }, @@ -530,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.h b/esphome/components/haier/haier_base.h index 87aa1d65ef..0c416623c0 100644 --- a/esphome/components/haier/haier_base.h +++ b/esphome/components/haier/haier_base.h @@ -177,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 1cee95bf16..1e9cb42f38 100644 --- a/esphome/components/haier/hon_climate.cpp +++ b/esphome/components/haier/hon_climate.cpp @@ -675,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; diff --git a/esphome/components/haier/hon_climate.h b/esphome/components/haier/hon_climate.h index 7c48a3748b..7a87f27b66 100644 --- a/esphome/components/haier/hon_climate.h +++ b/esphome/components/haier/hon_climate.h @@ -200,21 +200,5 @@ class HonClimate : public HaierClimateBase { 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/havells_solar/sensor.py b/esphome/components/havells_solar/sensor.py index 532315a1d1..f0683e1d9c 100644 --- a/esphome/components/havells_solar/sensor.py +++ b/esphome/components/havells_solar/sensor.py @@ -13,7 +13,9 @@ from esphome.const import ( CONF_VOLTAGE, DEVICE_CLASS_CURRENT, DEVICE_CLASS_ENERGY, + DEVICE_CLASS_FREQUENCY, DEVICE_CLASS_POWER, + DEVICE_CLASS_REACTIVE_POWER, DEVICE_CLASS_VOLTAGE, ICON_CURRENT_AC, STATE_CLASS_MEASUREMENT, @@ -64,6 +66,7 @@ PHASE_SENSORS = { unit_of_measurement=UNIT_VOLT, accuracy_decimals=2, device_class=DEVICE_CLASS_VOLTAGE, + state_class=STATE_CLASS_MEASUREMENT, ), CONF_CURRENT: sensor.sensor_schema( unit_of_measurement=UNIT_AMPERE, @@ -77,6 +80,7 @@ PV_SENSORS = { unit_of_measurement=UNIT_VOLT, accuracy_decimals=2, device_class=DEVICE_CLASS_VOLTAGE, + state_class=STATE_CLASS_MEASUREMENT, ), CONF_CURRENT: sensor.sensor_schema( unit_of_measurement=UNIT_AMPERE, @@ -123,6 +127,7 @@ CONFIG_SCHEMA = ( unit_of_measurement=UNIT_HERTZ, icon=ICON_CURRENT_AC, accuracy_decimals=2, + device_class=DEVICE_CLASS_FREQUENCY, state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional(CONF_ACTIVE_POWER): sensor.sensor_schema( @@ -134,6 +139,7 @@ CONFIG_SCHEMA = ( cv.Optional(CONF_REACTIVE_POWER): sensor.sensor_schema( unit_of_measurement=UNIT_VOLT_AMPS_REACTIVE, accuracy_decimals=2, + device_class=DEVICE_CLASS_REACTIVE_POWER, state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional(CONF_ENERGY_PRODUCTION_DAY): sensor.sensor_schema( @@ -171,6 +177,7 @@ CONFIG_SCHEMA = ( cv.Optional(CONF_INVERTER_BUS_VOLTAGE): sensor.sensor_schema( unit_of_measurement=UNIT_VOLT, accuracy_decimals=0, + device_class=DEVICE_CLASS_VOLTAGE, state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional(CONF_INSULATION_OF_PV_N_TO_GROUND): sensor.sensor_schema( @@ -181,21 +188,25 @@ CONFIG_SCHEMA = ( cv.Optional(CONF_GFCI_VALUE): sensor.sensor_schema( unit_of_measurement=UNIT_MILLIAMPERE, accuracy_decimals=0, + device_class=DEVICE_CLASS_CURRENT, state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional(CONF_DCI_OF_R): sensor.sensor_schema( unit_of_measurement=UNIT_MILLIAMPERE, accuracy_decimals=0, + device_class=DEVICE_CLASS_CURRENT, state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional(CONF_DCI_OF_S): sensor.sensor_schema( unit_of_measurement=UNIT_MILLIAMPERE, accuracy_decimals=0, + device_class=DEVICE_CLASS_CURRENT, state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional(CONF_DCI_OF_T): sensor.sensor_schema( unit_of_measurement=UNIT_MILLIAMPERE, accuracy_decimals=0, + device_class=DEVICE_CLASS_CURRENT, state_class=STATE_CLASS_MEASUREMENT, ), } 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/hlk_fm22x/__init__.py b/esphome/components/hlk_fm22x/__init__.py index cb6d5cdfd6..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( diff --git a/esphome/components/hlk_fm22x/hlk_fm22x.h b/esphome/components/hlk_fm22x/hlk_fm22x.h index d897d51881..fd8257b435 100644 --- a/esphome/components/hlk_fm22x/hlk_fm22x.h +++ b/esphome/components/hlk_fm22x/hlk_fm22x.h @@ -141,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 d0fd697d8f..22f292e47e 100644 --- a/esphome/components/hlw8012/hlw8012.cpp +++ b/esphome/components/hlw8012/hlw8012.cpp @@ -73,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/hlw8032/sensor.py b/esphome/components/hlw8032/sensor.py index 96800e46f4..846c9a398b 100644 --- a/esphome/components/hlw8032/sensor.py +++ b/esphome/components/hlw8032/sensor.py @@ -27,42 +27,46 @@ DEPENDENCIES = ["uart"] hlw8032_ns = cg.esphome_ns.namespace("hlw8032") HLW8032Component = hlw8032_ns.class_("HLW8032Component", cg.Component, uart.UARTDevice) -CONFIG_SCHEMA = cv.Schema( - { - cv.GenerateID(): cv.declare_id(HLW8032Component), - cv.Optional(CONF_VOLTAGE): sensor.sensor_schema( - unit_of_measurement=UNIT_VOLT, - accuracy_decimals=1, - device_class=DEVICE_CLASS_VOLTAGE, - state_class=STATE_CLASS_MEASUREMENT, - ), - cv.Optional(CONF_CURRENT): sensor.sensor_schema( - unit_of_measurement=UNIT_AMPERE, - accuracy_decimals=2, - device_class=DEVICE_CLASS_CURRENT, - state_class=STATE_CLASS_MEASUREMENT, - ), - cv.Optional(CONF_POWER): sensor.sensor_schema( - unit_of_measurement=UNIT_WATT, - accuracy_decimals=1, - device_class=DEVICE_CLASS_POWER, - state_class=STATE_CLASS_MEASUREMENT, - ), - cv.Optional(CONF_APPARENT_POWER): sensor.sensor_schema( - unit_of_measurement=UNIT_VOLT_AMPS, - accuracy_decimals=1, - device_class=DEVICE_CLASS_APPARENT_POWER, - state_class=STATE_CLASS_MEASUREMENT, - ), - cv.Optional(CONF_POWER_FACTOR): sensor.sensor_schema( - accuracy_decimals=2, - device_class=DEVICE_CLASS_POWER_FACTOR, - state_class=STATE_CLASS_MEASUREMENT, - ), - cv.Optional(CONF_CURRENT_RESISTOR, default=0.001): cv.resistance, - cv.Optional(CONF_VOLTAGE_DIVIDER, default=1.720): cv.positive_float, - } -).extend(uart.UART_DEVICE_SCHEMA) +CONFIG_SCHEMA = ( + cv.Schema( + { + cv.GenerateID(): cv.declare_id(HLW8032Component), + cv.Optional(CONF_VOLTAGE): sensor.sensor_schema( + unit_of_measurement=UNIT_VOLT, + accuracy_decimals=1, + device_class=DEVICE_CLASS_VOLTAGE, + state_class=STATE_CLASS_MEASUREMENT, + ), + cv.Optional(CONF_CURRENT): sensor.sensor_schema( + unit_of_measurement=UNIT_AMPERE, + accuracy_decimals=2, + device_class=DEVICE_CLASS_CURRENT, + state_class=STATE_CLASS_MEASUREMENT, + ), + cv.Optional(CONF_POWER): sensor.sensor_schema( + unit_of_measurement=UNIT_WATT, + accuracy_decimals=1, + device_class=DEVICE_CLASS_POWER, + state_class=STATE_CLASS_MEASUREMENT, + ), + cv.Optional(CONF_APPARENT_POWER): sensor.sensor_schema( + unit_of_measurement=UNIT_VOLT_AMPS, + accuracy_decimals=1, + device_class=DEVICE_CLASS_APPARENT_POWER, + state_class=STATE_CLASS_MEASUREMENT, + ), + cv.Optional(CONF_POWER_FACTOR): sensor.sensor_schema( + accuracy_decimals=2, + device_class=DEVICE_CLASS_POWER_FACTOR, + state_class=STATE_CLASS_MEASUREMENT, + ), + cv.Optional(CONF_CURRENT_RESISTOR, default=0.001): cv.resistance, + cv.Optional(CONF_VOLTAGE_DIVIDER, default=1.720): cv.positive_float, + } + ) + .extend(uart.UART_DEVICE_SCHEMA) + .extend(cv.COMPONENT_SCHEMA) +) FINAL_VALIDATE_SCHEMA = uart.final_validate_device_schema( "hlw8032", baud_rate=4800, require_rx=True, data_bits=8, parity="EVEN" diff --git a/esphome/components/hmc5883l/sensor.py b/esphome/components/hmc5883l/sensor.py index 96d0313008..cf3c594f36 100644 --- a/esphome/components/hmc5883l/sensor.py +++ b/esphome/components/hmc5883l/sensor.py @@ -87,6 +87,7 @@ heading_schema = sensor.sensor_schema( unit_of_measurement=UNIT_DEGREES, icon=ICON_SCREEN_ROTATION, accuracy_decimals=1, + state_class=STATE_CLASS_MEASUREMENT, ) CONFIG_SCHEMA = ( diff --git a/esphome/components/host/core.cpp b/esphome/components/host/core.cpp index a662e842ee..0ade4274fe 100644 --- a/esphome/components/host/core.cpp +++ b/esphome/components/host/core.cpp @@ -5,11 +5,17 @@ #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 HOT yield() { ::sched_yield(); } @@ -72,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/http_request/http_request.cpp b/esphome/components/http_request/http_request.cpp index 6590d2018e..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", diff --git a/esphome/components/http_request/http_request_idf.cpp b/esphome/components/http_request/http_request_idf.cpp index dda61e2400..30f53eecdc 100644 --- a/esphome/components/http_request/http_request_idf.cpp +++ b/esphome/components/http_request/http_request_idf.cpp @@ -17,6 +17,7 @@ namespace esphome::http_request { static const char *const TAG = "http_request.idf"; +static constexpr uint32_t ERROR_DURATION_MS = 1000; struct UserData { const std::vector &lower_case_collect_headers; @@ -57,7 +58,7 @@ std::shared_ptr HttpRequestIDF::perform(const std::string &url, c 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; } @@ -112,6 +113,11 @@ std::shared_ptr HttpRequestIDF::perform(const std::string &url, c config.event_handler = http_event_handler; 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); @@ -129,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; @@ -151,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; @@ -176,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; } @@ -189,7 +195,7 @@ 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; } @@ -214,7 +220,7 @@ 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; } diff --git a/esphome/components/htu21d/sensor.py b/esphome/components/htu21d/sensor.py index 92c088a22f..ed4fb5968a 100644 --- a/esphome/components/htu21d/sensor.py +++ b/esphome/components/htu21d/sensor.py @@ -118,6 +118,6 @@ async def set_heater_level_to_code(config, action_id, template_arg, args): 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/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/hydreon_rgxx/sensor.py b/esphome/components/hydreon_rgxx/sensor.py index f270b72e24..fdb606182f 100644 --- a/esphome/components/hydreon_rgxx/sensor.py +++ b/esphome/components/hydreon_rgxx/sensor.py @@ -9,6 +9,7 @@ from esphome.const import ( CONF_TEMPERATURE, DEVICE_CLASS_PRECIPITATION, DEVICE_CLASS_PRECIPITATION_INTENSITY, + DEVICE_CLASS_TEMPERATURE, ICON_THERMOMETER, STATE_CLASS_MEASUREMENT, STATE_CLASS_TOTAL_INCREASING, @@ -117,6 +118,7 @@ CONFIG_SCHEMA = cv.All( unit_of_measurement=UNIT_CELSIUS, accuracy_decimals=0, icon=ICON_THERMOMETER, + device_class=DEVICE_CLASS_TEMPERATURE, state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional(CONF_DISABLE_LED): cv.boolean, diff --git a/esphome/components/ili9xxx/__init__.py b/esphome/components/ili9xxx/__init__.py index e69de29bb2..84888bbabc 100644 --- a/esphome/components/ili9xxx/__init__.py +++ b/esphome/components/ili9xxx/__init__.py @@ -0,0 +1,4 @@ +DEPRECATED_COMPONENT = """ +The 'ili9xxx' component is deprecated and no new models will be added to it. +New model PRs should target the newer and more performant 'mipi_spi' component. +""" diff --git a/esphome/components/ili9xxx/display.py b/esphome/components/ili9xxx/display.py index bfb2300f4f..1f20b21a0e 100644 --- a/esphome/components/ili9xxx/display.py +++ b/esphome/components/ili9xxx/display.py @@ -210,8 +210,8 @@ def final_validate(config): ): LOGGER.info("Consider enabling PSRAM if available for the display buffer") - return spi.final_validate_device_schema( - "ili9xxx", require_miso=False, require_mosi=True + spi.final_validate_device_schema("ili9xxx", require_miso=False, require_mosi=True)( + config ) @@ -219,6 +219,9 @@ FINAL_VALIDATE_SCHEMA = final_validate async def to_code(config): + LOGGER.warning( + "The 'ili9xxx' component is deprecated, it is recommended to use 'mipi_spi' instead." + ) rhs = MODELS[config[CONF_MODEL]].new() var = cg.Pvariable(config[CONF_ID], rhs) diff --git a/esphome/components/image/__init__.py b/esphome/components/image/__init__.py index 6fb0e46d93..4a5fcc385e 100644 --- a/esphome/components/image/__init__.py +++ b/esphome/components/image/__init__.py @@ -12,7 +12,7 @@ from PIL import Image, UnidentifiedImageError from esphome import core, external_files import esphome.codegen as cg -from esphome.components.const import CONF_BYTE_ORDER +from esphome.components.const import CONF_BYTE_ORDER, KEY_METADATA import esphome.config_validation as cv from esphome.const import ( CONF_DEFAULTS, @@ -53,7 +53,6 @@ CONF_CHROMA_KEY = "chroma_key" CONF_ALPHA_CHANNEL = "alpha_channel" CONF_INVERT_ALPHA = "invert_alpha" CONF_IMAGES = "images" -KEY_METADATA = "metadata" TRANSPARENCY_TYPES = ( CONF_OPAQUE, diff --git a/esphome/components/infrared/infrared.cpp b/esphome/components/infrared/infrared.cpp index 658c9fd0df..9b97995a96 100644 --- a/esphome/components/infrared/infrared.cpp +++ b/esphome/components/infrared/infrared.cpp @@ -1,4 +1,7 @@ #include "infrared.h" + +#include + #include "esphome/core/log.h" #ifdef USE_API @@ -100,7 +103,7 @@ void Infrared::control(const InfraredCall &call) { // Zero-copy from packed protobuf data transmit_data->set_data_from_packed_sint32(call.get_packed_data(), call.get_packed_length(), call.get_packed_count()); - ESP_LOGD(TAG, "Transmitting packed raw timings: count=%u, repeat=%u", call.get_packed_count(), + ESP_LOGD(TAG, "Transmitting packed raw timings: count=%" PRIu16 ", repeat=%" PRIu32, call.get_packed_count(), call.get_repeat_count()); } else if (call.is_base64url()) { // Decode base64url (URL-safe) into transmit buffer @@ -113,16 +116,16 @@ void Infrared::control(const InfraredCall &call) { for (int32_t timing : transmit_data->get_data()) { int32_t abs_timing = timing < 0 ? -timing : timing; if (abs_timing > max_timing_us) { - ESP_LOGE(TAG, "Invalid timing value: %d µs (max %d)", timing, max_timing_us); + ESP_LOGE(TAG, "Invalid timing value: %" PRId32 " µs (max %" PRId32 ")", timing, max_timing_us); return; } } - ESP_LOGD(TAG, "Transmitting base64url raw timings: count=%zu, repeat=%u", transmit_data->get_data().size(), + ESP_LOGD(TAG, "Transmitting base64url raw timings: count=%zu, repeat=%" PRIu32, transmit_data->get_data().size(), call.get_repeat_count()); } else { // From vector (lambdas/automations) transmit_data->set_data(call.get_raw_timings()); - ESP_LOGD(TAG, "Transmitting raw timings: count=%zu, repeat=%u", call.get_raw_timings().size(), + ESP_LOGD(TAG, "Transmitting raw timings: count=%zu, repeat=%" PRIu32, call.get_raw_timings().size(), call.get_repeat_count()); } diff --git a/esphome/components/inkplate/inkplate.cpp b/esphome/components/inkplate/inkplate.cpp index 3b4b1a63d5..0511b451a8 100644 --- a/esphome/components/inkplate/inkplate.cpp +++ b/esphome/components/inkplate/inkplate.cpp @@ -3,6 +3,8 @@ #include "esphome/core/helpers.h" #include "esphome/core/log.h" +#include + #include namespace esphome { @@ -193,7 +195,7 @@ void Inkplate::dump_config() { ESP_LOGCONFIG(TAG, " Greyscale: %s\n" " Partial Updating: %s\n" - " Full Update Every: %d", + " Full Update Every: %" PRIu32, YESNO(this->greyscale_), YESNO(this->partial_updating_), this->full_update_every_); // Log pins LOG_PIN(" CKV Pin: ", this->ckv_pin_); @@ -306,7 +308,7 @@ void Inkplate::fill(Color color) { // If clipping is active, fall back to base implementation if (this->get_clipping().is_set()) { Display::fill(color); - ESP_LOGV(TAG, "Fill finished (%ums)", millis() - start_time); + ESP_LOGV(TAG, "Fill finished (%" PRIu32 "ms)", millis() - start_time); return; } @@ -329,12 +331,12 @@ void Inkplate::display() { this->display3b_(); } else { if (this->partial_updating_ && this->partial_update_()) { - ESP_LOGV(TAG, "Display finished (partial) (%ums)", millis() - start_time); + ESP_LOGV(TAG, "Display finished (partial) (%" PRIu32 "ms)", millis() - start_time); return; } this->display1b_(); } - ESP_LOGV(TAG, "Display finished (full) (%ums)", millis() - start_time); + ESP_LOGV(TAG, "Display finished (full) (%" PRIu32 "ms)", millis() - start_time); } void Inkplate::display1b_() { @@ -409,7 +411,7 @@ void Inkplate::display1b_() { uint32_t clock = (1UL << this->cl_pin_->get_pin()); uint32_t data_mask = this->get_data_pin_mask_(); - ESP_LOGV(TAG, "Display1b start loops (%ums)", millis() - start_time); + ESP_LOGV(TAG, "Display1b start loops (%" PRIu32 "ms)", millis() - start_time); for (uint8_t k = 0; k < rep; k++) { buffer_ptr = &this->buffer_[this->get_buffer_length_() - 1]; @@ -440,7 +442,7 @@ void Inkplate::display1b_() { } delayMicroseconds(230); } - ESP_LOGV(TAG, "Display1b first loop x %d (%ums)", 4, millis() - start_time); + ESP_LOGV(TAG, "Display1b first loop x %d (%" PRIu32 "ms)", 4, millis() - start_time); buffer_ptr = &this->buffer_[this->get_buffer_length_() - 1]; vscan_start_(); @@ -469,7 +471,7 @@ void Inkplate::display1b_() { vscan_end_(); } delayMicroseconds(230); - ESP_LOGV(TAG, "Display1b second loop (%ums)", millis() - start_time); + ESP_LOGV(TAG, "Display1b second loop (%" PRIu32 "ms)", millis() - start_time); if (this->model_ == INKPLATE_6_PLUS) { clean_fast_(2, 2); @@ -495,13 +497,13 @@ void Inkplate::display1b_() { vscan_end_(); } delayMicroseconds(230); - ESP_LOGV(TAG, "Display1b third loop (%ums)", millis() - start_time); + ESP_LOGV(TAG, "Display1b third loop (%" PRIu32 "ms)", millis() - start_time); } vscan_start_(); eink_off_(); this->block_partial_ = false; this->partial_updates_ = 0; - ESP_LOGV(TAG, "Display1b finished (%ums)", millis() - start_time); + ESP_LOGV(TAG, "Display1b finished (%" PRIu32 "ms)", millis() - start_time); } void Inkplate::display3b_() { @@ -614,7 +616,7 @@ void Inkplate::display3b_() { clean_fast_(3, 1); vscan_start_(); eink_off_(); - ESP_LOGV(TAG, "Display3b finished (%ums)", millis() - start_time); + ESP_LOGV(TAG, "Display3b finished (%" PRIu32 "ms)", millis() - start_time); } bool Inkplate::partial_update_() { @@ -641,7 +643,7 @@ bool Inkplate::partial_update_() { this->partial_buffer_2_[n--] = LUTW[diffw & 0x0F] & LUTB[diffb & 0x0F]; } } - ESP_LOGV(TAG, "Partial update buffer built after (%ums)", millis() - start_time); + ESP_LOGV(TAG, "Partial update buffer built after (%" PRIu32 "ms)", millis() - start_time); int rep = (this->model_ == INKPLATE_6_V2) ? 6 : 5; @@ -667,7 +669,7 @@ bool Inkplate::partial_update_() { vscan_end_(); } delayMicroseconds(230); - ESP_LOGV(TAG, "Partial update loop k=%d (%ums)", k, millis() - start_time); + ESP_LOGV(TAG, "Partial update loop k=%d (%" PRIu32 "ms)", k, millis() - start_time); } clean_fast_(2, 2); clean_fast_(3, 1); @@ -675,7 +677,7 @@ bool Inkplate::partial_update_() { eink_off_(); memcpy(this->buffer_, this->partial_buffer_, this->get_buffer_length_()); - ESP_LOGV(TAG, "Partial update finished (%ums)", millis() - start_time); + ESP_LOGV(TAG, "Partial update finished (%" PRIu32 "ms)", millis() - start_time); return true; } @@ -730,7 +732,7 @@ void Inkplate::clean() { clean_fast_(0, 8); // Black to Black clean_fast_(2, 1); // Black to White clean_fast_(1, 10); // White to White - ESP_LOGV(TAG, "Clean finished (%ums)", millis() - start_time); + ESP_LOGV(TAG, "Clean finished (%" PRIu32 "ms)", millis() - start_time); } void Inkplate::clean_fast_(uint8_t c, uint8_t rep) { @@ -773,9 +775,9 @@ void Inkplate::clean_fast_(uint8_t c, uint8_t rep) { vscan_end_(); } delayMicroseconds(230); - ESP_LOGV(TAG, "Clean fast rep loop %d finished (%ums)", k, millis() - start_time); + ESP_LOGV(TAG, "Clean fast rep loop %d finished (%" PRIu32 "ms)", k, millis() - start_time); } - ESP_LOGV(TAG, "Clean fast finished (%ums)", millis() - start_time); + ESP_LOGV(TAG, "Clean fast finished (%" PRIu32 "ms)", millis() - start_time); } void Inkplate::pins_z_state_() { diff --git a/esphome/components/internal_temperature/internal_temperature.h b/esphome/components/internal_temperature/internal_temperature.h index 78e3bcef7d..4810e8478d 100644 --- a/esphome/components/internal_temperature/internal_temperature.h +++ b/esphome/components/internal_temperature/internal_temperature.h @@ -1,18 +1,18 @@ #pragma once -#include "esphome/core/component.h" #include "esphome/components/sensor/sensor.h" +#include "esphome/core/component.h" -namespace esphome { -namespace internal_temperature { +namespace esphome::internal_temperature { class InternalTemperatureSensor : public sensor::Sensor, public PollingComponent { public: +#if defined(USE_ESP32) || (defined(USE_ZEPHYR) && defined(USE_NRF52)) void setup() override; +#endif // USE_ESP32 || (USE_ZEPHYR && USE_NRF52) void dump_config() override; void update() override; }; -} // namespace internal_temperature -} // namespace esphome +} // namespace esphome::internal_temperature diff --git a/esphome/components/internal_temperature/internal_temperature_bk72xx.cpp b/esphome/components/internal_temperature/internal_temperature_bk72xx.cpp new file mode 100644 index 0000000000..31a92f90a5 --- /dev/null +++ b/esphome/components/internal_temperature/internal_temperature_bk72xx.cpp @@ -0,0 +1,41 @@ +#ifdef USE_BK72XX + +#include "esphome/core/log.h" +#include "internal_temperature.h" + +extern "C" { +uint32_t temp_single_get_current_temperature(uint32_t *temp_value); +} + +namespace esphome::internal_temperature { + +static const char *const TAG = "internal_temperature.bk72xx"; + +void InternalTemperatureSensor::update() { + float temperature = NAN; + bool success = false; + + uint32_t raw, result; + result = temp_single_get_current_temperature(&raw); + success = (result == 0); +#if defined(USE_LIBRETINY_VARIANT_BK7231N) + temperature = raw * -0.38f + 156.0f; +#elif defined(USE_LIBRETINY_VARIANT_BK7231T) + temperature = raw * 0.04f; +#else // USE_LIBRETINY_VARIANT + temperature = raw * 0.128f; +#endif // USE_LIBRETINY_VARIANT + + if (success && std::isfinite(temperature)) { + this->publish_state(temperature); + } else { + ESP_LOGD(TAG, "Ignoring invalid temperature (success=%d, value=%.1f)", success, temperature); + if (!this->has_state()) { + this->publish_state(NAN); + } + } +} + +} // namespace esphome::internal_temperature + +#endif // USE_BK72XX diff --git a/esphome/components/internal_temperature/internal_temperature_common.cpp b/esphome/components/internal_temperature/internal_temperature_common.cpp new file mode 100644 index 0000000000..89a7d34333 --- /dev/null +++ b/esphome/components/internal_temperature/internal_temperature_common.cpp @@ -0,0 +1,10 @@ +#include "esphome/core/log.h" +#include "internal_temperature.h" + +namespace esphome::internal_temperature { + +static const char *const TAG = "internal_temperature"; + +void InternalTemperatureSensor::dump_config() { LOG_SENSOR("", "Internal Temperature Sensor", this); } + +} // namespace esphome::internal_temperature diff --git a/esphome/components/internal_temperature/internal_temperature.cpp b/esphome/components/internal_temperature/internal_temperature_esp32.cpp similarity index 74% rename from esphome/components/internal_temperature/internal_temperature.cpp rename to esphome/components/internal_temperature/internal_temperature_esp32.cpp index 34d7baf880..09121fa9c9 100644 --- a/esphome/components/internal_temperature/internal_temperature.cpp +++ b/esphome/components/internal_temperature/internal_temperature_esp32.cpp @@ -1,7 +1,8 @@ -#include "internal_temperature.h" -#include "esphome/core/log.h" - #ifdef USE_ESP32 + +#include "esphome/core/log.h" +#include "internal_temperature.h" + #if defined(USE_ESP32_VARIANT_ESP32) // there is no official API available on the original ESP32 extern "C" { @@ -13,32 +14,20 @@ uint8_t temprature_sens_read(); defined(USE_ESP32_VARIANT_ESP32S3) #include "driver/temperature_sensor.h" #endif // USE_ESP32_VARIANT -#endif // USE_ESP32 -#ifdef USE_RP2040 -#include "Arduino.h" -#endif // USE_RP2040 -#ifdef USE_BK72XX -extern "C" { -uint32_t temp_single_get_current_temperature(uint32_t *temp_value); -} -#endif // USE_BK72XX -namespace esphome { -namespace internal_temperature { +namespace esphome::internal_temperature { + +static const char *const TAG = "internal_temperature.esp32"; -static const char *const TAG = "internal_temperature"; -#ifdef USE_ESP32 #if defined(USE_ESP32_VARIANT_ESP32C2) || defined(USE_ESP32_VARIANT_ESP32C3) || defined(USE_ESP32_VARIANT_ESP32C5) || \ defined(USE_ESP32_VARIANT_ESP32C6) || defined(USE_ESP32_VARIANT_ESP32C61) || defined(USE_ESP32_VARIANT_ESP32H2) || \ defined(USE_ESP32_VARIANT_ESP32P4) || defined(USE_ESP32_VARIANT_ESP32S2) || defined(USE_ESP32_VARIANT_ESP32S3) static temperature_sensor_handle_t tsensNew = NULL; #endif // USE_ESP32_VARIANT -#endif // USE_ESP32 void InternalTemperatureSensor::update() { float temperature = NAN; bool success = false; -#ifdef USE_ESP32 #if defined(USE_ESP32_VARIANT_ESP32) uint8_t raw = temprature_sens_read(); ESP_LOGV(TAG, "Raw temperature value: %d", raw); @@ -54,23 +43,7 @@ void InternalTemperatureSensor::update() { ESP_LOGE(TAG, "Reading failed (%d)", result); } #endif // USE_ESP32_VARIANT -#endif // USE_ESP32 -#ifdef USE_RP2040 - temperature = analogReadTemp(); - success = (temperature != 0.0f); -#endif // USE_RP2040 -#ifdef USE_BK72XX - uint32_t raw, result; - result = temp_single_get_current_temperature(&raw); - success = (result == 0); -#if defined(USE_LIBRETINY_VARIANT_BK7231N) - temperature = raw * -0.38f + 156.0f; -#elif defined(USE_LIBRETINY_VARIANT_BK7231T) - temperature = raw * 0.04f; -#else // USE_LIBRETINY_VARIANT - temperature = raw * 0.128f; -#endif // USE_LIBRETINY_VARIANT -#endif // USE_BK72XX + if (success && std::isfinite(temperature)) { this->publish_state(temperature); } else { @@ -82,7 +55,6 @@ void InternalTemperatureSensor::update() { } void InternalTemperatureSensor::setup() { -#ifdef USE_ESP32 #if defined(USE_ESP32_VARIANT_ESP32C2) || defined(USE_ESP32_VARIANT_ESP32C3) || defined(USE_ESP32_VARIANT_ESP32C5) || \ defined(USE_ESP32_VARIANT_ESP32C6) || defined(USE_ESP32_VARIANT_ESP32C61) || defined(USE_ESP32_VARIANT_ESP32H2) || \ defined(USE_ESP32_VARIANT_ESP32P4) || defined(USE_ESP32_VARIANT_ESP32S2) || defined(USE_ESP32_VARIANT_ESP32S3) @@ -102,10 +74,8 @@ void InternalTemperatureSensor::setup() { return; } #endif // USE_ESP32_VARIANT -#endif // USE_ESP32 } -void InternalTemperatureSensor::dump_config() { LOG_SENSOR("", "Internal Temperature Sensor", this); } +} // namespace esphome::internal_temperature -} // namespace internal_temperature -} // namespace esphome +#endif // USE_ESP32 diff --git a/esphome/components/internal_temperature/internal_temperature_ln882x.cpp b/esphome/components/internal_temperature/internal_temperature_ln882x.cpp new file mode 100644 index 0000000000..621fbed030 --- /dev/null +++ b/esphome/components/internal_temperature/internal_temperature_ln882x.cpp @@ -0,0 +1,24 @@ +#ifdef USE_LN882X + +#include "internal_temperature.h" + +extern "C" { +uint16_t hal_adc_get_data(uint32_t adc_base, uint32_t ch); +} + +namespace esphome::internal_temperature { + +void InternalTemperatureSensor::update() { + static constexpr uint32_t ADC_BASE = 0x40000800U; + static constexpr uint32_t ADC_CH0 = 1U; + static constexpr uint16_t ADC_MASK = 0xFFF; + static constexpr float ADC_TEMP_SCALE = 2.54f; + static constexpr float ADC_TEMP_OFFSET = 278.15f; + uint16_t raw = hal_adc_get_data(ADC_BASE, ADC_CH0); + float temperature = (raw & ADC_MASK) / ADC_TEMP_SCALE - ADC_TEMP_OFFSET; + this->publish_state(temperature); +} + +} // namespace esphome::internal_temperature + +#endif // USE_LN882X diff --git a/esphome/components/internal_temperature/internal_temperature_rp2040.cpp b/esphome/components/internal_temperature/internal_temperature_rp2040.cpp new file mode 100644 index 0000000000..66dee9faf7 --- /dev/null +++ b/esphome/components/internal_temperature/internal_temperature_rp2040.cpp @@ -0,0 +1,31 @@ +#ifdef USE_RP2040 + +#include "esphome/core/log.h" +#include "internal_temperature.h" + +#include "Arduino.h" + +namespace esphome::internal_temperature { + +static const char *const TAG = "internal_temperature.rp2040"; + +void InternalTemperatureSensor::update() { + float temperature = NAN; + bool success = false; + + temperature = analogReadTemp(); + success = (temperature != 0.0f); + + if (success && std::isfinite(temperature)) { + this->publish_state(temperature); + } else { + ESP_LOGD(TAG, "Ignoring invalid temperature (success=%d, value=%.1f)", success, temperature); + if (!this->has_state()) { + this->publish_state(NAN); + } + } +} + +} // namespace esphome::internal_temperature + +#endif // USE_RP2040 diff --git a/esphome/components/internal_temperature/internal_temperature_zephyr.cpp b/esphome/components/internal_temperature/internal_temperature_zephyr.cpp new file mode 100644 index 0000000000..be72ab6f51 --- /dev/null +++ b/esphome/components/internal_temperature/internal_temperature_zephyr.cpp @@ -0,0 +1,56 @@ +#if defined(USE_ZEPHYR) && defined(USE_NRF52) + +#include "esphome/core/log.h" +#include "internal_temperature.h" + +#include +#include + +namespace esphome::internal_temperature { + +static const char *const TAG = "internal_temperature.zephyr"; + +static const struct device *const DIE_TEMPERATURE_SENSOR = DEVICE_DT_GET_ONE(nordic_nrf_temp); + +void InternalTemperatureSensor::update() { + struct sensor_value value; + int result = sensor_sample_fetch(DIE_TEMPERATURE_SENSOR); + if (result != 0) { + ESP_LOGE(TAG, "Failed to fetch nRF52 die temperature sample (%d)", result); + if (!this->has_state()) { + this->publish_state(NAN); + } + return; + } + + result = sensor_channel_get(DIE_TEMPERATURE_SENSOR, SENSOR_CHAN_DIE_TEMP, &value); + if (result != 0) { + ESP_LOGE(TAG, "Failed to get nRF52 die temperature (%d)", result); + if (!this->has_state()) { + this->publish_state(NAN); + } + return; + } + + const float temperature = value.val1 + (value.val2 / 1000000.0f); + if (std::isfinite(temperature)) { + this->publish_state(temperature); + } else { + ESP_LOGD(TAG, "Ignoring invalid nRF52 temperature (value=%.1f)", temperature); + if (!this->has_state()) { + this->publish_state(NAN); + } + } +} + +void InternalTemperatureSensor::setup() { + if (!device_is_ready(DIE_TEMPERATURE_SENSOR)) { + ESP_LOGE(TAG, "nRF52 die temperature sensor device %s not ready", DIE_TEMPERATURE_SENSOR->name); + this->mark_failed(); + return; + } +} + +} // namespace esphome::internal_temperature + +#endif // USE_ZEPHYR && USE_NRF52 diff --git a/esphome/components/internal_temperature/sensor.py b/esphome/components/internal_temperature/sensor.py index 93b98a30f4..02730b6862 100644 --- a/esphome/components/internal_temperature/sensor.py +++ b/esphome/components/internal_temperature/sensor.py @@ -1,15 +1,21 @@ import esphome.codegen as cg from esphome.components import sensor +from esphome.components.zephyr import zephyr_add_prj_conf +from esphome.config_helpers import filter_source_files_from_platform import esphome.config_validation as cv from esphome.const import ( DEVICE_CLASS_TEMPERATURE, ENTITY_CATEGORY_DIAGNOSTIC, PLATFORM_BK72XX, PLATFORM_ESP32, + PLATFORM_LN882X, + PLATFORM_NRF52, PLATFORM_RP2040, STATE_CLASS_MEASUREMENT, UNIT_CELSIUS, + PlatformFramework, ) +from esphome.core import CORE internal_temperature_ns = cg.esphome_ns.namespace("internal_temperature") InternalTemperatureSensor = internal_temperature_ns.class_( @@ -25,10 +31,40 @@ CONFIG_SCHEMA = cv.All( state_class=STATE_CLASS_MEASUREMENT, entity_category=ENTITY_CATEGORY_DIAGNOSTIC, ).extend(cv.polling_component_schema("60s")), - cv.only_on([PLATFORM_ESP32, PLATFORM_RP2040, PLATFORM_BK72XX]), + cv.only_on( + [ + PLATFORM_ESP32, + PLATFORM_RP2040, + PLATFORM_BK72XX, + PLATFORM_NRF52, + PLATFORM_LN882X, + ] + ), ) async def to_code(config): var = await sensor.new_sensor(config) await cg.register_component(var, config) + + if CORE.using_zephyr and CORE.is_nrf52: + zephyr_add_prj_conf("SENSOR", True) + zephyr_add_prj_conf("TEMP_NRF5", True) + + +FILTER_SOURCE_FILES = filter_source_files_from_platform( + { + "internal_temperature_esp32.cpp": { + PlatformFramework.ESP32_ARDUINO, + PlatformFramework.ESP32_IDF, + }, + "internal_temperature_rp2040.cpp": {PlatformFramework.RP2040_ARDUINO}, + "internal_temperature_bk72xx.cpp": { + PlatformFramework.BK72XX_ARDUINO, + }, + "internal_temperature_ln882x.cpp": { + PlatformFramework.LN882X_ARDUINO, + }, + "internal_temperature_zephyr.cpp": {PlatformFramework.NRF52_ZEPHYR}, + } +) diff --git a/esphome/components/kamstrup_kmp/sensor.py b/esphome/components/kamstrup_kmp/sensor.py index fb37ac2c8d..134ac245bf 100644 --- a/esphome/components/kamstrup_kmp/sensor.py +++ b/esphome/components/kamstrup_kmp/sensor.py @@ -13,6 +13,7 @@ from esphome.const import ( DEVICE_CLASS_POWER, DEVICE_CLASS_TEMPERATURE, DEVICE_CLASS_VOLUME, + DEVICE_CLASS_VOLUME_FLOW_RATE, STATE_CLASS_MEASUREMENT, STATE_CLASS_TOTAL_INCREASING, UNIT_CELSIUS, @@ -75,7 +76,7 @@ CONFIG_SCHEMA = ( ), cv.Optional(CONF_FLOW): sensor.sensor_schema( accuracy_decimals=1, - device_class=DEVICE_CLASS_VOLUME, + device_class=DEVICE_CLASS_VOLUME_FLOW_RATE, state_class=STATE_CLASS_MEASUREMENT, unit_of_measurement=UNIT_LITRE_PER_HOUR, ), diff --git a/esphome/components/lc709203f/sensor.py b/esphome/components/lc709203f/sensor.py index eb08a522e5..75ae703638 100644 --- a/esphome/components/lc709203f/sensor.py +++ b/esphome/components/lc709203f/sensor.py @@ -36,7 +36,7 @@ CONFIG_SCHEMA = ( cv.Schema( { cv.GenerateID(): cv.declare_id(lc709203f), - cv.Optional(CONF_SIZE, default="500"): cv.int_range(100, 3000), + cv.Optional(CONF_SIZE, default=500): cv.int_range(100, 3000), cv.Optional(CONF_VOLTAGE, default="3.7"): cv.enum( BATTERY_VOLTAGE_OPTIONS, upper=True ), diff --git a/esphome/components/ld2420/sensor/__init__.py b/esphome/components/ld2420/sensor/__init__.py index 6dde35753a..97acdabd7b 100644 --- a/esphome/components/ld2420/sensor/__init__.py +++ b/esphome/components/ld2420/sensor/__init__.py @@ -5,6 +5,7 @@ from esphome.const import ( CONF_ID, CONF_MOVING_DISTANCE, DEVICE_CLASS_DISTANCE, + STATE_CLASS_MEASUREMENT, UNIT_CENTIMETER, ) @@ -20,7 +21,9 @@ CONFIG_SCHEMA = cv.All( cv.GenerateID(): cv.declare_id(LD2420Sensor), cv.GenerateID(CONF_LD2420_ID): cv.use_id(LD2420Component), cv.Optional(CONF_MOVING_DISTANCE): sensor.sensor_schema( - device_class=DEVICE_CLASS_DISTANCE, unit_of_measurement=UNIT_CENTIMETER + device_class=DEVICE_CLASS_DISTANCE, + unit_of_measurement=UNIT_CENTIMETER, + state_class=STATE_CLASS_MEASUREMENT, ), } ), diff --git a/esphome/components/ld2450/__init__.py b/esphome/components/ld2450/__init__.py index 5854a5794c..37bf12bafc 100644 --- a/esphome/components/ld2450/__init__.py +++ b/esphome/components/ld2450/__init__.py @@ -2,7 +2,7 @@ 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_ID, CONF_ON_DATA, CONF_THROTTLE, CONF_TRIGGER_ID +from esphome.const import CONF_ID, CONF_ON_DATA, CONF_THROTTLE AUTO_LOAD = ["ld24xx"] DEPENDENCIES = ["uart"] @@ -12,7 +12,6 @@ MULTI_CONF = True ld2450_ns = cg.esphome_ns.namespace("ld2450") LD2450Component = ld2450_ns.class_("LD2450Component", cg.Component, uart.UARTDevice) -LD2450DataTrigger = ld2450_ns.class_("LD2450DataTrigger", automation.Trigger.template()) CONF_LD2450_ID = "ld2450_id" @@ -23,11 +22,7 @@ CONFIG_SCHEMA = cv.All( cv.Optional(CONF_THROTTLE): cv.invalid( f"{CONF_THROTTLE} has been removed; use per-sensor filters, instead" ), - cv.Optional(CONF_ON_DATA): automation.validate_automation( - { - cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(LD2450DataTrigger), - } - ), + cv.Optional(CONF_ON_DATA): automation.validate_automation({}), } ) .extend(uart.UART_DEVICE_SCHEMA) @@ -54,5 +49,6 @@ async def to_code(config): await cg.register_component(var, config) await uart.register_uart_device(var, config) for conf in config.get(CONF_ON_DATA, []): - trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) - await automation.build_automation(trigger, [], conf) + await automation.build_callback_automation( + var, "add_on_data_callback", [], conf + ) diff --git a/esphome/components/ld2450/ld2450.cpp b/esphome/components/ld2450/ld2450.cpp index 6230a8c30b..58c3cac42d 100644 --- a/esphome/components/ld2450/ld2450.cpp +++ b/esphome/components/ld2450/ld2450.cpp @@ -10,6 +10,7 @@ #include "esphome/core/component.h" #include "esphome/core/helpers.h" +#include #include #include @@ -575,7 +576,7 @@ void LD2450Component::handle_periodic_data_() { if (this->get_timeout_status_(this->presence_millis_)) { this->target_binary_sensor_->publish_state(false); } else { - ESP_LOGV(TAG, "Clear presence waiting timeout: %d", this->timeout_); + ESP_LOGV(TAG, "Clear presence waiting timeout: %" PRIu32, this->timeout_); } } } diff --git a/esphome/components/ld2450/ld2450.h b/esphome/components/ld2450/ld2450.h index e774dd9c75..cbcdec10b3 100644 --- a/esphome/components/ld2450/ld2450.h +++ b/esphome/components/ld2450/ld2450.h @@ -1,6 +1,5 @@ #pragma once -#include "esphome/core/automation.h" #include "esphome/core/defines.h" #include "esphome/core/component.h" #ifdef USE_SENSOR @@ -201,11 +200,4 @@ class LD2450Component : public Component, public uart::UARTDevice { LazyCallbackManager data_callback_; }; -class LD2450DataTrigger : public Trigger<> { - public: - explicit LD2450DataTrigger(LD2450Component *parent) { - parent->add_on_data_callback([this]() { this->trigger(); }); - } -}; - } // namespace esphome::ld2450 diff --git a/esphome/components/libretiny/preferences.cpp b/esphome/components/libretiny/preferences.cpp index 344ca4a8b3..fba6717294 100644 --- a/esphome/components/libretiny/preferences.cpp +++ b/esphome/components/libretiny/preferences.cpp @@ -108,16 +108,21 @@ bool LibreTinyPreferences::sync() { } written++; } else { - ESP_LOGD(TAG, "FDB data not changed; skipping %" PRIu32 " len=%zu", save.key, save.data.size()); + ESP_LOGV(TAG, "FDB data not changed; skipping %" PRIu32 " len=%zu", save.key, save.data.size()); cached++; } } s_pending_save.clear(); - 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=%d for key=%" PRIu32, failed, last_err, last_key); + ESP_LOGE(TAG, "Writing %d items: %d cached, %d written, %d failed. Last error=%d for key=%" PRIu32, + cached + written + failed, cached, written, failed, 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); } return failed == 0; diff --git a/esphome/components/light/light_call.cpp b/esphome/components/light/light_call.cpp index 7c936b51b7..a749cd7305 100644 --- a/esphome/components/light/light_call.cpp +++ b/esphome/components/light/light_call.cpp @@ -198,13 +198,13 @@ LightColorValues LightCall::validate_() { // Ensure there is always a color mode set if (!this->has_color_mode()) { - this->color_mode_ = this->compute_color_mode_(); + this->color_mode_ = this->compute_color_mode_(traits); this->set_flag_(FLAG_HAS_COLOR_MODE); } auto color_mode = this->color_mode_; // Transform calls that use non-native parameters for the current mode. - this->transform_parameters_(); + this->transform_parameters_(traits); // Business logic adjustments before validation // Flag whether an explicit turn off was requested, in which case we'll also stop the effect. @@ -366,9 +366,7 @@ LightColorValues LightCall::validate_() { return v; } -void LightCall::transform_parameters_() { - auto traits = this->parent_->get_traits(); - +void LightCall::transform_parameters_(const LightTraits &traits) { // Allow CWWW modes to be set with a white value and/or color temperature. // This is used in three cases in HA: // - CW/WW lights, which set the "brightness" and "color_temperature" @@ -407,8 +405,8 @@ void LightCall::transform_parameters_() { } } } -ColorMode LightCall::compute_color_mode_() { - auto supported_modes = this->parent_->get_traits().get_supported_color_modes(); +ColorMode LightCall::compute_color_mode_(const LightTraits &traits) { + auto supported_modes = traits.get_supported_color_modes(); int supported_count = supported_modes.size(); // Some lights don't support any color modes (e.g. monochromatic light), leave it at unknown. diff --git a/esphome/components/light/light_call.h b/esphome/components/light/light_call.h index 0eb1785239..88d29bd349 100644 --- a/esphome/components/light/light_call.h +++ b/esphome/components/light/light_call.h @@ -10,6 +10,7 @@ struct LogString; namespace light { class LightState; +class LightTraits; /** This class represents a requested change in a light state. * @@ -188,11 +189,11 @@ class LightCall { LightColorValues validate_(); //// Compute the color mode that should be used for this call. - ColorMode compute_color_mode_(); + ColorMode compute_color_mode_(const LightTraits &traits); /// Get potential color modes bitmask for this light call. color_mode_bitmask_t get_suitable_color_modes_mask_(); /// Some color modes also can be set using non-native parameters, transform those calls. - void transform_parameters_(); + void transform_parameters_(const LightTraits &traits); // Bitfield flags - each flag indicates whether a corresponding value has been set. enum FieldFlags : uint16_t { diff --git a/esphome/components/light/light_color_values.h b/esphome/components/light/light_color_values.h index a2c2dbca46..fa286a3941 100644 --- a/esphome/components/light/light_color_values.h +++ b/esphome/components/light/light_color_values.h @@ -104,9 +104,10 @@ class LightColorValues { this->green_ = 1.0f; this->blue_ = 1.0f; } else { - this->red_ /= max_value; - this->green_ /= max_value; - this->blue_ /= max_value; + float inv = 1.0f / max_value; + this->red_ *= inv; + this->green_ *= inv; + this->blue_ *= inv; } } } diff --git a/esphome/components/lock/__init__.py b/esphome/components/lock/__init__.py index fe4db23ae3..0df4b20cba 100644 --- a/esphome/components/lock/__init__.py +++ b/esphome/components/lock/__init__.py @@ -10,7 +10,6 @@ from esphome.const import ( CONF_MQTT_ID, CONF_ON_LOCK, CONF_ON_UNLOCK, - CONF_TRIGGER_ID, CONF_WEB_SERVER, ) from esphome.core import CORE, CoroPriority, coroutine_with_priority @@ -31,8 +30,7 @@ OpenAction = lock_ns.class_("OpenAction", automation.Action) LockPublishAction = lock_ns.class_("LockPublishAction", automation.Action) LockCondition = lock_ns.class_("LockCondition", Condition) -LockLockTrigger = lock_ns.class_("LockLockTrigger", automation.Trigger.template()) -LockUnlockTrigger = lock_ns.class_("LockUnlockTrigger", automation.Trigger.template()) +LockStateForwarder = lock_ns.class_("LockStateForwarder") LockState = lock_ns.enum("LockState") @@ -52,16 +50,8 @@ _LOCK_SCHEMA = ( .extend( { cv.OnlyWith(CONF_MQTT_ID, "mqtt"): cv.declare_id(mqtt.MQTTLockComponent), - cv.Optional(CONF_ON_LOCK): automation.validate_automation( - { - cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(LockLockTrigger), - } - ), - cv.Optional(CONF_ON_UNLOCK): automation.validate_automation( - { - cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(LockUnlockTrigger), - } - ), + cv.Optional(CONF_ON_LOCK): automation.validate_automation({}), + cv.Optional(CONF_ON_UNLOCK): automation.validate_automation({}), } ) ) @@ -93,12 +83,18 @@ def lock_schema( @setup_entity("lock") async def _setup_lock_core(var, config): - for conf in config.get(CONF_ON_LOCK, []): - trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) - await automation.build_automation(trigger, [], conf) - for conf in config.get(CONF_ON_UNLOCK, []): - trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) - await automation.build_automation(trigger, [], conf) + for conf_key, state_enum in ( + (CONF_ON_LOCK, LockState.LOCK_STATE_LOCKED), + (CONF_ON_UNLOCK, LockState.LOCK_STATE_UNLOCKED), + ): + for conf in config.get(conf_key, []): + await automation.build_callback_automation( + var, + "add_on_state_callback", + [], + conf, + forwarder=LockStateForwarder.template(state_enum), + ) if mqtt_id := config.get(CONF_MQTT_ID): mqtt_ = cg.new_Pvariable(mqtt_id, var) diff --git a/esphome/components/lock/automation.h b/esphome/components/lock/automation.h index 6f3c422693..c140bc568f 100644 --- a/esphome/components/lock/automation.h +++ b/esphome/components/lock/automation.h @@ -49,21 +49,17 @@ template class LockCondition : public Condition { bool state_; }; -template class LockStateTrigger : public Trigger<> { - public: - explicit LockStateTrigger(Lock *a_lock) : lock_(a_lock) { - a_lock->add_on_state_callback([this]() { - if (this->lock_->state == State) { - this->trigger(); - } - }); +/// Callback forwarder that triggers an Automation<> only when a specific lock state is entered. +/// Pointer-sized (single Automation* field) to fit inline in Callback::ctx_. +template struct LockStateForwarder { + Automation<> *automation; + void operator()(LockState state) const { + if (state == State) + this->automation->trigger(); } - - protected: - Lock *lock_; }; -using LockLockTrigger = LockStateTrigger; -using LockUnlockTrigger = LockStateTrigger; +static_assert(sizeof(LockStateForwarder) <= sizeof(void *)); +static_assert(std::is_trivially_copyable_v>); } // namespace esphome::lock diff --git a/esphome/components/lock/lock.cpp b/esphome/components/lock/lock.cpp index 90937485b9..3ff131af3d 100644 --- a/esphome/components/lock/lock.cpp +++ b/esphome/components/lock/lock.cpp @@ -42,7 +42,7 @@ void Lock::publish_state(LockState state) { this->state = state; this->rtc_.save(&this->state); ESP_LOGV(TAG, "'%s' >> %s", this->name_.c_str(), LOG_STR_ARG(lock_state_to_string(state))); - this->state_callback_.call(); + this->state_callback_.call(state); #if defined(USE_LOCK) && defined(USE_CONTROLLER_REGISTRY) ControllerRegistry::notify_lock_update(this); #endif diff --git a/esphome/components/lock/lock.h b/esphome/components/lock/lock.h index 707431d543..543a4b51a8 100644 --- a/esphome/components/lock/lock.h +++ b/esphome/components/lock/lock.h @@ -148,7 +148,7 @@ class Lock : public EntityBase { /** Set callback for state changes. * - * @param callback The void(bool) callback. + * @param callback The void(LockState) callback. */ template void add_on_state_callback(F &&callback) { this->state_callback_.add(std::forward(callback)); @@ -178,7 +178,7 @@ class Lock : public EntityBase { */ virtual void control(const LockCall &call) = 0; - LazyCallbackManager state_callback_{}; + LazyCallbackManager state_callback_{}; Deduplicator publish_dedup_; ESPPreferenceObject rtc_; }; diff --git a/esphome/components/logger/logger_esp8266.cpp b/esphome/components/logger/logger_esp8266.cpp index b9507e707a..5797b03ba7 100644 --- a/esphome/components/logger/logger_esp8266.cpp +++ b/esphome/components/logger/logger_esp8266.cpp @@ -1,5 +1,9 @@ #ifdef USE_ESP8266 #include "logger.h" +#include "esphome/core/defines.h" +#ifdef USE_ESP8266_CRASH_HANDLER +#include "esphome/components/esp8266/crash_handler.h" +#endif #include "esphome/core/log.h" namespace esphome::logger { @@ -26,6 +30,9 @@ void Logger::pre_setup() { global_logger = this; ESP_LOGI(TAG, "Log initialized"); +#ifdef USE_ESP8266_CRASH_HANDLER + esp8266::crash_handler_log(); +#endif } const LogString *Logger::get_uart_selection_() { diff --git a/esphome/components/ltr390/sensor.py b/esphome/components/ltr390/sensor.py index 579adb9051..37fceaf984 100644 --- a/esphome/components/ltr390/sensor.py +++ b/esphome/components/ltr390/sensor.py @@ -10,6 +10,7 @@ from esphome.const import ( DEVICE_CLASS_EMPTY, DEVICE_CLASS_ILLUMINANCE, ICON_BRIGHTNESS_5, + STATE_CLASS_MEASUREMENT, UNIT_LUX, ) @@ -57,24 +58,28 @@ CONFIG_SCHEMA = cv.All( icon=ICON_BRIGHTNESS_5, accuracy_decimals=1, device_class=DEVICE_CLASS_ILLUMINANCE, + state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional(CONF_AMBIENT_LIGHT): sensor.sensor_schema( unit_of_measurement=UNIT_COUNTS, icon=ICON_BRIGHTNESS_5, accuracy_decimals=1, device_class=DEVICE_CLASS_EMPTY, + state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional(CONF_UV_INDEX): sensor.sensor_schema( unit_of_measurement=UNIT_UVI, icon=ICON_BRIGHTNESS_5, accuracy_decimals=5, device_class=DEVICE_CLASS_EMPTY, + state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional(CONF_UV): sensor.sensor_schema( unit_of_measurement=UNIT_COUNTS, icon=ICON_BRIGHTNESS_5, accuracy_decimals=1, device_class=DEVICE_CLASS_EMPTY, + state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional(CONF_GAIN, default="X18"): cv.Any( cv.enum(GAIN_OPTIONS), diff --git a/esphome/components/ltr501/ltr501.h b/esphome/components/ltr501/ltr501.h index 2bd838a0fe..2b91463108 100644 --- a/esphome/components/ltr501/ltr501.h +++ b/esphome/components/ltr501/ltr501.h @@ -58,6 +58,14 @@ class LTRAlsPs501Component : public PollingComponent, public i2c::I2CDevice { void set_actual_integration_time_sensor(sensor::Sensor *sensor) { this->actual_integration_time_sensor_ = sensor; } void set_proximity_counts_sensor(sensor::Sensor *sensor) { this->proximity_counts_sensor_ = sensor; } + template void add_on_ps_high_trigger_callback(F &&callback) { + this->on_ps_high_trigger_callback_.add(std::forward(callback)); + } + + template void add_on_ps_low_trigger_callback(F &&callback) { + this->on_ps_low_trigger_callback_.add(std::forward(callback)); + } + protected: // // Internal state machine, used to split all the actions into @@ -151,36 +159,8 @@ class LTRAlsPs501Component : public PollingComponent, public i2c::I2CDevice { } bool is_any_ps_sensor_enabled_() const { return this->proximity_counts_sensor_ != nullptr; } - // - // Trigger section for the automations - // - friend class LTRPsHighTrigger; - friend class LTRPsLowTrigger; - CallbackManager on_ps_high_trigger_callback_; CallbackManager on_ps_low_trigger_callback_; - - template void add_on_ps_high_trigger_callback_(F &&callback) { - this->on_ps_high_trigger_callback_.add(std::forward(callback)); - } - - template void add_on_ps_low_trigger_callback_(F &&callback) { - this->on_ps_low_trigger_callback_.add(std::forward(callback)); - } -}; - -class LTRPsHighTrigger : public Trigger<> { - public: - explicit LTRPsHighTrigger(LTRAlsPs501Component *parent) { - parent->add_on_ps_high_trigger_callback_([this]() { this->trigger(); }); - } -}; - -class LTRPsLowTrigger : public Trigger<> { - public: - explicit LTRPsLowTrigger(LTRAlsPs501Component *parent) { - parent->add_on_ps_low_trigger_callback_([this]() { this->trigger(); }); - } }; } // namespace ltr501 } // namespace esphome diff --git a/esphome/components/ltr501/sensor.py b/esphome/components/ltr501/sensor.py index adaf669a72..712810222c 100644 --- a/esphome/components/ltr501/sensor.py +++ b/esphome/components/ltr501/sensor.py @@ -14,7 +14,6 @@ from esphome.const import ( CONF_INTEGRATION_TIME, CONF_NAME, CONF_REPEAT, - CONF_TRIGGER_ID, CONF_TYPE, DEVICE_CLASS_DISTANCE, DEVICE_CLASS_ILLUMINANCE, @@ -87,9 +86,6 @@ PS_GAINS = { "16X": PsGain.PS_GAIN_16, } -LTRPsHighTrigger = ltr501_ns.class_("LTRPsHighTrigger", automation.Trigger.template()) -LTRPsLowTrigger = ltr501_ns.class_("LTRPsLowTrigger", automation.Trigger.template()) - def validate_integration_time(value): value = cv.positive_time_period_milliseconds(value).total_milliseconds @@ -146,16 +142,8 @@ CONFIG_SCHEMA = cv.All( cv.Optional(CONF_PS_LOW_THRESHOLD, default=0): cv.int_range( min=0, max=65535 ), - cv.Optional(CONF_ON_PS_HIGH_THRESHOLD): automation.validate_automation( - { - cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(LTRPsHighTrigger), - } - ), - cv.Optional(CONF_ON_PS_LOW_THRESHOLD): automation.validate_automation( - { - cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(LTRPsLowTrigger), - } - ), + cv.Optional(CONF_ON_PS_HIGH_THRESHOLD): automation.validate_automation({}), + cv.Optional(CONF_ON_PS_LOW_THRESHOLD): automation.validate_automation({}), cv.Optional(CONF_AMBIENT_LIGHT): cv.maybe_simple_value( sensor.sensor_schema( unit_of_measurement=UNIT_LUX, @@ -252,13 +240,14 @@ async def to_code(config): sens = await sensor.new_sensor(prox_cnt_config) cg.add(var.set_proximity_counts_sensor(sens)) - for prox_high_tr in config.get(CONF_ON_PS_HIGH_THRESHOLD, []): - trigger = cg.new_Pvariable(prox_high_tr[CONF_TRIGGER_ID], var) - await automation.build_automation(trigger, [], prox_high_tr) - - for prox_low_tr in config.get(CONF_ON_PS_LOW_THRESHOLD, []): - trigger = cg.new_Pvariable(prox_low_tr[CONF_TRIGGER_ID], var) - await automation.build_automation(trigger, [], prox_low_tr) + for conf in config.get(CONF_ON_PS_HIGH_THRESHOLD, []): + await automation.build_callback_automation( + var, "add_on_ps_high_trigger_callback", [], conf + ) + for conf in config.get(CONF_ON_PS_LOW_THRESHOLD, []): + await automation.build_callback_automation( + var, "add_on_ps_low_trigger_callback", [], conf + ) cg.add(var.set_ltr_type(config[CONF_TYPE])) diff --git a/esphome/components/ltr_als_ps/ltr_als_ps.h b/esphome/components/ltr_als_ps/ltr_als_ps.h index 2e24a14283..8aa5c9f24b 100644 --- a/esphome/components/ltr_als_ps/ltr_als_ps.h +++ b/esphome/components/ltr_als_ps/ltr_als_ps.h @@ -58,6 +58,14 @@ class LTRAlsPsComponent : public PollingComponent, public i2c::I2CDevice { void set_actual_integration_time_sensor(sensor::Sensor *sensor) { this->actual_integration_time_sensor_ = sensor; } void set_proximity_counts_sensor(sensor::Sensor *sensor) { this->proximity_counts_sensor_ = sensor; } + template void add_on_ps_high_trigger_callback(F &&callback) { + this->on_ps_high_trigger_callback_.add(std::forward(callback)); + } + + template void add_on_ps_low_trigger_callback(F &&callback) { + this->on_ps_low_trigger_callback_.add(std::forward(callback)); + } + protected: // // Internal state machine, used to split all the actions into @@ -151,36 +159,8 @@ class LTRAlsPsComponent : public PollingComponent, public i2c::I2CDevice { } bool is_any_ps_sensor_enabled_() const { return this->proximity_counts_sensor_ != nullptr; } - // - // Trigger section for the automations - // - friend class LTRPsHighTrigger; - friend class LTRPsLowTrigger; - CallbackManager on_ps_high_trigger_callback_; CallbackManager on_ps_low_trigger_callback_; - - template void add_on_ps_high_trigger_callback_(F &&callback) { - this->on_ps_high_trigger_callback_.add(std::forward(callback)); - } - - template void add_on_ps_low_trigger_callback_(F &&callback) { - this->on_ps_low_trigger_callback_.add(std::forward(callback)); - } -}; - -class LTRPsHighTrigger : public Trigger<> { - public: - explicit LTRPsHighTrigger(LTRAlsPsComponent *parent) { - parent->add_on_ps_high_trigger_callback_([this]() { this->trigger(); }); - } -}; - -class LTRPsLowTrigger : public Trigger<> { - public: - explicit LTRPsLowTrigger(LTRAlsPsComponent *parent) { - parent->add_on_ps_low_trigger_callback_([this]() { this->trigger(); }); - } }; } // namespace ltr_als_ps } // namespace esphome diff --git a/esphome/components/ltr_als_ps/sensor.py b/esphome/components/ltr_als_ps/sensor.py index 0dbcff1bfb..57503772a1 100644 --- a/esphome/components/ltr_als_ps/sensor.py +++ b/esphome/components/ltr_als_ps/sensor.py @@ -14,7 +14,6 @@ from esphome.const import ( CONF_INTEGRATION_TIME, CONF_NAME, CONF_REPEAT, - CONF_TRIGGER_ID, CONF_TYPE, DEVICE_CLASS_ILLUMINANCE, ICON_BRIGHTNESS_5, @@ -93,11 +92,6 @@ PS_GAINS = { "64X": PsGain.PS_GAIN_64, } -LTRPsHighTrigger = ltr_als_ps_ns.class_( - "LTRPsHighTrigger", automation.Trigger.template() -) -LTRPsLowTrigger = ltr_als_ps_ns.class_("LTRPsLowTrigger", automation.Trigger.template()) - def validate_integration_time(value): value = cv.positive_time_period_milliseconds(value).total_milliseconds @@ -143,16 +137,8 @@ CONFIG_SCHEMA = cv.All( cv.Optional(CONF_PS_LOW_THRESHOLD, default=0): cv.int_range( min=0, max=65535 ), - cv.Optional(CONF_ON_PS_HIGH_THRESHOLD): automation.validate_automation( - { - cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(LTRPsHighTrigger), - } - ), - cv.Optional(CONF_ON_PS_LOW_THRESHOLD): automation.validate_automation( - { - cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(LTRPsLowTrigger), - } - ), + cv.Optional(CONF_ON_PS_HIGH_THRESHOLD): automation.validate_automation({}), + cv.Optional(CONF_ON_PS_LOW_THRESHOLD): automation.validate_automation({}), cv.Optional(CONF_AMBIENT_LIGHT): cv.maybe_simple_value( sensor.sensor_schema( unit_of_measurement=UNIT_LUX, @@ -244,13 +230,14 @@ async def to_code(config): sens = await sensor.new_sensor(prox_cnt_config) cg.add(var.set_proximity_counts_sensor(sens)) - for prox_high_tr in config.get(CONF_ON_PS_HIGH_THRESHOLD, []): - trigger = cg.new_Pvariable(prox_high_tr[CONF_TRIGGER_ID], var) - await automation.build_automation(trigger, [], prox_high_tr) - - for prox_low_tr in config.get(CONF_ON_PS_LOW_THRESHOLD, []): - trigger = cg.new_Pvariable(prox_low_tr[CONF_TRIGGER_ID], var) - await automation.build_automation(trigger, [], prox_low_tr) + for conf in config.get(CONF_ON_PS_HIGH_THRESHOLD, []): + await automation.build_callback_automation( + var, "add_on_ps_high_trigger_callback", [], conf + ) + for conf in config.get(CONF_ON_PS_LOW_THRESHOLD, []): + await automation.build_callback_automation( + var, "add_on_ps_low_trigger_callback", [], conf + ) cg.add(var.set_ltr_type(config[CONF_TYPE])) diff --git a/esphome/components/lvgl/__init__.py b/esphome/components/lvgl/__init__.py index a6afa12afa..b69f8ef57b 100644 --- a/esphome/components/lvgl/__init__.py +++ b/esphome/components/lvgl/__init__.py @@ -2,14 +2,14 @@ import importlib from pathlib import Path import pkgutil -from esphome.automation import build_automation, validate_automation +from esphome.automation import Trigger, build_automation, validate_automation import esphome.codegen as cg from esphome.components.const import ( CONF_BYTE_ORDER, CONF_COLOR_DEPTH, CONF_DRAW_ROUNDING, ) -from esphome.components.display import Display +from esphome.components.display import Display, get_display_metadata, validate_rotation from esphome.components.esp32 import ( VARIANT_ESP32P4, add_idf_component, @@ -34,9 +34,9 @@ from esphome.const import ( CONF_ID, CONF_LAMBDA, CONF_LOG_LEVEL, - CONF_ON_BOOT, CONF_ON_IDLE, CONF_PAGES, + CONF_ROTATION, CONF_TIMEOUT, CONF_TRIGGER_ID, ) @@ -48,6 +48,7 @@ from esphome.yaml_util import load_yaml from . import defines as df, helpers, lv_validation as lvalid, widgets from .automation import focused_widgets, layers_to_code, lvgl_update, refreshed_widgets +from .defines import CONF_ALIGN_TO_LAMBDA_ID from .encoders import ( ENCODERS_CONFIG, encoders_to_code, @@ -57,7 +58,7 @@ from .encoders import ( from .gradient import GRADIENT_SCHEMA, gradients_to_code from .keypads import KEYPADS_CONFIG, keypads_to_code from .lv_validation import lv_bool, lv_images_used -from .lvcode import LvContext, LvglComponent, lvgl_static +from .lvcode import LvContext, LvglComponent, lv_event_t_ptr, lvgl_static from .schemas import ( DISP_BG_SCHEMA, FULL_STYLE_SCHEMA, @@ -69,8 +70,18 @@ from .schemas import ( ) from .styles import styles_to_code, theme_to_code from .touchscreens import touchscreen_schema, touchscreens_to_code -from .trigger import add_on_boot_triggers, generate_triggers -from .types import IdleTrigger, PlainTrigger, lv_font_t, lv_group_t, lv_style_t, lvgl_ns +from .trigger import generate_align_tos, generate_triggers +from .types import ( + IdleTrigger, + PlainTrigger, + RotationType, + lv_font_t, + lv_group_t, + lv_lambda_t, + lv_obj_t_ptr, + lv_style_t, + lvgl_ns, +) from .widgets import ( LvScrActType, Widget, @@ -176,6 +187,7 @@ def final_validation(config_list): for config in config_list: if (pages := config.get(CONF_PAGES)) and all(p[df.CONF_SKIP] for p in pages): raise cv.Invalid("At least one page must not be skipped") + uses_rotation = CONF_ROTATION in config for display_id in config[df.CONF_DISPLAYS]: path = global_config.get_path_for_id(display_id)[:-1] display = global_config.get_config_for_path(path) @@ -183,6 +195,11 @@ def final_validation(config_list): raise cv.Invalid( "Using lambda: or pages: in display config is not compatible with LVGL" ) + # treating 0 as false is intended here. + if uses_rotation and display.get(CONF_ROTATION): + df.LOGGER.warning( + "use of 'rotation' in both LVGL and the display config is not recommended" + ) if display.get(CONF_AUTO_CLEAR_ENABLED) is True: raise cv.Invalid( "Using auto_clear_enabled: true in display config not compatible with LVGL" @@ -313,6 +330,18 @@ async def to_code(configs): displays = [ await cg.get_variable(display) for display in config[df.CONF_DISPLAYS] ] + rotation_type = RotationType.ROTATION_UNUSED + # options will have CONF_ROTATION true if rotation is changed in an automation. + if CONF_ROTATION in config or df.get_options().get(CONF_ROTATION) is True: + if all( + get_display_metadata(str(disp)).has_hardware_rotation + for disp in displays + ): + rotation_type = RotationType.ROTATION_HARDWARE + df.LOGGER.info("LVGL will use hardware rotation via display driver") + else: + rotation_type = RotationType.ROTATION_SOFTWARE + df.LOGGER.info("LVGL will use software rotation") lv_component = cg.new_Pvariable( config[CONF_ID], displays, @@ -321,8 +350,11 @@ async def to_code(configs): config[CONF_DRAW_ROUNDING], config[df.CONF_RESUME_ON_INPUT], config[df.CONF_UPDATE_WHEN_DISPLAY_IDLE], + rotation_type, ) await cg.register_component(lv_component, config) + if rotation := config.get(CONF_ROTATION): + cg.add(lv_component.set_rotation(rotation)) Widget.create(config[CONF_ID], lv_component, LvScrActType(), config) lv_scr_act = get_screen_active(lv_component) @@ -345,6 +377,7 @@ async def to_code(configs): Widget.widgets_completed = True async with LvContext(): await generate_triggers() + await generate_align_tos(configs[0]) for config in configs: lv_component = await cg.get_variable(config[CONF_ID]) await generate_page_triggers(config) @@ -365,12 +398,14 @@ async def to_code(configs): f"set_{trigger_name.removeprefix('on_')}_trigger", )(trigger_var) ) - await add_on_boot_triggers(config.get(CONF_ON_BOOT, ())) # This must be done after all widgets are created for comp in helpers.lvgl_components_required: cg.add_define(f"USE_LVGL_{comp.upper()}") - lv_image_formats = df.get_color_formats().copy() + for use in helpers.lv_uses: + df.add_define(f"LV_USE_{use.upper()}") + cg.add_define(f"USE_LVGL_{use.upper()}") + if { "transform_rotation", "transform_scale", @@ -378,22 +413,27 @@ async def to_code(configs): "transform_scale_y", } & styles_used: df.add_define("LV_COLOR_SCREEN_TRANSP", "1") - lv_image_formats.add("ARGB8888") - lv_image_formats.add( - "RGB565" - ) # Currently always need RGB565 for the display buffer - for use in helpers.lv_uses: - df.add_define(f"LV_USE_{use.upper()}") - cg.add_define(f"USE_LVGL_{use.upper()}") + + if configs[0].get(df.CONF_THEME, {}).get(df.CONF_DARK_MODE): + df.add_define("LV_THEME_DEFAULT_DARK", "1") + + # Currently always need RGB565 for the display buffer, and ARGB8888 is used for layer blending + lv_image_formats = {"RGB565", "ARGB8888"} + if { + "drop_shadow_color", + "drop_shadow_offset_x", + "drop_shadow_offset_y", + "drop_shadow_opa", + "drop_shadow_quality", + "drop_shadow_radius", + } & styles_used: + lv_image_formats.add("A8") for image_id in lv_images_used: await cg.get_variable(image_id) metadata = get_image_metadata(image_id.id) image_type = IMAGE_TYPE[metadata.image_type] transparent = metadata.transparency != CONF_OPAQUE - if transparent: - # Internal draw layer will use ARGB8888 - lv_image_formats.add("ARGB8888") if image_type == ImageBinary: lv_image_formats.add("I1") if image_type == ImageGrayscale: @@ -406,6 +446,7 @@ async def to_code(configs): lv_image_formats.add("RGB888") for fmt in lv_image_formats: df.add_define(f"LV_DRAW_SW_SUPPORT_{fmt}", "1") + lv_conf_h_file = CORE.relative_src_path(LV_CONF_FILENAME) write_file_if_changed(lv_conf_h_file, generate_lv_conf_h()) cg.add_build_flag("-DLV_CONF_H=1") @@ -443,8 +484,11 @@ def add_hello_world(config): def _theme_schema(value): return cv.Schema( { - cv.Optional(name): obj_schema(w).extend(FULL_STYLE_SCHEMA) - for name, w in WIDGET_TYPES.items() + cv.Optional(df.CONF_DARK_MODE, default=False): cv.boolean, + **{ + cv.Optional(name): obj_schema(w).extend(FULL_STYLE_SCHEMA) + for name, w in WIDGET_TYPES.items() + }, } )(value) @@ -457,7 +501,19 @@ LVGL_SCHEMA = cv.All( cv.polling_component_schema("1s") .extend( { + **{ + cv.Optional(event): validate_automation( + { + cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id( + Trigger.template(lv_obj_t_ptr, lv_event_t_ptr) + ), + } + ) + for event in df.LV_SCREEN_EVENT_TRIGGERS + + df.LV_DISPLAY_EVENT_TRIGGERS + }, cv.GenerateID(CONF_ID): cv.declare_id(LvglComponent), + cv.GenerateID(CONF_ALIGN_TO_LAMBDA_ID): cv.declare_id(lv_lambda_t), cv.GenerateID(df.CONF_DISPLAYS): display_schema, cv.Optional(CONF_COLOR_DEPTH, default=16): cv.one_of(16), cv.Optional( @@ -469,6 +525,7 @@ LVGL_SCHEMA = cv.All( ): cv.boolean, cv.Optional(CONF_DRAW_ROUNDING, default=2): cv.positive_int, cv.Optional(CONF_BUFFER_SIZE, default=0): cv.percentage, + cv.Optional(CONF_ROTATION): validate_rotation, cv.Optional(CONF_LOG_LEVEL, default="WARN"): cv.one_of( *df.LV_LOG_LEVELS, upper=True ), diff --git a/esphome/components/lvgl/automation.py b/esphome/components/lvgl/automation.py index 24579e5be8..b825320a40 100644 --- a/esphome/components/lvgl/automation.py +++ b/esphome/components/lvgl/automation.py @@ -3,8 +3,9 @@ from typing import Any from esphome import automation import esphome.codegen as cg +from esphome.components.display import validate_rotation import esphome.config_validation as cv -from esphome.const import CONF_ACTION, CONF_GROUP, CONF_ID, CONF_TIMEOUT +from esphome.const import CONF_ACTION, CONF_GROUP, CONF_ID, CONF_ROTATION, CONF_TIMEOUT from esphome.core import Lambda from esphome.cpp_generator import TemplateArguments, get_variable from esphome.cpp_types import nullptr @@ -23,6 +24,7 @@ from .defines import ( PARTS, StaticCastExpression, add_warning, + get_options, ) from .lv_validation import lv_bool, lv_milliseconds from .lvcode import ( @@ -136,7 +138,7 @@ async def update_to_code(config, action_id, template_arg, args): widget.type.w_type.value_property is not None and widget.type.w_type.value_property in config ): - lv.event_send(widget.obj, UPDATE_EVENT, nullptr) + lv_obj.send_event(widget.obj, UPDATE_EVENT, nullptr) widgets = await get_widgets(config[CONF_ID]) return await action_to_code( @@ -191,6 +193,33 @@ async def lvgl_is_idle(config, condition_id, template_arg, args): return var +def _validate_rotation(value): + # Note that we need rotation + get_options()[CONF_ROTATION] = True + return validate_rotation(value) + + +@automation.register_action( + "lvgl.display.set_rotation", + ObjUpdateAction, + cv.maybe_simple_value( + LVGL_SCHEMA.extend( + { + cv.Required(CONF_ROTATION): _validate_rotation, + } + ), + key=CONF_ROTATION, + ), + synchronous=True, +) +async def lvgl_set_rotation(config, action_id, template_arg, args): + lv_comp = await cg.get_variable(config[CONF_LVGL_ID]) + async with LambdaContext() as context: + add_line_marks(where=action_id) + lv_add(lv_comp.set_rotation(config[CONF_ROTATION])) + return cg.new_Pvariable(action_id, template_arg, await context.get_lambda()) + + @automation.register_action( "lvgl.widget.redraw", ObjUpdateAction, @@ -455,6 +484,6 @@ async def obj_refresh_to_code(config, action_id, template_arg, args): widget.type.w_type.value_property is not None and widget.type.w_type.value_property in config ): - lv.event_send(widget.obj, UPDATE_EVENT, nullptr) + lv_obj.send_event(widget.obj, UPDATE_EVENT, nullptr) return await action_to_code(widget, do_refresh, action_id, template_arg, args) diff --git a/esphome/components/lvgl/defines.py b/esphome/components/lvgl/defines.py index 0a53d88669..ef29a99ddd 100644 --- a/esphome/components/lvgl/defines.py +++ b/esphome/components/lvgl/defines.py @@ -29,6 +29,7 @@ KEY_COLOR_FORMATS = "color_formats" KEY_LV_DEFINES = "lv_defines" KEY_REMAPPED_USES = "remapped_uses" KEY_UPDATED_WIDGETS = "updated_widgets" +KEY_OPTIONS = "options" KEY_WARNINGS = "warnings" @@ -52,14 +53,14 @@ def get_remapped_uses(): return get_data(KEY_REMAPPED_USES, set()) -def get_color_formats(): - return get_data(KEY_COLOR_FORMATS, set()) - - def add_warning(msg: str): get_warnings().add(msg) +def get_options(): + return get_data(KEY_OPTIONS) + + class StaticCastExpression(Expression): __slots__ = ("type", "exp") @@ -255,25 +256,86 @@ LV_FONTS = list(f"montserrat_{s}" for s in range(8, 50, 2)) + [ ] LV_EVENT_MAP = { - "PRESS": "PRESSED", - "SHORT_CLICK": "SHORT_CLICKED", + "ALL_EVENTS": "ALL", + "CANCEL": "CANCEL", + "CHANGE": "VALUE_CHANGED", + "CHILD_CHANGE": "CHILD_CHANGED", + "CHILD_CREATE": "CHILD_CREATED", + "CHILD_DELETE": "CHILD_DELETED", + "CLICK": "CLICKED", + "COLOR_FORMAT_CHANGE": "COLOR_FORMAT_CHANGED", + "COVER_CHECK": "COVER_CHECK", + "CREATE": "CREATE", + "DEFOCUS": "DEFOCUSED", + "DELETE": "DELETE", + "DOUBLE_CLICK": "DOUBLE_CLICKED", + "DRAW_MAIN": "DRAW_MAIN", + "DRAW_MAIN_BEGIN": "DRAW_MAIN_BEGIN", + "DRAW_MAIN_END": "DRAW_MAIN_END", + "DRAW_POST": "DRAW_POST", + "DRAW_POST_BEGIN": "DRAW_POST_BEGIN", + "DRAW_POST_END": "DRAW_POST_END", + "DRAW_TASK_ADD": "DRAW_TASK_ADDED", + "FOCUS": "FOCUSED", + "GESTURE": "GESTURE", + "GET_SELF_SIZE": "GET_SELF_SIZE", + "HIT_TEST": "HIT_TEST", + "HOVER_LEAVE": "HOVER_LEAVE", + "HOVER_OVER": "HOVER_OVER", + "INDEV_RESET": "INDEV_RESET", + "INSERT": "INSERT", + "INVALIDATE_AREA": "INVALIDATE_AREA", + "KEY": "KEY", + "LAYOUT_CHANGE": "LAYOUT_CHANGED", + "LEAVE": "LEAVE", "LONG_PRESS": "LONG_PRESSED", "LONG_PRESS_REPEAT": "LONG_PRESSED_REPEAT", - "CLICK": "CLICKED", + "PRESS": "PRESSED", + "PRESS_LOST": "PRESS_LOST", + "PRESSING": "PRESSING", + "READY": "READY", + "REFRESH": "REFRESH", + "REFR_EXT_DRAW_SIZE": "REFR_EXT_DRAW_SIZE", "RELEASE": "RELEASED", + "ROTARY": "ROTARY", + "SCROLL": "SCROLL", "SCROLL_BEGIN": "SCROLL_BEGIN", "SCROLL_END": "SCROLL_END", - "SCROLL": "SCROLL", - "FOCUS": "FOCUSED", - "DEFOCUS": "DEFOCUSED", - "READY": "READY", - "CANCEL": "CANCEL", - "ALL_EVENTS": "ALL", - "CHANGE": "VALUE_CHANGED", - "GESTURE": "GESTURE", + "SCROLL_THROW_BEGIN": "SCROLL_THROW_BEGIN", + "SHORT_CLICK": "SHORT_CLICKED", + "SINGLE_CLICK": "SINGLE_CLICKED", + "SIZE_CHANGE": "SIZE_CHANGED", + "STATE_CHANGE": "STATE_CHANGED", + "STYLE_CHANGE": "STYLE_CHANGED", + "TRIPLE_CLICK": "TRIPLE_CLICKED", +} +LV_SCREEN_EVENT_MAP = { + "SCREEN_LOAD": "SCREEN_LOADED", + "SCREEN_LOAD_START": "SCREEN_LOAD_START", + "SCREEN_UNLOAD": "SCREEN_UNLOADED", + "SCREEN_UNLOAD_START": "SCREEN_UNLOAD_START", +} + +LV_DISPLAY_EVENT_MAP = { + "FLUSH_FINISH": "FLUSH_FINISH", + "FLUSH_START": "FLUSH_START", + "FLUSH_WAIT_FINISH": "FLUSH_WAIT_FINISH", + "FLUSH_WAIT_START": "FLUSH_WAIT_START", + "REFR_READY": "REFR_READY", + "REFR_REQUEST": "REFR_REQUEST", + "REFR_START": "REFR_START", + "RENDER_READY": "RENDER_READY", + "RENDER_START": "RENDER_START", + "RESOLUTION_CHANGE": "RESOLUTION_CHANGED", + "UPDATE_LAYOUT_COMPLETE": "UPDATE_LAYOUT_COMPLETED", + "VSYNC": "VSYNC", + "VSYNC_REQUEST": "VSYNC_REQUEST", } LV_EVENT_TRIGGERS = tuple(f"on_{x.lower()}" for x in LV_EVENT_MAP) +LV_DISPLAY_EVENT_TRIGGERS = tuple(f"on_{x.lower()}" for x in LV_DISPLAY_EVENT_MAP) +LV_SCREEN_EVENT_TRIGGERS = tuple(f"on_{x.lower()}" for x in LV_SCREEN_EVENT_MAP) + SWIPE_TRIGGERS = tuple( f"on_swipe_{x.lower()}" for x in DIRECTIONS.choices + ("up", "down") ) @@ -504,6 +566,7 @@ CONF_ACCEPTED_CHARS = "accepted_chars" CONF_ADJUSTABLE = "adjustable" CONF_ALIGN = "align" CONF_ALIGN_TO = "align_to" +CONF_ALIGN_TO_LAMBDA_ID = "align_to_lambda_id" CONF_ANGLE_RANGE = "angle_range" CONF_ANIMATED = "animated" CONF_ANIMATION = "animation" @@ -540,6 +603,7 @@ CONF_END_ANGLE = "end_angle" CONF_END_VALUE = "end_value" CONF_ENTER_BUTTON = "enter_button" CONF_ENTRIES = "entries" +CONF_EXT_CLICK_AREA = "ext_click_area" CONF_FLAGS = "flags" CONF_FLEX_FLOW = "flex_flow" CONF_FLEX_ALIGN_MAIN = "flex_align_main" @@ -547,6 +611,7 @@ CONF_FLEX_ALIGN_CROSS = "flex_align_cross" CONF_FLEX_ALIGN_TRACK = "flex_align_track" CONF_FLEX_GROW = "flex_grow" CONF_FREEZE = "freeze" +CONF_DARK_MODE = "dark_mode" CONF_FULL_REFRESH = "full_refresh" CONF_GRADIENTS = "gradients" CONF_GRID_CELL_ROW_POS = "grid_cell_row_pos" diff --git a/esphome/components/lvgl/hello_world.yaml b/esphome/components/lvgl/hello_world.yaml index 4af179a589..bbbd34e30a 100644 --- a/esphome/components/lvgl/hello_world.yaml +++ b/esphome/components/lvgl/hello_world.yaml @@ -3,7 +3,7 @@ - obj: id: hello_world_card_ pad_all: 12 - bg_color: white + bg_opa: cover height: 100% width: 100% scrollable: false diff --git a/esphome/components/lvgl/lvcode.py b/esphome/components/lvgl/lvcode.py index 146b261f26..eb8f7d4437 100644 --- a/esphome/components/lvgl/lvcode.py +++ b/esphome/components/lvgl/lvcode.py @@ -253,14 +253,10 @@ class MockLv: A mock object that can be used to generate LVGL calls. """ - # Mapping for LVGL 9 - ATTR_MAP = {"event_send": "obj_send_event", "dither": "bg_dither_mode"} - def __init__(self, base): self.base = base def __getattr__(self, attr: str) -> "MockLv": - attr = MockLv.ATTR_MAP.get(attr, attr) return MockLv(f"{self.base}{attr}") def append(self, expression): @@ -314,7 +310,6 @@ class ReturnStatement(ExpressionStatement): class LvExpr(MockLv): def __getattr__(self, attr: str) -> "MockLv": - attr = MockLv.ATTR_MAP.get(attr, attr) return LvExpr(f"{self.base}{attr}") def append(self, expression): diff --git a/esphome/components/lvgl/lvgl_esphome.cpp b/esphome/components/lvgl/lvgl_esphome.cpp index bf86a4e9ee..0ab49d0a10 100644 --- a/esphome/components/lvgl/lvgl_esphome.cpp +++ b/esphome/components/lvgl/lvgl_esphome.cpp @@ -83,6 +83,44 @@ std::string lv_event_code_name_for(lv_event_t *event) { return buf; } +void LvglComponent::set_rotation(display::DisplayRotation rotation) { + if (this->rotation_type_ == RotationType::ROTATION_UNUSED) { + ESP_LOGW(TAG, "Display rotation cannot be changed unless rotation was enabled during setup."); + return; + } + this->rotation_ = rotation; + if (this->is_ready()) { + this->set_resolution_(); + lv_obj_update_layout(this->get_screen_active()); + lv_obj_invalidate(this->get_screen_active()); + } +} + +void LvglComponent::rotate_coordinates(int32_t &x, int32_t &y) const { + switch (this->rotation_) { + default: + break; + + case display::DISPLAY_ROTATION_180_DEGREES: { + x = this->width_ - x - 1; + y = this->height_ - y - 1; + break; + } + case display::DISPLAY_ROTATION_270_DEGREES: { + auto tmp = x; + x = this->height_ - y - 1; + y = tmp; + break; + } + case display::DISPLAY_ROTATION_90_DEGREES: { + auto tmp = y; + y = this->width_ - x - 1; + x = tmp; + break; + } + } +} + static void rounder_cb(lv_event_t *event) { auto *comp = static_cast(lv_event_get_user_data(event)); auto *area = static_cast(lv_event_get_param(event)); @@ -118,7 +156,11 @@ void LvglComponent::dump_config() { " Buffer size: %zu%%\n" " Rotation: %d\n" " Draw rounding: %d", - this->width_, this->height_, 100 / this->buffer_frac_, this->rotation, (int) this->draw_rounding); + this->width_, this->height_, 100 / this->buffer_frac_, this->rotation_, (int) this->draw_rounding); + if (this->rotation_type_ != ROTATION_UNUSED) { + ESP_LOGCONFIG(TAG, " Rotation type: %s", + this->rotation_type_ == RotationType::ROTATION_SOFTWARE ? "software" : "hardware via display driver"); + } } void LvglComponent::set_paused(bool paused, bool show_snow) { @@ -216,48 +258,51 @@ void LvglComponent::draw_buffer_(const lv_area_t *area, lv_color_data *ptr) { auto height_rounded = (height + this->draw_rounding - 1) / this->draw_rounding * this->draw_rounding; auto x1 = area->x1; auto y1 = area->y1; - lv_color_data *dst = reinterpret_cast(this->rotate_buf_); - switch (this->rotation) { - case display::DISPLAY_ROTATION_90_DEGREES: - for (lv_coord_t x = height; x-- != 0;) { - for (lv_coord_t y = 0; y != width; y++) { - dst[y * height_rounded + x] = *ptr++; + if (this->rotation_type_ == RotationType::ROTATION_SOFTWARE) { + lv_color_data *dst = reinterpret_cast(this->rotate_buf_); + switch (this->rotation_) { + case display::DISPLAY_ROTATION_90_DEGREES: + for (lv_coord_t x = height; x-- != 0;) { + for (lv_coord_t y = 0; y != width; y++) { + dst[y * height_rounded + x] = *ptr++; + } } - } - y1 = x1; - x1 = this->height_ - area->y1 - height; - height = width; - width = height_rounded; - break; + y1 = x1; + x1 = this->width_ - area->y1 - height; + height = width; + width = height_rounded; + break; - case display::DISPLAY_ROTATION_180_DEGREES: - for (lv_coord_t y = height; y-- != 0;) { - for (lv_coord_t x = width; x-- != 0;) { - dst[y * width + x] = *ptr++; + case display::DISPLAY_ROTATION_180_DEGREES: + for (lv_coord_t y = height; y-- != 0;) { + for (lv_coord_t x = width; x-- != 0;) { + dst[y * width + x] = *ptr++; + } } - } - x1 = this->width_ - x1 - width; - y1 = this->height_ - y1 - height; - break; + x1 = this->width_ - x1 - width; + y1 = this->height_ - y1 - height; + break; - case display::DISPLAY_ROTATION_270_DEGREES: - for (lv_coord_t x = 0; x != height; x++) { - for (lv_coord_t y = width; y-- != 0;) { - dst[y * height_rounded + x] = *ptr++; + case display::DISPLAY_ROTATION_270_DEGREES: + for (lv_coord_t x = 0; x != height; x++) { + for (lv_coord_t y = width; y-- != 0;) { + dst[y * height_rounded + x] = *ptr++; + } } - } - x1 = y1; - y1 = this->width_ - area->x1 - width; - height = width; - width = height_rounded; - break; + x1 = y1; + y1 = this->height_ - area->x1 - width; + height = width; + width = height_rounded; + break; - default: - dst = ptr; - break; + default: + dst = ptr; + break; + } + ptr = dst; } for (auto *display : this->displays_) { - display->draw_pixels_at(x1, y1, width, height, (const uint8_t *) dst, display::COLOR_ORDER_RGB, LV_BITNESS, + display->draw_pixels_at(x1, y1, width, height, (const uint8_t *) ptr, display::COLOR_ORDER_RGB, LV_BITNESS, this->big_endian_); } } @@ -297,6 +342,7 @@ LVTouchListener::LVTouchListener(uint16_t long_press_time, uint16_t long_press_r if (l->touch_pressed_) { data->point.x = l->touch_point_.x; data->point.y = l->touch_point_.y; + l->parent_->rotate_coordinates(data->point.x, data->point.y); data->state = LV_INDEV_STATE_PRESSED; } else { data->state = LV_INDEV_STATE_RELEASED; @@ -343,26 +389,26 @@ void IndicatorLine::set_value(int value) { } void IndicatorLine::update_length_() { - uint32_t actual_needle_length; - auto radius = lv_obj_get_width(lv_obj_get_parent(this->obj)) / 2; + auto cx = lv_obj_get_width(lv_obj_get_parent(this->obj)) / 2; + auto cy = lv_obj_get_height(lv_obj_get_parent(this->obj)) / 2; + auto radius = clamp_at_most(cx, cy); auto length = lv_obj_get_style_length(this->obj, LV_PART_MAIN); auto radial_offset = lv_obj_get_style_radial_offset(this->obj, LV_PART_MAIN); if (LV_COORD_IS_PCT(radial_offset)) { radial_offset = radius * LV_COORD_GET_PCT(radial_offset) / 100; } if (LV_COORD_IS_PCT(length)) { - actual_needle_length = radius * LV_COORD_GET_PCT(length) / 100; + length = radius * LV_COORD_GET_PCT(length) / 100; } else if (length < 0) { - actual_needle_length = radius + length; - } else { - actual_needle_length = length; + length += radius; } auto x = lv_trigo_cos(this->angle_) / 32768.0f; auto y = lv_trigo_sin(this->angle_) / 32768.0f; + // radius here also represents the offset of the scale center from top left this->points_[0].x = radius + radial_offset * x; this->points_[0].y = radius + radial_offset * y; - this->points_[1].x = x * actual_needle_length + radius; - this->points_[1].y = y * actual_needle_length + radius; + this->points_[1].x = radius + x * (radial_offset + length); + this->points_[1].y = radius + y * (radial_offset + length); lv_obj_refresh_self_size(this->obj); lv_obj_invalidate(this->obj); } @@ -543,26 +589,44 @@ void LvglComponent::write_random_() { * multiple of 2, and so on. * @param resume_on_input if true, this component will resume rendering when the user * presses a key or clicks on the screen. + * @param rotation_type What rotation type to use, if any */ LvglComponent::LvglComponent(std::vector displays, float buffer_frac, bool full_refresh, - int draw_rounding, bool resume_on_input, bool update_when_display_idle) + int draw_rounding, bool resume_on_input, bool update_when_display_idle, + RotationType rotation_type) : draw_rounding(draw_rounding), displays_(std::move(displays)), buffer_frac_(buffer_frac), full_refresh_(full_refresh), resume_on_input_(resume_on_input), - update_when_display_idle_(update_when_display_idle) { + update_when_display_idle_(update_when_display_idle), + rotation_type_(rotation_type) { this->disp_ = lv_display_create(240, 240); } +void LvglComponent::set_resolution_() const { + int32_t width = this->width_; + int32_t height = this->height_; + if (this->rotation_ == display::DISPLAY_ROTATION_90_DEGREES || + this->rotation_ == display::DISPLAY_ROTATION_270_DEGREES) { + std::swap(width, height); + } + ESP_LOGD(TAG, "Setting resolution to %u x %u (rotation %d)", (unsigned) width, (unsigned) height, + (int) this->rotation_); + if (this->rotation_type_ == RotationType::ROTATION_HARDWARE) { + for (auto *display : this->displays_) + display->set_rotation(this->rotation_); + } + lv_display_set_resolution(this->disp_, width, height); +} void LvglComponent::setup() { auto *display = this->displays_[0]; auto rounding = this->draw_rounding; + this->width_ = display->get_native_width(); + this->height_ = display->get_native_height(); // cater for displays with dimensions that don't divide by the required rounding - this->width_ = display->get_width(); - this->height_ = display->get_height(); - auto width = (display->get_width() + rounding - 1) / rounding * rounding; - auto height = (display->get_height() + rounding - 1) / rounding * rounding; + auto width = (this->width_ + rounding - 1) / rounding * rounding; + auto height = (this->height_ + rounding - 1) / rounding * rounding; auto frac = this->buffer_frac_; if (frac == 0) frac = 1; @@ -586,15 +650,14 @@ void LvglComponent::setup() { return; } this->draw_buf_ = static_cast(buffer); - lv_display_set_resolution(this->disp_, this->width_, this->height_); + this->set_resolution_(); lv_display_set_color_format(this->disp_, LV_COLOR_FORMAT_RGB565); lv_display_set_flush_cb(this->disp_, static_flush_cb); lv_display_set_user_data(this->disp_, this); lv_display_add_event_cb(this->disp_, rounder_cb, LV_EVENT_INVALIDATE_AREA, this); lv_display_set_buffers(this->disp_, this->draw_buf_, nullptr, buf_bytes, this->full_refresh_ ? LV_DISPLAY_RENDER_MODE_FULL : LV_DISPLAY_RENDER_MODE_PARTIAL); - this->rotation = display->get_rotation(); - if (this->rotation != display::DISPLAY_ROTATION_0_DEGREES) { + if (this->rotation_type_ == RotationType::ROTATION_SOFTWARE) { this->rotate_buf_ = static_cast(lv_alloc_draw_buf(buf_bytes, false)); // NOLINT if (this->rotate_buf_ == nullptr) { this->status_set_error(LOG_STR("Memory allocation failure")); @@ -620,9 +683,6 @@ void LvglComponent::setup() { esp_log_printf_(LOG_LEVEL_MAP[level], TAG, 0, "%.*s", (int) strlen(buf) - 1, buf); }); #endif - // Rotation will be handled by our drawing function, so reset the display rotation. - for (auto *disp : this->displays_) - disp->set_rotation(display::DISPLAY_ROTATION_0_DEGREES); this->show_page(0, LV_SCREEN_LOAD_ANIM_NONE, 0); lv_display_trigger_activity(this->disp_); } @@ -682,15 +742,15 @@ void lv_scale_draw_event_cb(lv_event_t *e, int16_t range_start, int16_t range_en auto *line_dsc = static_cast(lv_draw_task_get_draw_dsc(task)); int tick = line_dsc->base.id2; if (tick >= range_start && tick <= range_end) { - unsigned range = range_end - range_start; + int ratio; if (local) { + int range = range_end - range_start; tick -= range_start; + ratio = range == 0 ? 0 : (tick * 255) / range; } else { - range = lv_scale_get_total_tick_count(scale) - 1; + // total tick count is guaranteed to be at least 2. + ratio = (line_dsc->base.id1 * 255) / (lv_scale_get_total_tick_count(scale) - 1); } - if (range == 0) - range = 1; - auto ratio = (tick * 255) / range; line_dsc->color = lv_color_mix(color_end, color_start, ratio); line_dsc->width += width; } diff --git a/esphome/components/lvgl/lvgl_esphome.h b/esphome/components/lvgl/lvgl_esphome.h index 8de82d50c0..4a4c11d383 100644 --- a/esphome/components/lvgl/lvgl_esphome.h +++ b/esphome/components/lvgl/lvgl_esphome.h @@ -74,11 +74,13 @@ inline void lv_style_set_text_font(lv_style_t *style, const font::Font *font) { #if defined(USE_LVGL_IMAGE) && defined(USE_IMAGE) // Shortcut / overload, so that the source of an image can easily be updated // from within a lambda. -inline void lv_image_set_src(lv_obj_t *obj, esphome::image::Image *image) { - lv_image_set_src(obj, image->get_lv_image_dsc()); +inline void lv_image_set_src(lv_obj_t *obj, image::Image *image) { lv_image_set_src(obj, image->get_lv_image_dsc()); } + +inline void lv_obj_set_style_bitmap_mask_src(lv_obj_t *obj, image::Image *image, lv_style_selector_t selector) { + lv_obj_set_style_bitmap_mask_src(obj, image->get_lv_image_dsc(), selector); } -inline void lv_obj_set_style_bg_image_src(lv_obj_t *obj, esphome::image::Image *image, lv_style_selector_t selector) { +inline void lv_obj_set_style_bg_image_src(lv_obj_t *obj, image::Image *image, lv_style_selector_t selector) { lv_obj_set_style_bg_image_src(obj, image->get_lv_image_dsc(), selector); } #endif // USE_LVGL_IMAGE @@ -128,10 +130,19 @@ class LvPageType : public Parented { bool skip; }; -using LvLambdaType = std::function; -using set_value_lambda_t = std::function; using event_callback_t = void(lv_event_t *); -using text_lambda_t = std::function; + +class LvLambdaComponent : public Component { + public: + LvLambdaComponent(void (*callback)()) : callback_(callback) {} + + void setup() override { this->callback_(); } + // execute after the LvglComponent is setup + float get_setup_priority() const override { return setup_priority::PROCESSOR - 5; } + + protected: + void (*callback_)(); +}; template class ObjUpdateAction : public Action { public: @@ -145,13 +156,18 @@ template class ObjUpdateAction : public Action { #ifdef USE_LVGL_ANIMIMG void lv_animimg_stop(lv_obj_t *obj); #endif // USE_LVGL_ANIMIMG +enum RotationType : uint8_t { + ROTATION_UNUSED, + ROTATION_SOFTWARE, + ROTATION_HARDWARE, +}; class LvglComponent : public PollingComponent { constexpr static const char *const TAG = "lvgl"; public: LvglComponent(std::vector displays, float buffer_frac, bool full_refresh, int draw_rounding, - bool resume_on_input, bool update_when_display_idle); + bool resume_on_input, bool update_when_display_idle, RotationType rotation_type); static void static_flush_cb(lv_display_t *disp_drv, const lv_area_t *area, uint8_t *color_p); float get_setup_priority() const override { return setup_priority::PROCESSOR; } @@ -205,13 +221,16 @@ class LvglComponent : public PollingComponent { // rounding factor to align bounds of update area when drawing size_t draw_rounding{2}; - display::DisplayRotation rotation{display::DISPLAY_ROTATION_0_DEGREES}; void set_pause_trigger(Trigger<> *trigger) { this->pause_callback_ = trigger; } void set_resume_trigger(Trigger<> *trigger) { this->resume_callback_ = trigger; } void set_draw_start_trigger(Trigger<> *trigger) { this->draw_start_callback_ = trigger; } void set_draw_end_trigger(Trigger<> *trigger) { this->draw_end_callback_ = trigger; } + void set_rotation(display::DisplayRotation rotation); + display::DisplayRotation get_rotation() const { return this->rotation_; } + void rotate_coordinates(int32_t &x, int32_t &y) const; protected: + void set_resolution_() const; void draw_end_(); // Not checking for non-null callback since the // LVGL callback that calls it is not set in that case @@ -245,6 +264,8 @@ class LvglComponent : public PollingComponent { Trigger<> *draw_start_callback_{}; Trigger<> *draw_end_callback_{}; void *rotate_buf_{}; + display::DisplayRotation rotation_{display::DISPLAY_ROTATION_0_DEGREES}; + RotationType rotation_type_; }; class IdleTrigger : public Trigger<> { diff --git a/esphome/components/lvgl/number/__init__.py b/esphome/components/lvgl/number/__init__.py index c48e051eac..d80e93708b 100644 --- a/esphome/components/lvgl/number/__init__.py +++ b/esphome/components/lvgl/number/__init__.py @@ -12,7 +12,7 @@ from ..lvcode import ( UPDATE_EVENT, LambdaContext, ReturnStatement, - lv, + lv_obj, lvgl_static, ) from ..types import LV_EVENT, LvNumber, lvgl_ns @@ -40,7 +40,7 @@ async def to_code(config): await widget.set_property( "value", MockObj("v") * MockObj(widget.get_scale()), config[CONF_ANIMATED] ) - lv.event_send(widget.obj, API_EVENT, cg.nullptr) + lv_obj.send_event(widget.obj, API_EVENT, cg.nullptr) event_code = ( LV_EVENT.VALUE_CHANGED if not config[CONF_UPDATE_ON_RELEASE] diff --git a/esphome/components/lvgl/schemas.py b/esphome/components/lvgl/schemas.py index bcbb193ce3..2c57452a55 100644 --- a/esphome/components/lvgl/schemas.py +++ b/esphome/components/lvgl/schemas.py @@ -23,6 +23,7 @@ from esphome.core.config import StartupTrigger from . import defines as df, lv_validation as lvalid from .defines import ( + CONF_EXT_CLICK_AREA, CONF_SCROLL_DIR, CONF_SCROLL_SNAP_X, CONF_SCROLL_SNAP_Y, @@ -146,26 +147,42 @@ def point_schema(value): # All LVGL styles and their validators -STYLE_PROPS = { +BASE_PROPS = { "align": df.CHILD_ALIGNMENTS.one_of, - "arc_opa": lvalid.opacity, + "anim_duration": lvalid.lv_milliseconds, "arc_color": lvalid.lv_color, + "arc_opa": lvalid.opacity, "arc_rounded": lvalid.lv_bool, "arc_width": lvalid.pixels, - "anim_time": lvalid.lv_milliseconds, + "base_dir": df.LvConstant("LV_BASE_DIR_", "LTR", "RTL", "AUTO").one_of, "bg_color": lvalid.lv_color, "bg_grad": lv_gradient, "bg_grad_color": lvalid.lv_color, - "bg_dither_mode": df.LvConstant("LV_DITHER_", "NONE", "ORDERED", "ERR_DIFF").one_of, "bg_grad_dir": LV_GRAD_DIR.one_of, + "bg_grad_opa": lvalid.opacity, "bg_grad_stop": lvalid.stop_value, "bg_image_opa": lvalid.opacity, "bg_image_recolor": lvalid.lv_color, "bg_image_recolor_opa": lvalid.opacity, "bg_image_src": lvalid.lv_image, "bg_image_tiled": lvalid.lv_bool, + "bg_main_opa": lvalid.opacity, "bg_main_stop": lvalid.stop_value, "bg_opa": lvalid.opacity, + "bitmap_mask_src": lvalid.lv_image, + "blend_mode": df.LvConstant( + "LV_BLEND_MODE_", + "NORMAL", + "ADDITIVE", + "SUBTRACTIVE", + "MULTIPLY", + "DIFFERENCE", + ).one_of, + "blur_backdrop": lvalid.lv_bool, + "blur_quality": df.LvConstant( + "LV_BLUR_QUALITY_", "AUTO", "SPEED", "PRECISION" + ).one_of, + "blur_radius": lvalid.lv_positive_int, "border_color": lvalid.lv_color, "border_opa": lvalid.opacity, "border_post": lvalid.lv_bool, @@ -175,33 +192,53 @@ STYLE_PROPS = { "border_width": lvalid.lv_positive_int, "clip_corner": lvalid.lv_bool, "color_filter_opa": lvalid.opacity, + "drop_shadow_color": lvalid.lv_color, + "drop_shadow_offset_x": lvalid.lv_int, + "drop_shadow_offset_y": lvalid.lv_int, + "drop_shadow_opa": lvalid.opacity, + "drop_shadow_quality": df.LvConstant( + "LV_BLUR_QUALITY_", "AUTO", "SPEED", "PRECISION" + ).one_of, + "drop_shadow_radius": lvalid.lv_positive_int, "height": lvalid.size, + "image_opa": lvalid.opacity, "image_recolor": lvalid.lv_color, "image_recolor_opa": lvalid.opacity, + "length": lvalid.pixels_or_percent, "line_color": lvalid.lv_color, "line_dash_gap": lvalid.lv_positive_int, "line_dash_width": lvalid.lv_positive_int, "line_opa": lvalid.opacity, "line_rounded": lvalid.lv_bool, "line_width": lvalid.lv_positive_int, + "margin_bottom": lvalid.padding, + "margin_left": lvalid.padding, + "margin_right": lvalid.padding, + "margin_top": lvalid.padding, + "max_height": lvalid.pixels_or_percent, + "max_width": lvalid.pixels_or_percent, + "min_height": lvalid.pixels_or_percent, + "min_width": lvalid.pixels_or_percent, "opa": lvalid.opacity, "opa_layered": lvalid.opacity, "outline_color": lvalid.lv_color, "outline_opa": lvalid.opacity, "outline_pad": lvalid.padding, "outline_width": lvalid.pixels, - "length": lvalid.pixels_or_percent, "pad_all": lvalid.padding, "pad_bottom": lvalid.padding, "pad_left": lvalid.padding, + "pad_radial": lvalid.padding, "pad_right": lvalid.padding, "pad_top": lvalid.padding, "radial_offset": lvalid.size, + "radius": lvalid.lv_fraction, + "recolor": lvalid.lv_color, + "recolor_opa": lvalid.opacity, + "rotary_sensitivity": lvalid.lv_positive_int, "shadow_color": lvalid.lv_color, "shadow_offset_x": lvalid.lv_int, "shadow_offset_y": lvalid.lv_int, - "shadow_ofs_x": lvalid.lv_int, - "shadow_ofs_y": lvalid.lv_int, "shadow_opa": lvalid.opacity, "shadow_spread": lvalid.lv_int, "shadow_width": lvalid.lv_positive_int, @@ -216,7 +253,9 @@ STYLE_PROPS = { "text_letter_space": lvalid.lv_positive_int, "text_line_space": lvalid.lv_positive_int, "text_opa": lvalid.opacity, - "transform_angle": lvalid.lv_angle, + "text_outline_stroke_color": lvalid.lv_color, + "text_outline_stroke_opa": lvalid.opacity, + "text_outline_stroke_width": lvalid.lv_positive_int, "transform_height": lvalid.pixels_or_percent, "transform_pivot_x": lvalid.pixels_or_percent, "transform_pivot_y": lvalid.pixels_or_percent, @@ -226,20 +265,17 @@ STYLE_PROPS = { "transform_scale_y": lvalid.scale, "transform_skew_x": lvalid.lv_angle, "transform_skew_y": lvalid.lv_angle, - "transform_zoom": lvalid.scale, + "transform_width": lvalid.pixels_or_percent, + "translate_radial": lvalid.lv_int, "translate_x": lvalid.pixels_or_percent, "translate_y": lvalid.pixels_or_percent, - "max_height": lvalid.pixels_or_percent, - "max_width": lvalid.pixels_or_percent, - "min_height": lvalid.pixels_or_percent, - "min_width": lvalid.pixels_or_percent, - "radius": lvalid.lv_fraction, "width": lvalid.size, "x": lvalid.pixels_or_percent, "y": lvalid.pixels_or_percent, } STYLE_REMAP = { + "anim_time": "anim_duration", "transform_angle": "transform_rotation", "transform_zoom": "transform_scale", "zoom": "scale", @@ -249,6 +285,10 @@ STYLE_REMAP = { "r_mod": "length", } +STYLE_PROPS = BASE_PROPS | { + p: BASE_PROPS[v] for p, v in STYLE_REMAP.items() if v in BASE_PROPS +} + def remap_property(prop, record=True): """ @@ -272,6 +312,7 @@ STYLE_SCHEMA = cv.Schema({cv.Optional(k): v for k, v in STYLE_PROPS.items()}).ex cv.Optional(df.CONF_SCROLLBAR_MODE): df.LvConstant( "LV_SCROLLBAR_MODE_", "OFF", "ON", "ACTIVE", "AUTO" ).one_of, + cv.Optional(CONF_EXT_CLICK_AREA): lvalid.pixels, cv.Optional(CONF_SCROLL_DIR): df.SCROLL_DIRECTIONS.one_of, cv.Optional(CONF_SCROLL_SNAP_X): df.SNAP_DIRECTIONS.one_of, cv.Optional(CONF_SCROLL_SNAP_Y): df.SNAP_DIRECTIONS.one_of, @@ -279,6 +320,7 @@ STYLE_SCHEMA = cv.Schema({cv.Optional(k): v for k, v in STYLE_PROPS.items()}).ex ) OBJ_PROPERTIES = { + CONF_EXT_CLICK_AREA, CONF_SCROLL_SNAP_X, CONF_SCROLL_SNAP_Y, CONF_SCROLL_DIR, diff --git a/esphome/components/lvgl/styles.py b/esphome/components/lvgl/styles.py index 6f43e78f90..c17f30383b 100644 --- a/esphome/components/lvgl/styles.py +++ b/esphome/components/lvgl/styles.py @@ -4,26 +4,12 @@ import esphome.config_validation as cv from esphome.const import CONF_ID from esphome.core import ID -from .defines import ( - CONF_STYLE_DEFINITIONS, - CONF_THEME, - CONF_TOP_LAYER, - LValidator, - literal, -) +from .defines import CONF_STYLE_DEFINITIONS, CONF_THEME, LValidator, literal from .helpers import add_lv_use -from .lvcode import LambdaContext, LocalVariable, lv -from .schemas import ALL_STYLES, FULL_STYLE_SCHEMA, remap_property -from .types import ObjUpdateAction, lv_obj_t, lv_style_t -from .widgets import ( - Widget, - add_widgets, - collect_parts, - set_obj_properties, - theme_widget_map, - wait_for_widgets, -) -from .widgets.obj import obj_spec +from .lvcode import LambdaContext, lv +from .schemas import ALL_STYLES, FULL_STYLE_SCHEMA, WIDGET_TYPES, remap_property +from .types import ObjUpdateAction, lv_style_t +from .widgets import collect_parts, theme_widget_map, wait_for_widgets def has_style_props(config) -> bool: @@ -99,7 +85,7 @@ async def style_update_to_code(config, action_id, template_arg, args): async def theme_to_code(config): if theme := config.get(CONF_THEME): add_lv_use(CONF_THEME) - for w_name, style in theme.items(): + for w_name, style in ((k, v) for k, v in theme.items() if k in WIDGET_TYPES): # Work around Python 3.10 bug with nested async comprehensions # With Python 3.11 this could be simplified # TODO: Now that we require Python 3.11+, this can be updated to use nested comprehensions @@ -112,12 +98,3 @@ async def theme_to_code(config): for state, props in states.items() } theme_widget_map[w_name] = styles - - -async def add_top_layer(lv_component, config): - top_layer = lv.disp_get_layer_top(lv_component.var.get_disp()) - if top_conf := config.get(CONF_TOP_LAYER): - with LocalVariable("top_layer", lv_obj_t, top_layer) as top_layer_obj: - top_w = Widget(top_layer_obj, obj_spec, top_conf) - await set_obj_properties(top_w, top_conf) - await add_widgets(top_w, top_conf) diff --git a/esphome/components/lvgl/switch/__init__.py b/esphome/components/lvgl/switch/__init__.py index 6d10a70d85..a43851b4a3 100644 --- a/esphome/components/lvgl/switch/__init__.py +++ b/esphome/components/lvgl/switch/__init__.py @@ -13,8 +13,8 @@ from ..lvcode import ( LambdaContext, LvConditional, LvContext, - lv, lv_add, + lv_obj, lvgl_static, ) from ..types import LV_EVENT, LV_STATE, lv_pseudo_button_t, lvgl_ns @@ -39,7 +39,7 @@ async def to_code(config): widget.add_state(LV_STATE.CHECKED) cond.else_() widget.clear_state(LV_STATE.CHECKED) - lv.event_send(widget.obj, API_EVENT, cg.nullptr) + lv_obj.send_event(widget.obj, API_EVENT, cg.nullptr) control.add(switch_id.publish_state(v)) switch = cg.new_Pvariable(config[CONF_ID], await control.get_lambda()) await cg.register_component(switch, config) diff --git a/esphome/components/lvgl/text/__init__.py b/esphome/components/lvgl/text/__init__.py index eb56cdb7a7..190ecacda5 100644 --- a/esphome/components/lvgl/text/__init__.py +++ b/esphome/components/lvgl/text/__init__.py @@ -10,8 +10,8 @@ from ..lvcode import ( UPDATE_EVENT, LambdaContext, LvContext, - lv, lv_add, + lv_obj, lvgl_static, ) from ..types import LV_EVENT, LvText, lvgl_ns @@ -33,7 +33,7 @@ async def to_code(config): await wait_for_widgets() async with LambdaContext([(cg.std_string, "text_value")]) as control: await widget.set_property("text", "text_value.c_str()") - lv.event_send(widget.obj, API_EVENT, cg.nullptr) + lv_obj.send_event(widget.obj, API_EVENT, cg.nullptr) control.add(textvar.publish_state(widget.get_value())) async with LambdaContext(EVENT_ARG) as lamb: lv_add(textvar.publish_state(widget.get_value())) diff --git a/esphome/components/lvgl/trigger.py b/esphome/components/lvgl/trigger.py index 077ff06bb7..f825999e8a 100644 --- a/esphome/components/lvgl/trigger.py +++ b/esphome/components/lvgl/trigger.py @@ -8,13 +8,21 @@ from esphome.const import ( CONF_X, CONF_Y, ) +from esphome.cpp_generator import MockObj, new_Pvariable +from esphome.cpp_helpers import register_component +from esphome.cpp_types import nullptr from .defines import ( CONF_ALIGN, CONF_ALIGN_TO, + CONF_ALIGN_TO_LAMBDA_ID, DIRECTIONS, + LV_DISPLAY_EVENT_MAP, + LV_DISPLAY_EVENT_TRIGGERS, LV_EVENT_MAP, LV_EVENT_TRIGGERS, + LV_SCREEN_EVENT_MAP, + LV_SCREEN_EVENT_TRIGGERS, SWIPE_TRIGGERS, literal, ) @@ -27,6 +35,7 @@ from .lvcode import ( lv, lv_add, lv_event_t_ptr, + lv_expr, lvgl_static, ) from .types import LV_EVENT @@ -46,25 +55,24 @@ async def generate_triggers(): Must be done after all widgets completed """ + all_triggers = ( + LV_EVENT_TRIGGERS + LV_DISPLAY_EVENT_TRIGGERS + LV_SCREEN_EVENT_TRIGGERS + ) for w in widget_map.values(): + config = w.config if isinstance(w.type, LvScrActType): w = get_screen_active(w.var) - if w.config: + if config: for event, conf in { - event: conf - for event, conf in w.config.items() - if event in LV_EVENT_TRIGGERS + event: conf for event, conf in config.items() if event in all_triggers }.items(): conf = conf[0] w.add_flag("LV_OBJ_FLAG_CLICKABLE") - event = literal("LV_EVENT_" + LV_EVENT_MAP[event[3:].upper()]) await add_trigger(conf, w, event) for event, conf in { - event: conf - for event, conf in w.config.items() - if event in SWIPE_TRIGGERS + event: conf for event, conf in config.items() if event in SWIPE_TRIGGERS }.items(): conf = conf[0] dir = event[9:].upper() @@ -74,11 +82,9 @@ async def generate_triggers(): selected = literal( f"lv_indev_get_gesture_dir(lv_indev_active()) == {dir}" ) - await add_trigger( - conf, w, literal("LV_EVENT_GESTURE"), is_selected=selected - ) + await add_trigger(conf, w, "GESTURE", is_selected=selected) - for conf in w.config.get(CONF_ON_VALUE, ()): + for conf in config.get(CONF_ON_VALUE, ()): await add_trigger( conf, w, @@ -87,16 +93,45 @@ async def generate_triggers(): UPDATE_EVENT, ) - await add_on_boot_triggers(w.config.get(CONF_ON_BOOT, ())) + await add_on_boot_triggers(config.get(CONF_ON_BOOT, ())) - # Generate align to directives while we're here - if align_to := w.config.get(CONF_ALIGN_TO): + +async def generate_align_tos(config: dict): + """ + Called once, with a full lvgl configuration to emit deferred align_to actions as a component + that executes after the LVGL setup. This is required since align_to actions are not recalculated on layout changes + and so must be applied after the display is properly laid out. + :param config: + :return: + """ + align_tos = tuple( + w for w in widget_map.values() if w.config and CONF_ALIGN_TO in w.config + ) + if align_tos: + async with LambdaContext(where="align_to") as context: + for w in align_tos: + align_to = w.config[CONF_ALIGN_TO] target = widget_map[align_to[CONF_ID]].obj align = literal(align_to[CONF_ALIGN]) x = align_to[CONF_X] y = align_to[CONF_Y] lv.obj_align_to(w.obj, target, align, x, y) + action_id = config[CONF_ALIGN_TO_LAMBDA_ID] + var = new_Pvariable(action_id, await context.get_lambda()) + await register_component(var, {}) + + +TRIGGER_MAP = LV_EVENT_MAP | LV_DISPLAY_EVENT_MAP | LV_SCREEN_EVENT_MAP +DISPLAY_TRIGGERS = set(LV_DISPLAY_EVENT_TRIGGERS) + + +def _get_event_literal(trigger: str | MockObj) -> MockObj: + if isinstance(trigger, MockObj): + return trigger + trigger = trigger.removeprefix("on_") + return literal("LV_EVENT_" + TRIGGER_MAP[trigger.upper()]) + async def add_trigger(conf, w, *events, is_selected=None): is_selected = is_selected or w.is_selected() @@ -108,4 +143,14 @@ async def add_trigger(conf, w, *events, is_selected=None): async with LambdaContext(EVENT_ARG, where=tid) as context: with LvConditional(is_selected): lv_add(trigger.trigger(*value, literal("event"))) - lv_add(lvgl_static.add_event_cb(w.obj, await context.get_lambda(), *events)) + callback = await context.get_lambda() + event_literals = [_get_event_literal(event) for event in events] + if isinstance(events[0], str) and events[0] in DISPLAY_TRIGGERS: + assert len(events) == 1 + lv.display_add_event_cb( + lv_expr.obj_get_display(w.obj), callback, event_literals[0], nullptr + ) + else: + lv_add( + lvgl_static.add_event_cb(w.obj, await context.get_lambda(), *event_literals) + ) diff --git a/esphome/components/lvgl/types.py b/esphome/components/lvgl/types.py index 8343a542a9..0c8ddfbfbd 100644 --- a/esphome/components/lvgl/types.py +++ b/esphome/components/lvgl/types.py @@ -1,7 +1,7 @@ from esphome import automation, codegen as cg from esphome.const import CONF_TEXT, CONF_VALUE from esphome.cpp_generator import MockObj -from esphome.cpp_types import esphome_ns +from esphome.cpp_types import Component, esphome_ns from .defines import lvgl_ns @@ -51,7 +51,7 @@ IdleTrigger = lvgl_ns.class_("IdleTrigger", automation.Trigger.template()) ObjUpdateAction = lvgl_ns.class_("ObjUpdateAction", automation.Action) LvglCondition = lvgl_ns.class_("LvglCondition", automation.Condition) LvglAction = lvgl_ns.class_("LvglAction", automation.Action) -lv_lambda_t = lvgl_ns.class_("LvLambdaType") +lv_lambda_t = lvgl_ns.class_("LvLambdaComponent", Component) LvCompound = lvgl_ns.class_("LvCompound") lv_font_t = cg.global_ns.class_("lv_font_t") lv_style_t = cg.global_ns.struct("lv_style_t") @@ -69,6 +69,7 @@ lv_page_t = LvType("LvPageType", parents=(LvCompound,)) lv_image_t = LvType("lv_image_t") lv_gradient_t = LvType("lv_grad_dsc_t") lv_event_t = LvType("lv_event_t") +RotationType = lvgl_ns.enum("RotationType") LV_EVENT = MockObj(base="LV_EVENT_", op="") LV_STATE = MockObj(base="LV_STATE_", op="") diff --git a/esphome/components/lvgl/widgets/__init__.py b/esphome/components/lvgl/widgets/__init__.py index b383196963..0ac4062106 100644 --- a/esphome/components/lvgl/widgets/__init__.py +++ b/esphome/components/lvgl/widgets/__init__.py @@ -1,5 +1,4 @@ import sys -from typing import Any from esphome import codegen as cg, config_validation as cv from esphome.automation import register_action @@ -405,7 +404,11 @@ class Widget: # Map of widgets to their config, used for trigger generation -widget_map: dict[Any, Widget] = {} +widget_map: dict[ID, Widget] = {} + + +def is_widget_completed(name: ID) -> bool: + return name in widget_map class LvScrActType(WidgetType): diff --git a/esphome/components/lvgl/widgets/canvas.py b/esphome/components/lvgl/widgets/canvas.py index 0e40d0dfbe..f12766bae1 100644 --- a/esphome/components/lvgl/widgets/canvas.py +++ b/esphome/components/lvgl/widgets/canvas.py @@ -42,7 +42,6 @@ from ..defines import ( CONF_SRC, CONF_START_ANGLE, addr, - get_color_formats, literal, ) from ..lv_validation import ( @@ -99,7 +98,6 @@ class CanvasType(WidgetType): # RGB565 is 16-bit (2 bytes per pixel), ARGB8888 is 32-bit (4 bytes per pixel) if config[CONF_TRANSPARENT]: color_format = "LV_COLOR_FORMAT_ARGB8888" - get_color_formats().add("ARGB8888") else: color_format = "LV_COLOR_FORMAT_NATIVE" diff --git a/esphome/components/lvgl/widgets/keyboard.py b/esphome/components/lvgl/widgets/keyboard.py index d4a71078d0..029ca5f684 100644 --- a/esphome/components/lvgl/widgets/keyboard.py +++ b/esphome/components/lvgl/widgets/keyboard.py @@ -1,12 +1,15 @@ from esphome.components.key_provider import KeyProvider import esphome.config_validation as cv from esphome.const import CONF_ITEMS, CONF_MODE +from esphome.core import CORE from esphome.cpp_types import std_string +from .. import LvContext from ..defines import CONF_MAIN, KEYBOARD_MODES, literal -from ..helpers import add_lv_use, lvgl_components_required +from ..helpers import lvgl_components_required from ..types import LvCompound, LvType -from . import Widget, WidgetType, get_widgets +from . import Widget, WidgetType, get_widgets, is_widget_completed +from .buttonmatrix import CONF_BUTTONMATRIX from .textarea import CONF_TEXTAREA, lv_textarea_t CONF_KEYBOARD = "keyboard" @@ -41,16 +44,27 @@ class KeyboardType(WidgetType): ) def get_uses(self): - return CONF_KEYBOARD, CONF_TEXTAREA + return CONF_KEYBOARD, CONF_TEXTAREA, CONF_BUTTONMATRIX async def to_code(self, w: Widget, config: dict): lvgl_components_required.add("KEY_LISTENER") lvgl_components_required.add(CONF_KEYBOARD) - add_lv_use("btnmatrix") if mode := config.get(CONF_MODE): await w.set_property(CONF_MODE, await KEYBOARD_MODES.process(mode)) - if ta := await get_widgets(config, CONF_TEXTAREA): - await w.set_property(CONF_TEXTAREA, ta[0].obj) + if textarea := config.get(CONF_TEXTAREA): + # If a textarea is configured, it must be generated before the keyboard can attach it. + # If not yet configured, defer the attachment code. + + async def add_textarea(): + async with LvContext(): + await w.set_property( + CONF_TEXTAREA, (await get_widgets(config, CONF_TEXTAREA))[0].obj + ) + + if is_widget_completed(textarea): + await add_textarea() + else: + CORE.add_job(add_textarea) keyboard_spec = KeyboardType() diff --git a/esphome/components/lvgl/widgets/label.py b/esphome/components/lvgl/widgets/label.py index bb5900b8c9..5ac92f2717 100644 --- a/esphome/components/lvgl/widgets/label.py +++ b/esphome/components/lvgl/widgets/label.py @@ -35,7 +35,7 @@ class LabelType(WidgetType): if (value := config.get(CONF_TEXT)) is not None: await w.set_property(CONF_TEXT, await lv_text.process(value)) await w.set_property(CONF_LONG_MODE, config) - await w.set_property(CONF_RECOLOR, config) + await w.set_property(CONF_RECOLOR, config, processor=lv_bool) label_spec = LabelType() diff --git a/esphome/components/lvgl/widgets/line.py b/esphome/components/lvgl/widgets/line.py index a9b202163f..3112cc28d0 100644 --- a/esphome/components/lvgl/widgets/line.py +++ b/esphome/components/lvgl/widgets/line.py @@ -17,11 +17,6 @@ lv_point_t = cg.global_ns.struct("lv_point_t") lv_point_precise_t = cg.global_ns.struct("lv_point_precise_t") -LINE_SCHEMA = { - cv.Required(CONF_POINTS): cv.ensure_list(point_schema), -} - - async def process_coord(coord): if isinstance(coord, Lambda): return call_lambda(await cg.process_lambda(coord, [], return_type=lv_coord_t)) @@ -34,15 +29,17 @@ class LineType(WidgetType): CONF_LINE, LvType("LvLineType", parents=(LvCompound,)), (CONF_MAIN,), - LINE_SCHEMA, + schema={cv.Required(CONF_POINTS): cv.ensure_list(point_schema)}, + modify_schema={cv.Optional(CONF_POINTS): cv.ensure_list(point_schema)}, ) async def to_code(self, w: Widget, config): - points = [ - [await process_coord(p[CONF_X]), await process_coord(p[CONF_Y])] - for p in config[CONF_POINTS] - ] - lv_add(w.var.set_points(points)) + if CONF_POINTS in config: + points = [ + [await process_coord(p[CONF_X]), await process_coord(p[CONF_Y])] + for p in config[CONF_POINTS] + ] + lv_add(w.var.set_points(points)) line_spec = LineType() diff --git a/esphome/components/lvgl/widgets/meter.py b/esphome/components/lvgl/widgets/meter.py index 494f811a8e..ab65a7c47d 100644 --- a/esphome/components/lvgl/widgets/meter.py +++ b/esphome/components/lvgl/widgets/meter.py @@ -56,11 +56,11 @@ from ..lv_validation import ( lv_float, lv_image, lv_int, + lv_positive_int, opacity, padding, pixels, pixels_or_percent, - pixels_or_percent_validator, requires_component, size, ) @@ -88,7 +88,10 @@ CONF_COLOR_START = "color_start" CONF_DRAW_TICKS_ON_TOP = "draw_ticks_on_top" CONF_IMAGE_ID = "image_id" CONF_INDICATORS = "indicators" +CONF_DASH_GAP = "dash_gap" +CONF_DASH_WIDTH = "dash_width" CONF_LINE_ID = "line_id" +CONF_ROUNDED = "rounded" CONF_LABEL_GAP = "label_gap" CONF_MAJOR = "major" CONF_METER = "meter" @@ -135,9 +138,12 @@ INDICATOR_LINE_SCHEMA = cv.Schema( { cv.Optional(CONF_WIDTH, default=4): cv.int_, cv.Optional(CONF_COLOR, default=0): lv_color, + cv.Optional(CONF_ROUNDED, default=True): lv_bool, + cv.Optional(CONF_DASH_GAP): lv_positive_int, + cv.Optional(CONF_DASH_WIDTH): lv_positive_int, cv.Optional(CONF_R_MOD): padding, - cv.Optional(CONF_LENGTH): pixels_or_percent_validator, - cv.Optional(CONF_RADIAL_OFFSET, 0): pixels_or_percent_validator, + cv.Optional(CONF_LENGTH): pixels_or_percent, + cv.Optional(CONF_RADIAL_OFFSET): pixels_or_percent, cv.Optional(CONF_VALUE, default=0.0): lv_float, cv.Optional(CONF_OPA, default=1.0): opacity, } @@ -249,17 +255,17 @@ SCALE_SCHEMA = cv.Schema( { cv.Optional(CONF_COUNT, default=12): cv.int_range(min=2), cv.Optional(CONF_WIDTH, default=2): cv.positive_int, - cv.Optional(CONF_LENGTH, default=10): size, - cv.Optional(CONF_RADIAL_OFFSET, default=0): size, + cv.Optional(CONF_LENGTH, default=10): cv.positive_int, + cv.Optional(CONF_RADIAL_OFFSET): cv.positive_int, cv.Optional(CONF_COLOR, default=0x808080): lv_color, cv.Optional(CONF_MAJOR): cv.Schema( { cv.Optional(CONF_STRIDE, default=3): cv.positive_int, cv.Optional(CONF_WIDTH, default=5): size, - cv.Optional(CONF_LENGTH, default="15%"): size, - cv.Optional(CONF_RADIAL_OFFSET, default=0): size, + cv.Optional(CONF_LENGTH, default=12): cv.positive_int, + cv.Optional(CONF_RADIAL_OFFSET): cv.positive_int, cv.Optional(CONF_COLOR, default=0): lv_color, - cv.Optional(CONF_LABEL_GAP, default=4): size, + cv.Optional(CONF_LABEL_GAP, default=4): cv.int_, } ), } @@ -466,11 +472,15 @@ class MeterType(WidgetType): CONF_OPA: v[CONF_OPA], CONF_LINE_WIDTH: v[CONF_WIDTH], "line_color": v[CONF_COLOR], - "line_rounded": True, + "line_rounded": v[CONF_ROUNDED], CONF_ALIGN: CHILD_ALIGNMENTS.TOP_LEFT, CONF_LENGTH: length, - CONF_RADIAL_OFFSET: v[CONF_RADIAL_OFFSET], } + if radial_offset := v.get(CONF_RADIAL_OFFSET): + props[CONF_RADIAL_OFFSET] = radial_offset + for option in (CONF_DASH_WIDTH, CONF_DASH_GAP): + if option in v: + props["line_" + option] = v[option] lw = await widget_to_code(props, line_indicator_type, scale_var) await set_indicator_values(lw, v) @@ -478,10 +488,8 @@ class MeterType(WidgetType): add_lv_use(CONF_IMAGE) src = v[CONF_SRC] src_data = get_image_metadata(src.id) - pivot_x = await pixels.process(v[CONF_PIVOT_X]) - pivot_y = await pixels.process( - v.get(CONF_PIVOT_Y, src_data.height // 2) - ) + pivot_x = v[CONF_PIVOT_X] + pivot_y = v.get(CONF_PIVOT_Y, src_data.height // 2) props = { CONF_X: src_data.width // 2 - pivot_x, "transform_pivot_x": pivot_x, @@ -511,11 +519,12 @@ class MeterType(WidgetType): lv_obj.set_style_line_width( scale_var, await size.process(ticks[CONF_WIDTH]), LV_PART.ITEMS ) - lv_obj.set_style_radial_offset( - scale_var, - await size.process(ticks[CONF_RADIAL_OFFSET]), - LV_PART.ITEMS, - ) + if radial_offset := ticks.get(CONF_RADIAL_OFFSET): + lv_obj.set_style_radial_offset( + scale_var, + -radial_offset, + LV_PART.ITEMS, + ) lv_obj.set_style_line_color( scale_var, await lv_color.process(ticks[CONF_COLOR]), @@ -536,11 +545,12 @@ class MeterType(WidgetType): await size.process(major[CONF_LENGTH]), LV_PART.INDICATOR, ) - lv_obj.set_style_radial_offset( - scale_var, - await size.process(ticks[CONF_RADIAL_OFFSET]), - LV_PART.INDICATOR, - ) + if radial_offset := major.get(CONF_RADIAL_OFFSET): + lv_obj.set_style_radial_offset( + scale_var, + -radial_offset, + LV_PART.INDICATOR, + ) lv_obj.set_style_line_width( scale_var, await size.process(major[CONF_WIDTH]), @@ -553,12 +563,9 @@ class MeterType(WidgetType): ) # Set label gap (padding) - label_gap = await size.process(major[CONF_LABEL_GAP]) - if isinstance(label_gap, int): - label_gap -= DEFAULT_LABEL_GAP lv_obj.set_style_pad_radial( scale_var, - label_gap, + major[CONF_LABEL_GAP] - DEFAULT_LABEL_GAP, LV_PART.INDICATOR, ) else: diff --git a/esphome/components/lvgl/widgets/msgbox.py b/esphome/components/lvgl/widgets/msgbox.py index af27ee7553..d0e6bfa3a2 100644 --- a/esphome/components/lvgl/widgets/msgbox.py +++ b/esphome/components/lvgl/widgets/msgbox.py @@ -33,6 +33,7 @@ from ..styles import LVStyle from ..types import LV_EVENT, lv_obj_t from . import Widget, WidgetType, add_widgets, set_obj_properties, widget_to_code from .button import button_spec, lv_button_t +from .img import CONF_IMAGE from .label import CONF_LABEL from .obj import obj_spec @@ -41,7 +42,7 @@ CONF_MSGBOX = "msgbox" OUTER_STYLE = LVStyle( "msgbox_outer", { - "bg_opa": 128, + "bg_opa": 0.5, "bg_color": "black", "border_width": 0, "pad_all": 0, @@ -119,6 +120,7 @@ async def msgbox_to_code(top_layer, conf): CONF_BUTTON, CONF_LABEL, CONF_MSGBOX, + CONF_IMAGE, *button_spec.get_uses(), ) if CONF_BUTTON_STYLE in conf: @@ -156,7 +158,7 @@ async def msgbox_to_code(top_layer, conf): with LocalVariable( "close_btn_", lv_obj_t, lv_expr.msgbox_add_close_button(msgbox) ) as close_btn: - lv_obj.remove_event_cb(close_btn, nullptr) + lv_obj.remove_event(close_btn, 0) lv_obj.add_event_cb( close_btn, await close_action.get_lambda(), @@ -170,6 +172,6 @@ async def msgbox_to_code(top_layer, conf): async def msgboxes_to_code(lv_component, config): - top_layer = lv.disp_get_layer_top(lv_component.get_disp()) + top_layer = lv_expr.disp_get_layer_top(lv_component.get_disp()) for conf in config.get(CONF_MSGBOXES, ()): await msgbox_to_code(top_layer, conf) diff --git a/esphome/components/lvgl/widgets/qrcode.py b/esphome/components/lvgl/widgets/qrcode.py index 82c4370543..df76ab6bb0 100644 --- a/esphome/components/lvgl/widgets/qrcode.py +++ b/esphome/components/lvgl/widgets/qrcode.py @@ -2,7 +2,7 @@ import esphome.codegen as cg import esphome.config_validation as cv from esphome.const import CONF_SIZE, CONF_TEXT -from ..defines import CONF_MAIN, get_color_formats +from ..defines import CONF_MAIN from ..lv_validation import color, lv_color, lv_int, lv_text from ..lvcode import LocalVariable, lv from ..schemas import TEXT_SCHEMA @@ -44,7 +44,6 @@ class QrCodeType(WidgetType): return CONF_CANVAS, CONF_IMAGE async def to_code(self, w: Widget, config): - get_color_formats().add("ARGB8888") await w.set_property( CONF_LIGHT_COLOR, await lv_color.process(config.get(CONF_LIGHT_COLOR)) ) diff --git a/esphome/components/lvgl/widgets/tabview.py b/esphome/components/lvgl/widgets/tabview.py index 60ba664f04..7629b03e9d 100644 --- a/esphome/components/lvgl/widgets/tabview.py +++ b/esphome/components/lvgl/widgets/tabview.py @@ -26,7 +26,7 @@ from ..schemas import container_schema, part_schema from ..types import LV_EVENT, LvType, ObjUpdateAction, lv_obj_t, lv_obj_t_ptr from . import Widget, WidgetType, add_widgets, get_widgets, set_obj_properties from .button import button_spec -from .buttonmatrix import buttonmatrix_spec +from .buttonmatrix import CONF_BUTTONMATRIX, buttonmatrix_spec from .obj import obj_spec CONF_TABVIEW = "tabview" @@ -73,7 +73,7 @@ class TabviewType(WidgetType): ) def get_uses(self): - return "btnmatrix", TYPE_FLEX + return CONF_BUTTONMATRIX, TYPE_FLEX async def to_code(self, w: Widget, config: dict): await w.set_property( diff --git a/esphome/components/lvgl/widgets/tileview.py b/esphome/components/lvgl/widgets/tileview.py index 8e9d95f349..4657d628de 100644 --- a/esphome/components/lvgl/widgets/tileview.py +++ b/esphome/components/lvgl/widgets/tileview.py @@ -129,6 +129,6 @@ async def tileview_select(config, action_id, template_arg, args): lv.tileview_set_tile_by_index( widgets[0].obj, column, row, literal(config[CONF_ANIMATED]) ) - lv.event_send(w.obj, LV_EVENT.VALUE_CHANGED, cg.nullptr) + lv_obj.send_event(w.obj, LV_EVENT.VALUE_CHANGED, cg.nullptr) return await action_to_code(widgets, do_select, action_id, template_arg, args) diff --git a/esphome/components/max31855/sensor.py b/esphome/components/max31855/sensor.py index 93e48beee0..35ae28d04c 100644 --- a/esphome/components/max31855/sensor.py +++ b/esphome/components/max31855/sensor.py @@ -19,6 +19,7 @@ CONFIG_SCHEMA = ( unit_of_measurement=UNIT_CELSIUS, accuracy_decimals=1, device_class=DEVICE_CLASS_TEMPERATURE, + state_class=STATE_CLASS_MEASUREMENT, ) .extend( { diff --git a/esphome/components/max44009/max44009.cpp b/esphome/components/max44009/max44009.cpp index 8b8e38c1ea..cbce053519 100644 --- a/esphome/components/max44009/max44009.cpp +++ b/esphome/components/max44009/max44009.cpp @@ -8,17 +8,17 @@ namespace max44009 { static const char *const TAG = "max44009.sensor"; // REGISTERS -static const uint8_t MAX44009_REGISTER_CONFIGURATION = 0x02; -static const uint8_t MAX44009_LUX_READING_HIGH = 0x03; -static const uint8_t MAX44009_LUX_READING_LOW = 0x04; +static constexpr uint8_t MAX44009_REGISTER_CONFIGURATION = 0x02; +static constexpr uint8_t MAX44009_LUX_READING_HIGH = 0x03; +static constexpr uint8_t MAX44009_LUX_READING_LOW = 0x04; // CONFIGURATION MASKS -static const uint8_t MAX44009_CFG_CONTINUOUS = 0x80; +static constexpr uint8_t MAX44009_CFG_CONTINUOUS = 0x80; // ERROR CODES -static const uint8_t MAX44009_OK = 0; -static const uint8_t MAX44009_ERROR_WIRE_REQUEST = -10; -static const uint8_t MAX44009_ERROR_OVERFLOW = -20; -static const uint8_t MAX44009_ERROR_HIGH_BYTE = -30; -static const uint8_t MAX44009_ERROR_LOW_BYTE = -31; +static constexpr int8_t MAX44009_OK = 0; +static constexpr int8_t MAX44009_ERROR_WIRE_REQUEST = -10; +static constexpr int8_t MAX44009_ERROR_OVERFLOW = -20; +static constexpr int8_t MAX44009_ERROR_HIGH_BYTE = -30; +static constexpr int8_t MAX44009_ERROR_LOW_BYTE = -31; void MAX44009Sensor::setup() { bool state_ok = false; diff --git a/esphome/components/max44009/max44009.h b/esphome/components/max44009/max44009.h index 59eea66ed9..d0ffd7bc70 100644 --- a/esphome/components/max44009/max44009.h +++ b/esphome/components/max44009/max44009.h @@ -28,8 +28,8 @@ class MAX44009Sensor : public sensor::Sensor, public PollingComponent, public i2 uint8_t read_(uint8_t reg); void write_(uint8_t reg, uint8_t value); - int error_; - MAX44009Mode mode_; + int8_t error_{0}; + MAX44009Mode mode_{MAX44009_MODE_AUTO}; }; } // namespace max44009 diff --git a/esphome/components/max6956/__init__.py b/esphome/components/max6956/__init__.py index be6390fc17..e9fae4cceb 100644 --- a/esphome/components/max6956/__init__.py +++ b/esphome/components/max6956/__init__.py @@ -117,7 +117,7 @@ async def max6956_pin_to_code(config): async def max6956_set_brightness_global_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_BRIGHTNESS_GLOBAL], args, float) + template_ = await cg.templatable(config[CONF_BRIGHTNESS_GLOBAL], args, cg.uint8) cg.add(var.set_brightness_global(template_)) return var @@ -139,6 +139,8 @@ async def max6956_set_brightness_global_to_code(config, action_id, template_arg, async def max6956_set_brightness_mode_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_BRIGHTNESS_MODE], args, float) + template_ = await cg.templatable( + config[CONF_BRIGHTNESS_MODE], args, MAX6956_CURRENTMODE + ) cg.add(var.set_brightness_mode(template_)) return var diff --git a/esphome/components/max7219digit/max7219digit.cpp b/esphome/components/max7219digit/max7219digit.cpp index cdceafad50..f9b46cf797 100644 --- a/esphome/components/max7219digit/max7219digit.cpp +++ b/esphome/components/max7219digit/max7219digit.cpp @@ -6,6 +6,7 @@ #include "max7219font.h" #include +#include namespace esphome { namespace max7219digit { @@ -92,7 +93,9 @@ void MAX7219Component::loop() { if (this->scroll_mode_ == ScrollMode::STOP) { if (static_cast(this->stepsleft_ + get_width_internal()) == first_line_size + 1) { if (millis_since_last_scroll < this->scroll_dwell_) { - ESP_LOGVV(TAG, "Dwell time at end of string in case of stop at end. Step %d, since last scroll %d, dwell %d.", + ESP_LOGVV(TAG, + "Dwell time at end of string in case of stop at end. Step %d, since last scroll %" PRIu32 + ", dwell %d.", this->stepsleft_, millis_since_last_scroll, this->scroll_dwell_); return; } diff --git a/esphome/components/mcp23008/mcp23008.cpp b/esphome/components/mcp23008/mcp23008.cpp index 0c34e4971a..5f73e03f6f 100644 --- a/esphome/components/mcp23008/mcp23008.cpp +++ b/esphome/components/mcp23008/mcp23008.cpp @@ -6,6 +6,8 @@ namespace mcp23008 { static const char *const TAG = "mcp23008"; +static constexpr uint8_t IOCON_ODR = 0x04; // Open-drain output for INT pin + void MCP23008::setup() { uint8_t iocon; if (!this->read_reg(mcp23x08_base::MCP23X08_IOCON, &iocon)) { @@ -18,11 +20,16 @@ void MCP23008::setup() { if (this->open_drain_ints_) { // enable open-drain interrupt pins, 3.3V-safe - this->write_reg(mcp23x08_base::MCP23X08_IOCON, 0x04); + this->write_reg(mcp23x08_base::MCP23X08_IOCON, iocon | IOCON_ODR); } + + this->setup_interrupt_pin_(); } -void MCP23008::dump_config() { ESP_LOGCONFIG(TAG, "MCP23008:"); } +void MCP23008::dump_config() { + ESP_LOGCONFIG(TAG, "MCP23008:"); + LOG_PIN(" Interrupt Pin: ", this->interrupt_pin_); +} bool MCP23008::read_reg(uint8_t reg, uint8_t *value) { if (this->is_failed()) diff --git a/esphome/components/mcp23017/mcp23017.cpp b/esphome/components/mcp23017/mcp23017.cpp index 1ad2036939..212c15ccf2 100644 --- a/esphome/components/mcp23017/mcp23017.cpp +++ b/esphome/components/mcp23017/mcp23017.cpp @@ -6,6 +6,9 @@ namespace mcp23017 { static const char *const TAG = "mcp23017"; +static constexpr uint8_t IOCON_MIRROR = 0x40; // Mirror INTA/INTB pins +static constexpr uint8_t IOCON_ODR = 0x04; // Open-drain output for INT pin + void MCP23017::setup() { uint8_t iocon; if (!this->read_reg(mcp23x17_base::MCP23X17_IOCONA, &iocon)) { @@ -17,14 +20,26 @@ void MCP23017::setup() { this->read_reg(mcp23x17_base::MCP23X17_OLATA, &this->olat_a_); this->read_reg(mcp23x17_base::MCP23X17_OLATB, &this->olat_b_); + uint8_t iocon_flags = 0; if (this->open_drain_ints_) { - // enable open-drain interrupt pins, 3.3V-safe - this->write_reg(mcp23x17_base::MCP23X17_IOCONA, 0x04); - this->write_reg(mcp23x17_base::MCP23X17_IOCONB, 0x04); + iocon_flags |= IOCON_ODR; } + if (this->interrupt_pin_ != nullptr) { + // Mirror INTA/INTB so either pin fires for changes on any port + iocon_flags |= IOCON_MIRROR; + } + if (iocon_flags != 0) { + this->write_reg(mcp23x17_base::MCP23X17_IOCONA, iocon | iocon_flags); + this->write_reg(mcp23x17_base::MCP23X17_IOCONB, iocon | iocon_flags); + } + + this->setup_interrupt_pin_(); } -void MCP23017::dump_config() { ESP_LOGCONFIG(TAG, "MCP23017:"); } +void MCP23017::dump_config() { + ESP_LOGCONFIG(TAG, "MCP23017:"); + LOG_PIN(" Interrupt Pin: ", this->interrupt_pin_); +} bool MCP23017::read_reg(uint8_t reg, uint8_t *value) { if (this->is_failed()) diff --git a/esphome/components/mcp23s08/__init__.py b/esphome/components/mcp23s08/__init__.py index 3d4e304f9b..312da79b75 100644 --- a/esphome/components/mcp23s08/__init__.py +++ b/esphome/components/mcp23s08/__init__.py @@ -18,7 +18,7 @@ CONFIG_SCHEMA = ( cv.Schema( { cv.Required(CONF_ID): cv.declare_id(mcp23S08), - cv.Optional(CONF_DEVICEADDRESS, default=0): cv.uint8_t, + cv.Optional(CONF_DEVICEADDRESS, default=0): cv.int_range(min=0, max=3), } ) .extend(mcp23xxx_base.MCP23XXX_CONFIG_SCHEMA) diff --git a/esphome/components/mcp23s08/mcp23s08.cpp b/esphome/components/mcp23s08/mcp23s08.cpp index 3d944b45d5..983c1aa600 100644 --- a/esphome/components/mcp23s08/mcp23s08.cpp +++ b/esphome/components/mcp23s08/mcp23s08.cpp @@ -6,6 +6,11 @@ namespace mcp23s08 { static const char *const TAG = "mcp23s08"; +// IOCON register bits +static constexpr uint8_t IOCON_SEQOP = 0x20; // Sequential operation mode +static constexpr uint8_t IOCON_HAEN = 0x08; // Hardware address enable +static constexpr uint8_t IOCON_ODR = 0x04; // Open-drain output for INT pin + void MCP23S08::set_device_address(uint8_t device_addr) { if (device_addr != 0) { this->device_opcode_ |= ((device_addr & 0x03) << 1); @@ -15,25 +20,28 @@ void MCP23S08::set_device_address(uint8_t device_addr) { void MCP23S08::setup() { this->spi_setup(); + // Enable HAEN (broadcast to all chips since HAEN isn't active yet) this->enable(); - uint8_t cmd = 0b01000000; - this->transfer_byte(cmd); + this->transfer_byte(0b01000000); this->transfer_byte(mcp23x08_base::MCP23X08_IOCON); - this->transfer_byte(0b00011000); // Enable HAEN pins for addressing + this->transfer_byte(IOCON_SEQOP | IOCON_HAEN); this->disable(); // Read current output register state this->read_reg(mcp23x08_base::MCP23X08_OLAT, &this->olat_); if (this->open_drain_ints_) { - // enable open-drain interrupt pins, 3.3V-safe - this->write_reg(mcp23x08_base::MCP23X08_IOCON, 0x04); + // enable open-drain interrupt pins, 3.3V-safe (addressed, only this chip) + this->write_reg(mcp23x08_base::MCP23X08_IOCON, IOCON_SEQOP | IOCON_HAEN | IOCON_ODR); } + + this->setup_interrupt_pin_(); } void MCP23S08::dump_config() { ESP_LOGCONFIG(TAG, "MCP23S08:"); LOG_PIN(" CS Pin: ", this->cs_); + LOG_PIN(" Interrupt Pin: ", this->interrupt_pin_); } bool MCP23S08::read_reg(uint8_t reg, uint8_t *value) { diff --git a/esphome/components/mcp23s17/__init__.py b/esphome/components/mcp23s17/__init__.py index ea8433af2e..599bfa0851 100644 --- a/esphome/components/mcp23s17/__init__.py +++ b/esphome/components/mcp23s17/__init__.py @@ -18,7 +18,7 @@ CONFIG_SCHEMA = ( cv.Schema( { cv.Required(CONF_ID): cv.declare_id(mcp23S17), - cv.Optional(CONF_DEVICEADDRESS, default=0): cv.uint8_t, + cv.Optional(CONF_DEVICEADDRESS, default=0): cv.int_range(min=0, max=7), } ) .extend(mcp23xxx_base.MCP23XXX_CONFIG_SCHEMA) diff --git a/esphome/components/mcp23s17/mcp23s17.cpp b/esphome/components/mcp23s17/mcp23s17.cpp index 1624eda9e4..db9a34e230 100644 --- a/esphome/components/mcp23s17/mcp23s17.cpp +++ b/esphome/components/mcp23s17/mcp23s17.cpp @@ -6,6 +6,12 @@ namespace mcp23s17 { static const char *const TAG = "mcp23s17"; +// IOCON register bits +static constexpr uint8_t IOCON_SEQOP = 0x20; // Sequential operation mode +static constexpr uint8_t IOCON_MIRROR = 0x40; // Mirror INTA/INTB pins +static constexpr uint8_t IOCON_HAEN = 0x08; // Hardware address enable +static constexpr uint8_t IOCON_ODR = 0x04; // Open-drain output for INT pin + void MCP23S17::set_device_address(uint8_t device_addr) { if (device_addr != 0) { this->device_opcode_ |= ((device_addr & 0b111) << 1); @@ -15,34 +21,43 @@ void MCP23S17::set_device_address(uint8_t device_addr) { void MCP23S17::setup() { this->spi_setup(); + // Enable HAEN (broadcast to addresses 0 and 4 since HAEN isn't active yet) this->enable(); - uint8_t cmd = 0b01000000; - this->transfer_byte(cmd); + this->transfer_byte(0b01000000); this->transfer_byte(mcp23x17_base::MCP23X17_IOCONA); - this->transfer_byte(0b00011000); // Enable HAEN pins for addressing + this->transfer_byte(IOCON_SEQOP | IOCON_HAEN); this->disable(); this->enable(); - cmd = 0b01001000; - this->transfer_byte(cmd); + this->transfer_byte(0b01001000); this->transfer_byte(mcp23x17_base::MCP23X17_IOCONA); - this->transfer_byte(0b00011000); // Enable HAEN pins for addressing + this->transfer_byte(IOCON_SEQOP | IOCON_HAEN); this->disable(); // Read current output register state this->read_reg(mcp23x17_base::MCP23X17_OLATA, &this->olat_a_); this->read_reg(mcp23x17_base::MCP23X17_OLATB, &this->olat_b_); + uint8_t iocon_flags = IOCON_SEQOP | IOCON_HAEN; if (this->open_drain_ints_) { - // enable open-drain interrupt pins, 3.3V-safe - this->write_reg(mcp23x17_base::MCP23X17_IOCONA, 0x04); - this->write_reg(mcp23x17_base::MCP23X17_IOCONB, 0x04); + iocon_flags |= IOCON_ODR; } + if (this->interrupt_pin_ != nullptr) { + // Mirror INTA/INTB so either pin fires for changes on any port + iocon_flags |= IOCON_MIRROR; + } + if (this->open_drain_ints_ || this->interrupt_pin_ != nullptr) { + this->write_reg(mcp23x17_base::MCP23X17_IOCONA, iocon_flags); + this->write_reg(mcp23x17_base::MCP23X17_IOCONB, iocon_flags); + } + + this->setup_interrupt_pin_(); } void MCP23S17::dump_config() { ESP_LOGCONFIG(TAG, "MCP23S17:"); LOG_PIN(" CS Pin: ", this->cs_); + LOG_PIN(" Interrupt Pin: ", this->interrupt_pin_); } bool MCP23S17::read_reg(uint8_t reg, uint8_t *value) { diff --git a/esphome/components/mcp23x08_base/mcp23x08_base.cpp b/esphome/components/mcp23x08_base/mcp23x08_base.cpp index 92228be62c..197b739204 100644 --- a/esphome/components/mcp23x08_base/mcp23x08_base.cpp +++ b/esphome/components/mcp23x08_base/mcp23x08_base.cpp @@ -32,6 +32,16 @@ void MCP23X08Base::pin_mode(uint8_t pin, gpio::Flags flags) { } else if (flags == gpio::FLAG_OUTPUT) { this->update_reg(pin, false, iodir); } + // When interrupt_pin is configured, auto-enable CHANGE interrupt for input pins + // so the chip's INT output fires on any input state change + if (this->interrupt_pin_ != nullptr && (flags & gpio::FLAG_INPUT)) { + this->pin_interrupt_mode(pin, mcp23xxx_base::MCP23XXX_CHANGE); + } + // Enable polling loop for input pins (not needed for interrupt-driven mode + // where the ISR handles re-enabling loop) + if (this->interrupt_pin_ == nullptr && (flags & gpio::FLAG_INPUT)) { + this->enable_loop(); + } } void MCP23X08Base::pin_interrupt_mode(uint8_t pin, mcp23xxx_base::MCP23XXXInterruptMode interrupt_mode) { diff --git a/esphome/components/mcp23x17_base/mcp23x17_base.cpp b/esphome/components/mcp23x17_base/mcp23x17_base.cpp index 6f95ee98fd..efed7f5f17 100644 --- a/esphome/components/mcp23x17_base/mcp23x17_base.cpp +++ b/esphome/components/mcp23x17_base/mcp23x17_base.cpp @@ -44,6 +44,16 @@ void MCP23X17Base::pin_mode(uint8_t pin, gpio::Flags flags) { } else if (flags == gpio::FLAG_OUTPUT) { this->update_reg(pin, false, iodir); } + // When interrupt_pin is configured, auto-enable CHANGE interrupt for input pins + // so the chip's INT output fires on any input state change + if (this->interrupt_pin_ != nullptr && (flags & gpio::FLAG_INPUT)) { + this->pin_interrupt_mode(pin, mcp23xxx_base::MCP23XXX_CHANGE); + } + // Enable polling loop for input pins (not needed for interrupt-driven mode + // where the ISR handles re-enabling loop) + if (this->interrupt_pin_ == nullptr && (flags & gpio::FLAG_INPUT)) { + this->enable_loop(); + } } void MCP23X17Base::pin_interrupt_mode(uint8_t pin, mcp23xxx_base::MCP23XXXInterruptMode interrupt_mode) { diff --git a/esphome/components/mcp23xxx_base/__init__.py b/esphome/components/mcp23xxx_base/__init__.py index d6e82101ad..cd952099c0 100644 --- a/esphome/components/mcp23xxx_base/__init__.py +++ b/esphome/components/mcp23xxx_base/__init__.py @@ -5,6 +5,7 @@ from esphome.const import ( CONF_ID, CONF_INPUT, CONF_INTERRUPT, + CONF_INTERRUPT_PIN, CONF_INVERTED, CONF_MODE, CONF_NUMBER, @@ -32,6 +33,7 @@ MCP23XXX_INTERRUPT_MODES = { MCP23XXX_CONFIG_SCHEMA = cv.Schema( { cv.Optional(CONF_OPEN_DRAIN_INTERRUPT, default=False): cv.boolean, + cv.Optional(CONF_INTERRUPT_PIN): pins.internal_gpio_input_pin_schema, } ).extend(cv.COMPONENT_SCHEMA) @@ -43,6 +45,8 @@ async def register_mcp23xxx(config, num_pins): await cg.register_component(var, config) CORE.data.setdefault(CONF_MCP23XXX, {})[id.id] = num_pins cg.add(var.set_open_drain_ints(config[CONF_OPEN_DRAIN_INTERRUPT])) + if interrupt_pin := config.get(CONF_INTERRUPT_PIN): + cg.add(var.set_interrupt_pin(await cg.gpio_pin_expression(interrupt_pin))) return var diff --git a/esphome/components/mcp23xxx_base/mcp23xxx_base.cpp b/esphome/components/mcp23xxx_base/mcp23xxx_base.cpp index 535119fc5c..4c1daac562 100644 --- a/esphome/components/mcp23xxx_base/mcp23xxx_base.cpp +++ b/esphome/components/mcp23xxx_base/mcp23xxx_base.cpp @@ -7,7 +7,12 @@ namespace mcp23xxx_base { template void MCP23XXXGPIOPin::setup() { this->pin_mode(flags_); - this->parent_->pin_interrupt_mode(this->pin_, this->interrupt_mode_); + // When interrupt_pin is configured, pin_mode() already auto-enables CHANGE + // interrupt for input pins, so skip the explicit call if the user didn't + // override the default (NO_INTERRUPT) + if (this->interrupt_mode_ != MCP23XXX_NO_INTERRUPT || this->parent_->get_interrupt_pin() == nullptr) { + this->parent_->pin_interrupt_mode(this->pin_, this->interrupt_mode_); + } } template void MCP23XXXGPIOPin::pin_mode(gpio::Flags flags) { this->parent_->pin_mode(this->pin_, flags); } template bool MCP23XXXGPIOPin::digital_read() { diff --git a/esphome/components/mcp23xxx_base/mcp23xxx_base.h b/esphome/components/mcp23xxx_base/mcp23xxx_base.h index fb992466d5..6efd04e246 100644 --- a/esphome/components/mcp23xxx_base/mcp23xxx_base.h +++ b/esphome/components/mcp23xxx_base/mcp23xxx_base.h @@ -15,11 +15,34 @@ template class MCP23XXXBase : public Component, public gpio_expander: virtual void pin_interrupt_mode(uint8_t pin, MCP23XXXInterruptMode interrupt_mode); void set_open_drain_ints(const bool value) { this->open_drain_ints_ = value; } + void set_interrupt_pin(InternalGPIOPin *pin) { this->interrupt_pin_ = pin; } + InternalGPIOPin *get_interrupt_pin() const { return this->interrupt_pin_; } float get_setup_priority() const override { return setup_priority::IO; } - void loop() override { this->reset_pin_cache_(); } + void loop() override { + this->reset_pin_cache_(); + if (this->interrupt_pin_ != nullptr) { + this->disable_loop(); + } + } protected: + // No need to clear latched interrupts before attaching the ISR — if INT is + // already low the ISR fires immediately, loop runs, cache invalidates, and + // the GPIO read clears the latch. One harmless extra read at most. + void setup_interrupt_pin_() { + if (this->interrupt_pin_ != nullptr) { + this->interrupt_pin_->setup(); + this->interrupt_pin_->attach_interrupt(&MCP23XXXBase::gpio_intr, this, gpio::INTERRUPT_FALLING_EDGE); + this->set_invalidate_on_read_(false); + } + // Disable loop until an input pin is configured via pin_mode() + // For interrupt-driven mode, loop is re-enabled by the ISR + // For polling mode, loop is re-enabled when pin_mode() registers an input pin + this->disable_loop(); + } + static void IRAM_ATTR gpio_intr(MCP23XXXBase *arg) { arg->enable_loop_soon_any_context(); } + // read a given register virtual bool read_reg(uint8_t reg, uint8_t *value) = 0; // write a value to a given register @@ -28,6 +51,7 @@ template class MCP23XXXBase : public Component, public gpio_expander: virtual void update_reg(uint8_t pin, bool pin_value, uint8_t reg_a) = 0; bool open_drain_ints_; + InternalGPIOPin *interrupt_pin_{nullptr}; }; template class MCP23XXXGPIOPin : public GPIOPin { diff --git a/esphome/components/media_player/__init__.py b/esphome/components/media_player/__init__.py index a5baca2994..842f620dae 100644 --- a/esphome/components/media_player/__init__.py +++ b/esphome/components/media_player/__init__.py @@ -9,7 +9,6 @@ from esphome.const import ( CONF_ON_STATE, CONF_ON_TURN_OFF, CONF_ON_TURN_ON, - CONF_TRIGGER_ID, CONF_VOLUME, ) from esphome.core import CORE @@ -65,15 +64,19 @@ _COMMAND_ACTIONS = [ "clear_playlist", ] -# State triggers: (config_key, C++ class name) +StateAnyForwarder = media_player_ns.class_("StateAnyForwarder") +StateEnterForwarder = media_player_ns.class_("StateEnterForwarder") +MediaPlayerState = media_player_ns.enum("MediaPlayerState") + +# State triggers: (config_key, state enum or None for any-state) _STATE_TRIGGERS = [ - (CONF_ON_STATE, "StateTrigger"), - (CONF_ON_IDLE, "IdleTrigger"), - (CONF_ON_PLAY, "PlayTrigger"), - (CONF_ON_PAUSE, "PauseTrigger"), - (CONF_ON_ANNOUNCEMENT, "AnnouncementTrigger"), - (CONF_ON_TURN_ON, "OnTrigger"), - (CONF_ON_TURN_OFF, "OffTrigger"), + (CONF_ON_STATE, None), + (CONF_ON_IDLE, MediaPlayerState.MEDIA_PLAYER_STATE_IDLE), + (CONF_ON_PLAY, MediaPlayerState.MEDIA_PLAYER_STATE_PLAYING), + (CONF_ON_PAUSE, MediaPlayerState.MEDIA_PLAYER_STATE_PAUSED), + (CONF_ON_ANNOUNCEMENT, MediaPlayerState.MEDIA_PLAYER_STATE_ANNOUNCING), + (CONF_ON_TURN_ON, MediaPlayerState.MEDIA_PLAYER_STATE_ON), + (CONF_ON_TURN_OFF, MediaPlayerState.MEDIA_PLAYER_STATE_OFF), ] # State conditions that all share the same schema and codegen handler @@ -91,6 +94,9 @@ _STATE_CONDITIONS = [ PlayMediaAction = media_player_ns.class_( "PlayMediaAction", automation.Action, cg.Parented.template(MediaPlayer) ) +EnqueueMediaAction = media_player_ns.class_( + "EnqueueMediaAction", automation.Action, cg.Parented.template(MediaPlayer) +) VolumeSetAction = media_player_ns.class_( "VolumeSetAction", automation.Action, cg.Parented.template(MediaPlayer) ) @@ -98,10 +104,15 @@ VolumeSetAction = media_player_ns.class_( @setup_entity("media_player") async def setup_media_player_core_(var, config): - for conf_key, _ in _STATE_TRIGGERS: + for conf_key, state_enum in _STATE_TRIGGERS: for conf in config.get(conf_key, []): - trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) - await automation.build_automation(trigger, [], conf) + if state_enum is None: + forwarder = StateAnyForwarder + else: + forwarder = StateEnterForwarder.template(state_enum) + await automation.build_callback_automation( + var, "add_on_state_callback", [], conf, forwarder=forwarder + ) async def register_media_player(var, config): @@ -120,14 +131,8 @@ async def new_media_player(config, *args): _MEDIA_PLAYER_SCHEMA = cv.ENTITY_BASE_SCHEMA.extend( { - cv.Optional(conf_key): automation.validate_automation( - { - cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id( - media_player_ns.class_(class_name, automation.Trigger.template()) - ), - } - ) - for conf_key, class_name in _STATE_TRIGGERS + cv.Optional(conf_key): automation.validate_automation({}) + for conf_key, _ in _STATE_TRIGGERS } ) @@ -166,20 +171,17 @@ MEDIA_PLAYER_CONDITION_SCHEMA = automation.maybe_simple_id( ) -@automation.register_action( - "media_player.play_media", - PlayMediaAction, - cv.maybe_simple_value( - { - cv.GenerateID(): cv.use_id(MediaPlayer), - cv.Required(CONF_MEDIA_URL): cv.templatable(cv.url), - cv.Optional(CONF_ANNOUNCEMENT, default=False): cv.templatable(cv.boolean), - }, - key=CONF_MEDIA_URL, - ), - synchronous=True, +_MEDIA_URL_ACTION_SCHEMA = cv.maybe_simple_value( + { + cv.GenerateID(): cv.use_id(MediaPlayer), + cv.Required(CONF_MEDIA_URL): cv.templatable(cv.url), + cv.Optional(CONF_ANNOUNCEMENT, default=False): cv.templatable(cv.boolean), + }, + key=CONF_MEDIA_URL, ) -async def media_player_play_media_action(config, action_id, template_arg, args): + + +async def _media_action_handler(config, action_id, template_arg, args): var = cg.new_Pvariable(action_id, template_arg) await cg.register_parented(var, config[CONF_ID]) media_url = await cg.templatable(config[CONF_MEDIA_URL], args, cg.std_string) @@ -189,6 +191,21 @@ async def media_player_play_media_action(config, action_id, template_arg, args): return var +automation.register_action( + "media_player.play_media", + PlayMediaAction, + _MEDIA_URL_ACTION_SCHEMA, + synchronous=True, +)(_media_action_handler) + +automation.register_action( + "media_player.enqueue", + EnqueueMediaAction, + _MEDIA_URL_ACTION_SCHEMA, + synchronous=True, +)(_media_action_handler) + + def _snake_to_camel(name): return "".join(word.capitalize() for word in name.split("_")) diff --git a/esphome/components/media_player/automation.h b/esphome/components/media_player/automation.h index 031f6657f4..14ce3c6aed 100644 --- a/esphome/components/media_player/automation.h +++ b/esphome/components/media_player/automation.h @@ -55,48 +55,49 @@ using GroupJoinAction = MediaPlayerCommandAction using ClearPlaylistAction = MediaPlayerCommandAction; -template class PlayMediaAction : public Action, public Parented { +template +class MediaPlayerMediaAction : public Action, public Parented { TEMPLATABLE_VALUE(std::string, media_url) TEMPLATABLE_VALUE(bool, announcement) void play(const Ts &...x) override { - this->parent_->make_call() - .set_media_url(this->media_url_.value(x...)) - .set_announcement(this->announcement_.value(x...)) - .perform(); + auto call = this->parent_->make_call(); + if constexpr (Command != MediaPlayerCommand::MEDIA_PLAYER_COMMAND_PLAY) + call.set_command(Command); + call.set_media_url(this->media_url_.value(x...)).set_announcement(this->announcement_.value(x...)).perform(); } }; +template +using PlayMediaAction = MediaPlayerMediaAction; +template +using EnqueueMediaAction = MediaPlayerMediaAction; + template class VolumeSetAction : public Action, public Parented { TEMPLATABLE_VALUE(float, volume) void play(const Ts &...x) override { this->parent_->make_call().set_volume(this->volume_.value(x...)).perform(); } }; -class StateTrigger : public Trigger<> { - public: - explicit StateTrigger(MediaPlayer *player) { - player->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()(MediaPlayerState /*state*/) const { this->automation->trigger(); } +}; + +/// Callback forwarder that triggers an Automation<> only when a specific media player state is entered. +/// Pointer-sized (single Automation* field) to fit inline in Callback::ctx_. +template struct StateEnterForwarder { + Automation<> *automation; + void operator()(MediaPlayerState state) const { + if (state == State) + this->automation->trigger(); } }; -template class MediaPlayerStateTrigger : public Trigger<> { - public: - explicit MediaPlayerStateTrigger(MediaPlayer *player) : player_(player) { - player->add_on_state_callback([this]() { - if (this->player_->state == State) - this->trigger(); - }); - } - - protected: - MediaPlayer *player_; -}; - -using IdleTrigger = MediaPlayerStateTrigger; -using PlayTrigger = MediaPlayerStateTrigger; -using PauseTrigger = MediaPlayerStateTrigger; -using AnnouncementTrigger = MediaPlayerStateTrigger; -using OnTrigger = MediaPlayerStateTrigger; -using OffTrigger = MediaPlayerStateTrigger; +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 IsIdleCondition : public Condition, public Parented { public: diff --git a/esphome/components/media_player/media_player.cpp b/esphome/components/media_player/media_player.cpp index a0eb7b5500..48d23fa0b1 100644 --- a/esphome/components/media_player/media_player.cpp +++ b/esphome/components/media_player/media_player.cpp @@ -199,7 +199,7 @@ MediaPlayerCall &MediaPlayerCall::set_announcement(bool announce) { } void MediaPlayer::publish_state() { - this->state_callback_.call(); + this->state_callback_.call(this->state); #if defined(USE_MEDIA_PLAYER) && defined(USE_CONTROLLER_REGISTRY) ControllerRegistry::notify_media_player_update(this); #endif diff --git a/esphome/components/media_player/media_player.h b/esphome/components/media_player/media_player.h index 26eca469e7..d5d0020797 100644 --- a/esphome/components/media_player/media_player.h +++ b/esphome/components/media_player/media_player.h @@ -168,7 +168,7 @@ class MediaPlayer : public EntityBase { virtual void control(const MediaPlayerCall &call) = 0; - LazyCallbackManager state_callback_{}; + LazyCallbackManager state_callback_{}; }; } // namespace media_player diff --git a/esphome/components/micro_wake_word/__init__.py b/esphome/components/micro_wake_word/__init__.py index 372eb4c3b0..ff27dec6df 100644 --- a/esphome/components/micro_wake_word/__init__.py +++ b/esphome/components/micro_wake_word/__init__.py @@ -7,7 +7,7 @@ from urllib.parse import urljoin from esphome import automation, external_files, git from esphome.automation import register_action, register_condition import esphome.codegen as cg -from esphome.components import esp32, microphone, ota, socket +from esphome.components import esp32, microphone, ota import esphome.config_validation as cv from esphome.const import ( CONF_FILE, @@ -32,7 +32,7 @@ _LOGGER = logging.getLogger(__name__) CODEOWNERS = ["@kahrendt", "@jesserockz"] DEPENDENCIES = ["microphone"] -AUTO_LOAD = ["socket"] + DOMAIN = "micro_wake_word" @@ -405,7 +405,7 @@ def _model_config_to_manifest_data(model_config): file = _compute_local_file_path(model_config) / "manifest.json" else: - raise ValueError("Unsupported config type: {model_config[CONF_TYPE]}") + raise ValueError(f"Unsupported config type: {model_config[CONF_TYPE]}") return _load_model_data(file) @@ -444,10 +444,6 @@ async def to_code(config): var = cg.new_Pvariable(config[CONF_ID]) await cg.register_component(var, config) - # Enable wake_loop_threadsafe() for low-latency wake word detection - # The inference task queues detection events that need immediate processing - socket.require_wake_loop_threadsafe() - mic_source = await microphone.microphone_source_to_code(config[CONF_MICROPHONE]) cg.add(var.set_microphone_source(mic_source)) diff --git a/esphome/components/micro_wake_word/micro_wake_word.cpp b/esphome/components/micro_wake_word/micro_wake_word.cpp index b93bf1b556..f1aac875f1 100644 --- a/esphome/components/micro_wake_word/micro_wake_word.cpp +++ b/esphome/components/micro_wake_word/micro_wake_word.cpp @@ -431,9 +431,7 @@ void MicroWakeWord::process_probabilities_() { xQueueSend(this->detection_queue_, &wake_word_state, portMAX_DELAY); // Wake main loop immediately to process wake word detection -#if defined(USE_SOCKET_SELECT_SUPPORT) && defined(USE_WAKE_LOOP_THREADSAFE) App.wake_loop_threadsafe(); -#endif model->reset_probabilities(); #ifdef USE_MICRO_WAKE_WORD_VAD diff --git a/esphome/components/mipi/__init__.py b/esphome/components/mipi/__init__.py index 4dbc81caa2..ccd43c72cf 100644 --- a/esphome/components/mipi/__init__.py +++ b/esphome/components/mipi/__init__.py @@ -128,6 +128,8 @@ MADCTL_MH = 0x04 # Bit 2 LCD refresh right to left MADCTL_XFLIP = 0x02 # Mirror the display horizontally MADCTL_YFLIP = 0x01 # Mirror the display vertically +MADCTL_FLIP_FLAG = 0x100 # meta-flag to indicate use of axis flips + # Special constant for delays in command sequences DELAY_FLAG = 0xFFF # Special flag to indicate a delay @@ -329,7 +331,13 @@ class DriverChip: return CONF_SWAP_XY in transforms and CONF_MIRROR_X in transforms return CONF_SWAP_XY in transforms and CONF_MIRROR_Y in transforms - def get_dimensions(self, config) -> tuple[int, int, int, int]: + def get_dimensions(self, config, swap: bool = True) -> tuple[int, int, int, int]: + """ + Return the dimensions of the current model. + :param config: The current configuration + :param swap: If width/height should be swapped when axes are swapped. + :return: + """ if CONF_DIMENSIONS in config: # Explicit dimensions, just use as is dimensions = config[CONF_DIMENSIONS] @@ -361,13 +369,12 @@ class DriverChip: ) offset_height = native_height - height - offset_height # Swap default dimensions if swap_xy is set, or if rotation is 90/270 and we are not using a buffer - if transform.get(CONF_SWAP_XY) is True: + if swap and transform.get(CONF_SWAP_XY) is True: width, height = height, width offset_height, offset_width = offset_width, offset_height return width, height, offset_width, offset_height - def get_transform(self, config) -> dict[str, bool]: - can_transform = self.rotation_as_transform(config) + def get_base_transform(self, config): transform = config.get( CONF_TRANSFORM, { @@ -376,14 +383,20 @@ class DriverChip: CONF_SWAP_XY: self.get_default(CONF_SWAP_XY), }, ) - if not isinstance(transform, dict): - # Presumably disabled - return { - CONF_MIRROR_X: False, - CONF_MIRROR_Y: False, - CONF_SWAP_XY: False, - CONF_TRANSFORM: False, - } + if isinstance(transform, dict): + return transform + + # Transform is disabled + return { + CONF_MIRROR_X: False, + CONF_MIRROR_Y: False, + CONF_SWAP_XY: False, + CONF_TRANSFORM: False, + } + + def get_transform(self, config) -> dict[str, bool]: + transform = self.get_base_transform(config) + can_transform = self.rotation_as_transform(config) # Can we use the MADCTL register to set the rotation? if can_transform and CONF_TRANSFORM not in config: rotation = config[CONF_ROTATION] @@ -411,11 +424,15 @@ class DriverChip: return {cv.Required(CONF_SWAP_XY): cv.boolean} return {cv.Optional(CONF_SWAP_XY, default=False): validator} - def add_madctl(self, sequence: list, config: dict): - # Add the MADCTL command to the sequence based on the configuration. - use_flip = config.get(CONF_USE_AXIS_FLIPS) - madctl = 0 - transform = self.get_transform(config) + def get_madctl(self, transform: dict, config: dict) -> int: + """ + Convert a transform to MADCTL bits + :param transform: The transform dict + :param use_flip: Whether to use axis flips + :return: MADCTL value + """ + use_flip = config.get(CONF_USE_AXIS_FLIPS, False) + madctl = MADCTL_FLIP_FLAG if use_flip else 0 if transform[CONF_MIRROR_X]: madctl |= MADCTL_XFLIP if use_flip else MADCTL_MX if transform[CONF_MIRROR_Y]: @@ -424,22 +441,28 @@ class DriverChip: madctl |= MADCTL_MV if config[CONF_COLOR_ORDER] == MODE_BGR: madctl |= MADCTL_BGR - sequence.append((MADCTL, madctl)) return madctl + def add_madctl(self, sequence: list, config: dict): + # Add the MADCTL command to the sequence based on the configuration. + # This takes into account rotation if it can be implemented in the transform + transform = self.get_transform(config) + madctl = self.get_madctl(transform, config) + sequence.append((MADCTL, madctl & 0xFF)) + def skip_command(self, command: str): """ Allow suppressing a standard command in the init sequence. """ return self.get_default(f"no_{command.lower()}", False) - def get_sequence(self, config) -> tuple[tuple[int, ...], int]: + def get_sequence(self, config, add_madctl=True) -> tuple[int, ...]: """ Create the init sequence for the display. Use the default sequence from the model, if any, and append any custom sequence provided in the config. Append SLPOUT (if not already in the sequence) and DISPON to the end of the sequence - Pixel format, color order, and orientation will be set. - Returns a tuple of the init sequence and the computed MADCTL value. + MADCTL will be set if add_madctl is True + Returns the init sequence """ sequence = list(self.initsequence or ()) custom_sequence = config.get(CONF_INIT_SEQUENCE, []) @@ -457,7 +480,8 @@ class DriverChip: if self.rotation_as_transform(config): LOGGER.info("Using hardware transform to implement rotation") - madctl = self.add_madctl(sequence, config) + if add_madctl: + self.add_madctl(sequence, config) if config[CONF_INVERT_COLORS]: sequence.append((INVON,)) else: @@ -471,7 +495,7 @@ class DriverChip: # Flatten the sequence into a list of bytes, with the length of each command # or the delay flag inserted where needed - return flatten_sequence(sequence), madctl + return flatten_sequence(sequence) def requires_buffer(config) -> bool: diff --git a/esphome/components/mipi_dsi/display.py b/esphome/components/mipi_dsi/display.py index 85bfad7f1a..026c214569 100644 --- a/esphome/components/mipi_dsi/display.py +++ b/esphome/components/mipi_dsi/display.py @@ -192,10 +192,9 @@ async def to_code(config): width, height, _offset_width, _offset_height = model.get_dimensions(config) var = cg.new_Pvariable(config[CONF_ID], width, height, color_depth, pixel_mode) - sequence, madctl = model.get_sequence(config) + sequence = model.get_sequence(config) cg.add(var.set_model(config[CONF_MODEL])) cg.add(var.set_init_sequence(sequence)) - cg.add(var.set_madctl(madctl)) cg.add(var.set_invert_colors(config[CONF_INVERT_COLORS])) cg.add(var.set_hsync_pulse_width(config[CONF_HSYNC_PULSE_WIDTH])) cg.add(var.set_hsync_back_porch(config[CONF_HSYNC_BACK_PORCH])) diff --git a/esphome/components/mipi_dsi/mipi_dsi.cpp b/esphome/components/mipi_dsi/mipi_dsi.cpp index e8e9ca2bfb..fc59aeffe8 100644 --- a/esphome/components/mipi_dsi/mipi_dsi.cpp +++ b/esphome/components/mipi_dsi/mipi_dsi.cpp @@ -392,9 +392,6 @@ void MIPI_DSI::dump_config() { "\n Model: %s" "\n Width: %u" "\n Height: %u" - "\n Mirror X: %s" - "\n Mirror Y: %s" - "\n Swap X/Y: %s" "\n Rotation: %d degrees" "\n DSI Lanes: %u" "\n Lane Bit Rate: %.0fMbps" @@ -406,14 +403,11 @@ void MIPI_DSI::dump_config() { "\n VSync Front Porch: %u" "\n Buffer Color Depth: %d bit" "\n Display Pixel Mode: %d bit" - "\n Color Order: %s" "\n Invert Colors: %s" "\n Pixel Clock: %.1fMHz", - this->model_, this->width_, this->height_, YESNO(this->madctl_ & (MADCTL_XFLIP | MADCTL_MX)), - YESNO(this->madctl_ & (MADCTL_YFLIP | MADCTL_MY)), YESNO(this->madctl_ & MADCTL_MV), this->rotation_, - this->lanes_, this->lane_bit_rate_, this->hsync_pulse_width_, this->hsync_back_porch_, - this->hsync_front_porch_, this->vsync_pulse_width_, this->vsync_back_porch_, this->vsync_front_porch_, - (3 - this->color_depth_) * 8, this->pixel_mode_, this->madctl_ & MADCTL_BGR ? "BGR" : "RGB", + this->model_, this->width_, this->height_, this->rotation_, this->lanes_, this->lane_bit_rate_, + this->hsync_pulse_width_, this->hsync_back_porch_, this->hsync_front_porch_, this->vsync_pulse_width_, + this->vsync_back_porch_, this->vsync_front_porch_, (3 - this->color_depth_) * 8, this->pixel_mode_, YESNO(this->invert_colors_), this->pclk_frequency_); LOG_PIN(" Reset Pin ", this->reset_pin_); } diff --git a/esphome/components/mipi_dsi/mipi_dsi.h b/esphome/components/mipi_dsi/mipi_dsi.h index 6e27912aa5..c27c9ccc6e 100644 --- a/esphome/components/mipi_dsi/mipi_dsi.h +++ b/esphome/components/mipi_dsi/mipi_dsi.h @@ -60,7 +60,6 @@ class MIPI_DSI : public display::Display { void set_model(const char *model) { this->model_ = model; } void set_lane_bit_rate(float lane_bit_rate) { this->lane_bit_rate_ = lane_bit_rate; } void set_lanes(uint8_t lanes) { this->lanes_ = lanes; } - void set_madctl(uint8_t madctl) { this->madctl_ = madctl; } void smark_failed(const LogString *message, esp_err_t err); @@ -86,7 +85,6 @@ class MIPI_DSI : public display::Display { std::vector enable_pins_{}; size_t width_{}; size_t height_{}; - uint8_t madctl_{}; uint16_t hsync_pulse_width_ = 10; uint16_t hsync_back_porch_ = 10; uint16_t hsync_front_porch_ = 20; diff --git a/esphome/components/mipi_rgb/display.py b/esphome/components/mipi_rgb/display.py index 0aa8c56719..4952bda95f 100644 --- a/esphome/components/mipi_rgb/display.py +++ b/esphome/components/mipi_rgb/display.py @@ -265,9 +265,8 @@ async def to_code(config): if CONF_SPI_ID in config: await spi.register_spi_device(var, config, write_only=True) - sequence, madctl = model.get_sequence(config) + sequence = model.get_sequence(config) cg.add(var.set_init_sequence(sequence)) - cg.add(var.set_madctl(madctl)) cg.add(var.set_color_mode(COLOR_ORDERS[config[CONF_COLOR_ORDER]])) cg.add(var.set_invert_colors(config[CONF_INVERT_COLORS])) diff --git a/esphome/components/mipi_rgb/mipi_rgb.cpp b/esphome/components/mipi_rgb/mipi_rgb.cpp index 0b0a5344e4..6f5e2f2490 100644 --- a/esphome/components/mipi_rgb/mipi_rgb.cpp +++ b/esphome/components/mipi_rgb/mipi_rgb.cpp @@ -118,15 +118,7 @@ void MipiRgbSpi::dump_config() { MipiRgb::dump_config(); LOG_PIN(" CS Pin: ", this->cs_); LOG_PIN(" DC Pin: ", this->dc_pin_); - ESP_LOGCONFIG(TAG, - " SPI Data rate: %uMHz" - "\n Mirror X: %s" - "\n Mirror Y: %s" - "\n Swap X/Y: %s" - "\n Color Order: %s", - (unsigned) (this->data_rate_ / 1000000), YESNO(this->madctl_ & (MADCTL_XFLIP | MADCTL_MX)), - YESNO(this->madctl_ & (MADCTL_YFLIP | MADCTL_MY | MADCTL_ML)), YESNO(this->madctl_ & MADCTL_MV), - this->madctl_ & MADCTL_BGR ? "BGR" : "RGB"); + ESP_LOGCONFIG(TAG, " SPI Data rate: %uMHz", (unsigned) (this->data_rate_ / 1000000)); } #endif // USE_SPI diff --git a/esphome/components/mipi_rgb/mipi_rgb.h b/esphome/components/mipi_rgb/mipi_rgb.h index 76b48bb249..accc251a18 100644 --- a/esphome/components/mipi_rgb/mipi_rgb.h +++ b/esphome/components/mipi_rgb/mipi_rgb.h @@ -38,7 +38,6 @@ class MipiRgb : public display::Display { display::ColorOrder get_color_mode() { return this->color_mode_; } void set_color_mode(display::ColorOrder color_mode) { this->color_mode_ = color_mode; } void set_invert_colors(bool invert_colors) { this->invert_colors_ = invert_colors; } - void set_madctl(uint8_t madctl) { this->madctl_ = madctl; } void add_data_pin(InternalGPIOPin *data_pin, size_t index) { this->data_pins_[index] = data_pin; }; void set_de_pin(InternalGPIOPin *de_pin) { this->de_pin_ = de_pin; } @@ -84,7 +83,6 @@ class MipiRgb : public display::Display { uint16_t vsync_front_porch_ = 10; uint32_t pclk_frequency_ = 16 * 1000 * 1000; bool pclk_inverted_{true}; - uint8_t madctl_{}; const char *model_{"Unknown"}; bool invert_colors_{}; display::ColorOrder color_mode_{display::COLOR_ORDER_BGR}; diff --git a/esphome/components/mipi_spi/display.py b/esphome/components/mipi_spi/display.py index 8dccfa3a92..42c7ec2224 100644 --- a/esphome/components/mipi_spi/display.py +++ b/esphome/components/mipi_spi/display.py @@ -10,7 +10,7 @@ from esphome.components.const import ( CONF_COLOR_DEPTH, CONF_DRAW_ROUNDING, ) -from esphome.components.display import CONF_SHOW_TEST_CARD, DISPLAY_ROTATIONS +from esphome.components.display import CONF_SHOW_TEST_CARD from esphome.components.mipi import ( CONF_PIXEL_MODE, CONF_USE_AXIS_FLIPS, @@ -47,12 +47,10 @@ from esphome.const import ( CONF_MIRROR_Y, CONF_MODEL, CONF_RESET_PIN, - CONF_ROTATION, CONF_SWAP_XY, CONF_TRANSFORM, CONF_WIDTH, ) -from esphome.core import CORE from esphome.cpp_generator import TemplateArguments from esphome.final_validate import full_config @@ -113,22 +111,21 @@ DISPLAY_PIXEL_MODES = { def denominator(config): """ Calculate the best denominator for a buffer size fraction. - The denominator must be a number between 2 and 16 that divides the display height evenly, + The denominator should be a number between 2 and 16 that divides the display height evenly, and the fraction represented by the denominator must be less than or equal to the given fraction. :config: The configuration dictionary containing the buffer size fraction and display dimensions :return: The denominator to use for the buffer size fraction """ model = MODELS[config[CONF_MODEL]] frac = config.get(CONF_BUFFER_SIZE) - if frac is None or frac > 0.75: + _width, height, _offset_width, _offset_height = model.get_dimensions(config) + if frac is None or frac > 0.75 or height < 32: return 1 - height, _width, _offset_width, _offset_height = model.get_dimensions(config) try: return next(x for x in range(2, 17) if frac >= 1 / x and height % x == 0) except StopIteration: - raise cv.Invalid( - f"Buffer size fraction {frac} is not compatible with display height {height}" - ) from StopIteration + # No exact divisor, just use the closest. + return next(x for x in range(2, 17) if frac >= 1 / x) def model_schema(config): @@ -282,75 +279,33 @@ def _final_validate(config): from esphome.components.lvgl import DOMAIN as LVGL_DOMAIN + if config[CONF_BUS_MODE] == TYPE_SINGLE: + spi.final_validate_device_schema(DOMAIN, require_miso=False, require_mosi=True)( + config + ) if not requires_buffer(config) and LVGL_DOMAIN not in global_config: # If no drawing methods are configured, and LVGL is not enabled, show a test card config[CONF_SHOW_TEST_CARD] = True if PSRAM_DOMAIN not in global_config and CONF_BUFFER_SIZE not in config: - if not requires_buffer(config): - return config # No buffer needed, so no need to set a buffer size # If PSRAM is not enabled, choose a small buffer size by default if not requires_buffer(config): - # not our problem. - return config + return # No need to pick a size color_depth = get_color_depth(config) frac = denominator(config) - height, width, _offset_width, _offset_height = model.get_dimensions(config) + width, height, _offset_width, _offset_height = model.get_dimensions(config) buffer_size = color_depth // 8 * width * height // frac - # Target a buffer size of 20kB - fraction = 20000.0 / buffer_size - try: - config[CONF_BUFFER_SIZE] = 1.0 / next( - x for x in range(2, 17) if fraction >= 1 / x and height % x == 0 - ) - except StopIteration: - # Either the screen is too big, or the height is not divisible by any of the fractions, so use 1.0 - # PSRAM will be needed. - if CORE.is_esp32: - raise cv.Invalid( - "PSRAM is required for this display" - ) from StopIteration - - return config + # Target a buffer size of 20kB, except for large displays, which shouldn't end up here + fraction = min(20000.0, buffer_size // 16) / buffer_size + config[CONF_BUFFER_SIZE] = 1.0 / next( + x for x in range(2, 17) if fraction >= 1 / x + ) FINAL_VALIDATE_SCHEMA = _final_validate -def get_transform(config): - """ - Get the transformation configuration for the display. - :param config: - :return: - """ - model = MODELS[config[CONF_MODEL]] - can_transform = model.rotation_as_transform(config) - transform = config.get( - CONF_TRANSFORM, - { - CONF_MIRROR_X: model.get_default(CONF_MIRROR_X, False), - CONF_MIRROR_Y: model.get_default(CONF_MIRROR_Y, False), - CONF_SWAP_XY: model.get_default(CONF_SWAP_XY, False), - }, - ) - - # Can we use the MADCTL register to set the rotation? - if can_transform and CONF_TRANSFORM not in config: - 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] - else: - transform[CONF_SWAP_XY] = not transform[CONF_SWAP_XY] - transform[CONF_MIRROR_Y] = not transform[CONF_MIRROR_Y] - transform[CONF_TRANSFORM] = True - return transform - - def get_instance(config): """ Get the type of MipiSpi instance to create based on the configuration, @@ -359,7 +314,16 @@ def get_instance(config): :return: type, template arguments """ model = MODELS[config[CONF_MODEL]] - width, height, offset_width, offset_height = model.get_dimensions(config) + has_hardware_transform = config.get( + CONF_TRANSFORM + ) != CONF_DISABLED and model.transforms == { + CONF_MIRROR_X, + CONF_MIRROR_Y, + CONF_SWAP_XY, + } + width, height, offset_width, offset_height = model.get_dimensions( + config, not has_hardware_transform + ) color_depth = int(config[CONF_COLOR_DEPTH].removesuffix("bit")) bufferpixels = COLOR_DEPTHS[color_depth] @@ -373,57 +337,43 @@ def get_instance(config): bus_type = BusTypes[bus_type] buffer_type = cg.uint8 if color_depth == 8 else cg.uint16 frac = denominator(config) - rotation = ( - 0 if model.rotation_as_transform(config) else config.get(CONF_ROTATION, 0) - ) + madctl = model.get_madctl(model.get_base_transform(config), config) + has_writer = requires_buffer(config) templateargs = [ buffer_type, bufferpixels, config[CONF_BYTE_ORDER] == "big_endian", display_pixel_mode, bus_type, + width, + height, + offset_width, + offset_height, + madctl, + has_hardware_transform, ] + display.add_metadata( + config[CONF_ID], width, height, has_writer, has_hardware_transform + ) # If a buffer is required, use MipiSpiBuffer, otherwise use MipiSpi if requires_buffer(config): templateargs.extend( [ - width, - height, - offset_width, - offset_height, - DISPLAY_ROTATIONS[rotation], frac, config[CONF_DRAW_ROUNDING], ] ) return MipiSpiBuffer, templateargs - # Swap height and width if the display is rotated 90 or 270 degrees in software - if rotation in (90, 270): - width, height = height, width - offset_width, offset_height = offset_height, offset_width - templateargs.extend( - [ - width, - height, - offset_width, - offset_height, - ] - ) return MipiSpi, templateargs async def to_code(config): model = MODELS[config[CONF_MODEL]] var_id = config[CONF_ID] + init_sequence = model.get_sequence(config, False) var_id.type, templateargs = get_instance(config) var = cg.new_Pvariable(var_id, TemplateArguments(*templateargs)) - init_sequence, _madctl = model.get_sequence(config) cg.add(var.set_init_sequence(init_sequence)) - if model.rotation_as_transform(config): - if CONF_TRANSFORM in config: - LOGGER.warning("Use of 'transform' with 'rotation' is not recommended") - else: - config[CONF_ROTATION] = 0 cg.add(var.set_model(config[CONF_MODEL])) if enable_pin := config.get(CONF_ENABLE_PIN): enable = [await cg.gpio_pin_expression(pin) for pin in enable_pin] diff --git a/esphome/components/mipi_spi/mipi_spi.cpp b/esphome/components/mipi_spi/mipi_spi.cpp index 90f6324511..2eec3b12d1 100644 --- a/esphome/components/mipi_spi/mipi_spi.cpp +++ b/esphome/components/mipi_spi/mipi_spi.cpp @@ -5,7 +5,8 @@ namespace esphome::mipi_spi { void internal_dump_config(const char *model, int width, int height, int offset_width, int offset_height, uint8_t madctl, bool invert_colors, int display_bits, bool is_big_endian, const optional &brightness, - GPIOPin *cs, GPIOPin *reset, GPIOPin *dc, int spi_mode, uint32_t data_rate, int bus_width) { + GPIOPin *cs, GPIOPin *reset, GPIOPin *dc, int spi_mode, uint32_t data_rate, int bus_width, + bool has_hardware_rotation) { ESP_LOGCONFIG(TAG, "MIPI_SPI Display\n" " Model: %s\n" @@ -14,6 +15,7 @@ void internal_dump_config(const char *model, int width, int height, int offset_w " Swap X/Y: %s\n" " Mirror X: %s\n" " Mirror Y: %s\n" + " Hardware rotation: %s\n" " Invert colors: %s\n" " Color order: %s\n" " Display pixels: %d bits\n" @@ -22,9 +24,9 @@ void internal_dump_config(const char *model, int width, int height, int offset_w " SPI Data rate: %uMHz\n" " SPI Bus width: %d", model, width, height, YESNO(madctl & MADCTL_MV), YESNO(madctl & (MADCTL_MX | MADCTL_XFLIP)), - YESNO(madctl & (MADCTL_MY | MADCTL_YFLIP)), YESNO(invert_colors), (madctl & MADCTL_BGR) ? "BGR" : "RGB", - display_bits, is_big_endian ? "Big" : "Little", spi_mode, static_cast(data_rate / 1000000), - bus_width); + YESNO(madctl & (MADCTL_MY | MADCTL_YFLIP)), YESNO(has_hardware_rotation), YESNO(invert_colors), + (madctl & MADCTL_BGR) ? "BGR" : "RGB", display_bits, is_big_endian ? "Big" : "Little", spi_mode, + static_cast(data_rate / 1000000), bus_width); LOG_PIN(" CS Pin: ", cs); LOG_PIN(" Reset Pin: ", reset); LOG_PIN(" DC Pin: ", dc); diff --git a/esphome/components/mipi_spi/mipi_spi.h b/esphome/components/mipi_spi/mipi_spi.h index 083ff9507f..423226b1d7 100644 --- a/esphome/components/mipi_spi/mipi_spi.h +++ b/esphome/components/mipi_spi/mipi_spi.h @@ -34,13 +34,14 @@ static constexpr uint8_t SWIRE1 = 0x5A; static constexpr uint8_t SWIRE2 = 0x5B; static constexpr uint8_t PAGESEL = 0xFE; -static constexpr uint8_t MADCTL_MY = 0x80; // Bit 7 Bottom to top -static constexpr uint8_t MADCTL_MX = 0x40; // Bit 6 Right to left -static constexpr uint8_t MADCTL_MV = 0x20; // Bit 5 Swap axes -static constexpr uint8_t MADCTL_RGB = 0x00; // Bit 3 Red-Green-Blue pixel order -static constexpr uint8_t MADCTL_BGR = 0x08; // Bit 3 Blue-Green-Red pixel order -static constexpr uint8_t MADCTL_XFLIP = 0x02; // Mirror the display horizontally -static constexpr uint8_t MADCTL_YFLIP = 0x01; // Mirror the display vertically +static constexpr uint8_t MADCTL_MY = 0x80; // Bit 7 Bottom to top +static constexpr uint8_t MADCTL_MX = 0x40; // Bit 6 Right to left +static constexpr uint8_t MADCTL_MV = 0x20; // Bit 5 Swap axes +static constexpr uint8_t MADCTL_RGB = 0x00; // Bit 3 Red-Green-Blue pixel order +static constexpr uint8_t MADCTL_BGR = 0x08; // Bit 3 Blue-Green-Red pixel order +static constexpr uint8_t MADCTL_XFLIP = 0x02; // Mirror the display horizontally +static constexpr uint8_t MADCTL_YFLIP = 0x01; // Mirror the display vertically +static constexpr uint16_t MADCTL_FLIP_FLAG = 0x100; // controller uses axis flip bits static constexpr uint8_t DELAY_FLAG = 0xFF; // store a 16 bit value in a buffer, big endian. @@ -66,7 +67,8 @@ enum BusType { // Helper function for dump_config - defined in mipi_spi.cpp to allow use of LOG_PIN macro void internal_dump_config(const char *model, int width, int height, int offset_width, int offset_height, uint8_t madctl, bool invert_colors, int display_bits, bool is_big_endian, const optional &brightness, - GPIOPin *cs, GPIOPin *reset, GPIOPin *dc, int spi_mode, uint32_t data_rate, int bus_width); + GPIOPin *cs, GPIOPin *reset, GPIOPin *dc, int spi_mode, uint32_t data_rate, int bus_width, + bool has_hardware_rotation); /** * Base class for MIPI SPI displays. @@ -83,7 +85,7 @@ void internal_dump_config(const char *model, int width, int height, int offset_w * buffer */ template + int WIDTH, int HEIGHT, int OFFSET_WIDTH, int OFFSET_HEIGHT, uint16_t MADCTL, bool HAS_HARDWARE_ROTATION> class MipiSpi : public display::Display, public spi::SPIDevice { @@ -103,10 +105,39 @@ class MipiSpi : public display::Display, this->brightness_ = brightness; this->reset_params_(); } + void set_rotation(display::DisplayRotation rotation) override { + this->rotation_ = rotation; + if constexpr (HAS_HARDWARE_ROTATION) { + this->reset_params_(); + } + } display::DisplayType get_display_type() override { return display::DisplayType::DISPLAY_TYPE_COLOR; } - int get_width_internal() override { return WIDTH; } - int get_height_internal() override { return HEIGHT; } + int get_width() override { + if (this->rotation_ == display::DISPLAY_ROTATION_90_DEGREES || + this->rotation_ == display::DISPLAY_ROTATION_270_DEGREES) + return HEIGHT; + return WIDTH; + } + + int get_height() override { + if (this->rotation_ == display::DISPLAY_ROTATION_90_DEGREES || + this->rotation_ == display::DISPLAY_ROTATION_270_DEGREES) + return WIDTH; + return HEIGHT; + } + + // If hardware rotation is in use, the actual display width/height changes with rotation + int get_width_internal() override { + if constexpr (HAS_HARDWARE_ROTATION) + return get_width(); + return WIDTH; + } + int get_height_internal() override { + if constexpr (HAS_HARDWARE_ROTATION) + return get_height(); + return HEIGHT; + } void set_init_sequence(const std::vector &sequence) { this->init_sequence_ = sequence; } // reset the display, and write the init sequence @@ -166,9 +197,6 @@ class MipiSpi : public display::Display, case INVERT_ON: this->invert_colors_ = true; break; - case MADCTL_CMD: - this->madctl_ = arg_byte; - break; case BRIGHTNESS: this->brightness_ = arg_byte; break; @@ -177,13 +205,13 @@ class MipiSpi : public display::Display, break; } const auto *ptr = vec.data() + index; - esph_log_d(TAG, "Command %02X, length %d, byte %02X", cmd, num_args, arg_byte); this->write_command_(cmd, ptr, num_args); index += num_args; if (cmd == SLEEP_OUT) delay(10); } } + this->reset_params_(); // init sequence no longer needed this->init_sequence_.clear(); } @@ -206,9 +234,10 @@ class MipiSpi : public display::Display, } void dump_config() override { - internal_dump_config(this->model_, WIDTH, HEIGHT, OFFSET_WIDTH, OFFSET_HEIGHT, this->madctl_, this->invert_colors_, - DISPLAYPIXEL * 8, IS_BIG_ENDIAN, this->brightness_, this->cs_, this->reset_pin_, this->dc_pin_, - this->mode_, this->data_rate_, BUS_TYPE); + internal_dump_config(this->model_, this->get_width(), this->get_height(), OFFSET_WIDTH, OFFSET_HEIGHT, MADCTL, + this->invert_colors_, DISPLAYPIXEL * 8, IS_BIG_ENDIAN, this->brightness_, this->cs_, + this->reset_pin_, this->dc_pin_, this->mode_, this->data_rate_, BUS_TYPE, + HAS_HARDWARE_ROTATION); } protected: @@ -219,10 +248,13 @@ class MipiSpi : public display::Display, // Writes a command to the display, with the given bytes. void write_command_(uint8_t cmd, const uint8_t *bytes, size_t len) { -#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE char hex_buf[format_hex_pretty_size(MIPI_SPI_MAX_CMD_LOG_BYTES)]; - esph_log_v(TAG, "Command %02X, length %d, bytes %s", cmd, len, format_hex_pretty_to(hex_buf, bytes, len)); -#endif + // Don't spam the log after setup + if (this->init_sequence_.empty()) { + esph_log_v(TAG, "Command %02X, length %d, bytes %s", cmd, len, format_hex_pretty_to(hex_buf, bytes, len)); + } else { + esph_log_d(TAG, "Command %02X, length %d, bytes %s", cmd, len, format_hex_pretty_to(hex_buf, bytes, len)); + } if constexpr (BUS_TYPE == BUS_TYPE_QUAD) { this->enable(); this->write_cmd_addr_data(8, 0x02, 24, cmd << 8, bytes, len); @@ -271,16 +303,60 @@ class MipiSpi : public display::Display, this->write_command_(this->invert_colors_ ? INVERT_ON : INVERT_OFF); if (this->brightness_.has_value()) this->write_command_(BRIGHTNESS, this->brightness_.value()); + + // calculate new madctl value from base value adjusted for rotation + uint8_t madctl = MADCTL; // lower 8 bits only + constexpr bool use_flips = (MADCTL & MADCTL_FLIP_FLAG) != 0; + constexpr uint8_t x_mask = use_flips ? MADCTL_XFLIP : MADCTL_MX; + constexpr uint8_t y_mask = use_flips ? MADCTL_YFLIP : MADCTL_MY; + if constexpr (HAS_HARDWARE_ROTATION) { + switch (this->rotation_) { + default: + break; + case display::DISPLAY_ROTATION_90_DEGREES: + madctl ^= x_mask; // flip X axis + madctl ^= MADCTL_MV; // swap X and Y axes + break; + case display::DISPLAY_ROTATION_180_DEGREES: + madctl ^= x_mask; // flip X axis + madctl ^= y_mask; // flip Y axis + break; + case display::DISPLAY_ROTATION_270_DEGREES: + madctl ^= y_mask; // flip Y axis + madctl ^= MADCTL_MV; // swap X and Y axes + break; + } + } + esph_log_d(TAG, "Setting MADCTL for rotation %d, value %X", this->rotation_, madctl); + this->write_command_(MADCTL_CMD, madctl); + } + + uint16_t get_offset_width_() { + if constexpr (HAS_HARDWARE_ROTATION) { + if (this->rotation_ == display::DISPLAY_ROTATION_90_DEGREES || + this->rotation_ == display::DISPLAY_ROTATION_270_DEGREES) + return OFFSET_HEIGHT; + } + return OFFSET_WIDTH; + } + + uint16_t get_offset_height_() { + if constexpr (HAS_HARDWARE_ROTATION) { + if (this->rotation_ == display::DISPLAY_ROTATION_90_DEGREES || + this->rotation_ == display::DISPLAY_ROTATION_270_DEGREES) + return OFFSET_WIDTH; + } + return OFFSET_HEIGHT; } // set the address window for the next data write void set_addr_window_(uint16_t x1, uint16_t y1, uint16_t x2, uint16_t y2) { esph_log_v(TAG, "Set addr %d/%d, %d/%d", x1, y1, x2, y2); uint8_t buf[4]; - x1 += OFFSET_WIDTH; - x2 += OFFSET_WIDTH; - y1 += OFFSET_HEIGHT; - y2 += OFFSET_HEIGHT; + x1 += get_offset_width_(); + x2 += get_offset_width_(); + y1 += get_offset_height_(); + y2 += get_offset_height_(); put16_be(buf, y1); put16_be(buf + 2, y2); this->write_command_(RASET, buf, sizeof buf); @@ -408,7 +484,6 @@ class MipiSpi : public display::Display, optional brightness_{}; const char *model_{"Unknown"}; std::vector init_sequence_{}; - uint8_t madctl_{}; }; /** @@ -427,22 +502,21 @@ class MipiSpi : public display::Display, * @tparam ROUNDING The alignment requirement for drawing operations (e.g. 2 means that x coordinates must be even) */ template + uint16_t WIDTH, uint16_t HEIGHT, int OFFSET_WIDTH, int OFFSET_HEIGHT, uint16_t MADCTL, + bool HAS_HARDWARE_ROTATION, int FRACTION, unsigned ROUNDING> class MipiSpiBuffer : public MipiSpi { + OFFSET_WIDTH, OFFSET_HEIGHT, MADCTL, HAS_HARDWARE_ROTATION> { public: // these values define the buffer size needed to write in accordance with the chip pixel alignment // requirements. If the required rounding does not divide the width and height, we round up to the next multiple and // ignore the extra columns and rows when drawing, but use them to write to the display. - static constexpr unsigned BUFFER_WIDTH = (WIDTH + ROUNDING - 1) / ROUNDING * ROUNDING; - static constexpr unsigned BUFFER_HEIGHT = (HEIGHT + ROUNDING - 1) / ROUNDING * ROUNDING; + static constexpr size_t round_buffer(size_t size) { return (size + ROUNDING - 1) / ROUNDING * ROUNDING; } - MipiSpiBuffer() { this->rotation_ = ROTATION; } + MipiSpiBuffer() = default; void dump_config() override { - MipiSpi::dump_config(); + MipiSpi::dump_config(); esph_log_config(TAG, " Rotation: %d°\n" " Buffer pixels: %d bits\n" @@ -450,14 +524,14 @@ class MipiSpiBuffer : public MipiSpirotation_, BUFFERPIXEL * 8, FRACTION, - sizeof(BUFFERTYPE) * BUFFER_WIDTH * BUFFER_HEIGHT / FRACTION, ROUNDING); + sizeof(BUFFERTYPE) * round_buffer(WIDTH) * round_buffer(HEIGHT) / FRACTION, ROUNDING); } void setup() override { - MipiSpi::setup(); + MipiSpi::setup(); RAMAllocator allocator{}; - this->buffer_ = allocator.allocate(BUFFER_WIDTH * BUFFER_HEIGHT / FRACTION); + this->buffer_ = allocator.allocate(round_buffer(WIDTH) * round_buffer(HEIGHT) / FRACTION); if (this->buffer_ == nullptr) { this->mark_failed(LOG_STR("Buffer allocation failed")); } @@ -472,11 +546,13 @@ class MipiSpiBuffer : public MipiSpistart_line_ = 0; this->start_line_ < HEIGHT; this->start_line_ += HEIGHT / FRACTION) { + for (this->start_line_ = 0; this->start_line_ < this->get_height_internal(); + this->start_line_ += this->get_height_internal() / FRACTION) { #if ESPHOME_LOG_LEVEL == ESPHOME_LOG_LEVEL_VERBOSE auto lap = millis(); #endif - this->end_line_ = this->start_line_ + HEIGHT / FRACTION; + this->end_line_ = + clamp_at_most(this->start_line_ + this->get_height_internal() / FRACTION, this->get_height_internal()); if (this->auto_clear_enabled_) { this->clear(); } @@ -503,10 +579,10 @@ class MipiSpiBuffer : public MipiSpix_high_ - this->x_low_ + 1; int h = this->y_high_ - this->y_low_ + 1; this->write_to_display_(this->x_low_, this->y_low_, w, h, this->buffer_, this->x_low_, - this->y_low_ - this->start_line_, BUFFER_WIDTH - w); + this->y_low_ - this->start_line_, round_buffer(this->get_width_internal()) - w); // invalidate watermarks - this->x_low_ = WIDTH; - this->y_low_ = HEIGHT; + this->x_low_ = this->get_width_internal(); + this->y_low_ = this->get_height_internal(); this->x_high_ = 0; this->y_high_ = 0; #if ESPHOME_LOG_LEVEL == ESPHOME_LOG_LEVEL_VERBOSE @@ -523,10 +599,23 @@ class MipiSpiBuffer : public MipiSpiget_clipping().inside(x, y)) return; - rotate_coordinates(x, y); - if (x < 0 || x >= WIDTH || y < this->start_line_ || y >= this->end_line_) + if constexpr (not HAS_HARDWARE_ROTATION) { + if (this->rotation_ == display::DISPLAY_ROTATION_180_DEGREES) { + x = WIDTH - x - 1; + y = HEIGHT - y - 1; + } else if (this->rotation_ == display::DISPLAY_ROTATION_90_DEGREES) { + auto tmp = x; + x = WIDTH - y - 1; + y = tmp; + } else if (this->rotation_ == display::DISPLAY_ROTATION_270_DEGREES) { + auto tmp = y; + y = HEIGHT - x - 1; + x = tmp; + } + } + if (x < 0 || x >= this->get_width_internal() || y < this->start_line_ || y >= this->end_line_) return; - this->buffer_[(y - this->start_line_) * BUFFER_WIDTH + x] = convert_color(color); + this->buffer_[(y - this->start_line_) * round_buffer(this->get_width_internal()) + x] = convert_color(color); if (x < this->x_low_) { this->x_low_ = x; } @@ -551,39 +640,14 @@ class MipiSpiBuffer : public MipiSpix_low_ = 0; this->y_low_ = this->start_line_; - this->x_high_ = WIDTH - 1; + this->x_high_ = this->get_width_internal() - 1; this->y_high_ = this->end_line_ - 1; - std::fill_n(this->buffer_, HEIGHT * BUFFER_WIDTH / FRACTION, convert_color(color)); - } - - int get_width() override { - if constexpr (ROTATION == display::DISPLAY_ROTATION_90_DEGREES || ROTATION == display::DISPLAY_ROTATION_270_DEGREES) - return HEIGHT; - return WIDTH; - } - - int get_height() override { - if constexpr (ROTATION == display::DISPLAY_ROTATION_90_DEGREES || ROTATION == display::DISPLAY_ROTATION_270_DEGREES) - return WIDTH; - return HEIGHT; + std::fill_n(this->buffer_, (this->end_line_ - this->start_line_) * round_buffer(this->get_width_internal()), + convert_color(color)); } protected: // Rotate the coordinates to match the display orientation. - static void rotate_coordinates(int &x, int &y) { - if constexpr (ROTATION == display::DISPLAY_ROTATION_180_DEGREES) { - x = WIDTH - x - 1; - y = HEIGHT - y - 1; - } else if constexpr (ROTATION == display::DISPLAY_ROTATION_90_DEGREES) { - auto tmp = x; - x = WIDTH - y - 1; - y = tmp; - } else if constexpr (ROTATION == display::DISPLAY_ROTATION_270_DEGREES) { - auto tmp = y; - y = HEIGHT - x - 1; - x = tmp; - } - } // Convert a color to the buffer pixel format. static BUFFERTYPE convert_color(const Color &color) { diff --git a/esphome/components/mitsubishi_cn105/__init__.py b/esphome/components/mitsubishi_cn105/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/esphome/components/mitsubishi_cn105/climate.py b/esphome/components/mitsubishi_cn105/climate.py new file mode 100644 index 0000000000..7fa6825ea6 --- /dev/null +++ b/esphome/components/mitsubishi_cn105/climate.py @@ -0,0 +1,55 @@ +import esphome.codegen as cg +from esphome.components import climate, uart +import esphome.config_validation as cv +from esphome.const import CONF_UPDATE_INTERVAL +from esphome.types import ConfigType + +DEPENDENCIES = ["uart"] +AUTO_LOAD = ["climate"] +CODEOWNERS = ["@crnjan"] + +CONF_CURRENT_TEMPERATURE_MIN_INTERVAL = "current_temperature_min_interval" + +mitsubishi_ns = cg.esphome_ns.namespace("mitsubishi_cn105") + +MitsubishiCN105Climate = mitsubishi_ns.class_( + "MitsubishiCN105Climate", + climate.Climate, + cg.Component, + uart.UARTDevice, +) + +CONFIG_SCHEMA = ( + climate.climate_schema(MitsubishiCN105Climate) + .extend(uart.UART_DEVICE_SCHEMA) + .extend( + { + cv.Optional(CONF_UPDATE_INTERVAL, default="1s"): cv.update_interval, + cv.Optional( + CONF_CURRENT_TEMPERATURE_MIN_INTERVAL, default="60s" + ): cv.update_interval, + } + ) +) + +FINAL_VALIDATE_SCHEMA = cv.All( + uart.final_validate_device_schema( + "mitsubishi_cn105", + require_rx=True, + require_tx=True, + data_bits=8, + parity="EVEN", + stop_bits=1, + ) +) + + +async def to_code(config: ConfigType) -> None: + var = await climate.new_climate(config) + await cg.register_component(var, config) + await uart.register_uart_device(var, config) + cg.add( + var.set_current_temperature_min_interval( + config[CONF_CURRENT_TEMPERATURE_MIN_INTERVAL] + ) + ) diff --git a/esphome/components/mitsubishi_cn105/mitsubishi_cn105.cpp b/esphome/components/mitsubishi_cn105/mitsubishi_cn105.cpp new file mode 100644 index 0000000000..1a35495618 --- /dev/null +++ b/esphome/components/mitsubishi_cn105/mitsubishi_cn105.cpp @@ -0,0 +1,510 @@ +#include +#include +#include +#include "mitsubishi_cn105.h" + +namespace esphome::mitsubishi_cn105 { + +static const char *const TAG = "mitsubishi_cn105.driver"; + +static constexpr uint32_t WRITE_TIMEOUT_MS = 2000; + +static constexpr uint8_t TARGET_TEMPERATURE_ENC_A_OFFSET = 31; + +static constexpr size_t REQUEST_PAYLOAD_LEN = 0x10; +static constexpr size_t HEADER_LEN = 5; +static constexpr uint8_t PREAMBLE = 0xFC; +static constexpr uint8_t HEADER_BYTE_1 = 0x01; +static constexpr uint8_t HEADER_BYTE_2 = 0x30; + +static constexpr uint8_t PACKET_TYPE_CONNECT_REQUEST = 0x5A; +static constexpr uint8_t PACKET_TYPE_CONNECT_RESPONSE = 0x7A; +static constexpr std::array CONNECT_REQUEST_PAYLOAD = {0xCA, 0x01}; + +static constexpr uint8_t PACKET_TYPE_STATUS_REQUEST = 0x42; +static constexpr uint8_t PACKET_TYPE_STATUS_RESPONSE = 0x62; +static constexpr uint8_t STATUS_MSG_SETTINGS = 0x02; +static constexpr uint8_t STATUS_MSG_ROOM_TEMP = 0x03; + +static constexpr uint8_t PACKET_TYPE_WRITE_SETTINGS_REQUEST = 0x41; +static constexpr uint8_t PACKET_TYPE_WRITE_SETTINGS_RESPONSE = 0x61; + +static constexpr std::array, 9> PROTOCOL_MODE_MAP = { + std::nullopt, // 0x00 + MitsubishiCN105::Mode::HEAT, // 0x01 + MitsubishiCN105::Mode::DRY, // 0x02 + MitsubishiCN105::Mode::COOL, // 0x03 + std::nullopt, // 0x04 + std::nullopt, // 0x05 + std::nullopt, // 0x06 + MitsubishiCN105::Mode::FAN_ONLY, // 0x07 + MitsubishiCN105::Mode::AUTO // 0x08 +}; + +static constexpr std::array, 7> PROTOCOL_FAN_MODE_MAP = { + MitsubishiCN105::FanMode::AUTO, // 0x00 + MitsubishiCN105::FanMode::QUIET, // 0x01 + MitsubishiCN105::FanMode::SPEED_1, // 0x02 + MitsubishiCN105::FanMode::SPEED_2, // 0x03 + std::nullopt, // 0x04 + MitsubishiCN105::FanMode::SPEED_3, // 0x05 + MitsubishiCN105::FanMode::SPEED_4 // 0x06 +}; + +template +static constexpr std::optional lookup(const std::array, N> &table, uint8_t value) { + return (value < N) ? table[value] : std::nullopt; +} + +template +static constexpr bool reverse_lookup(const std::array, N> &table, T value, uint8_t &placeholder) { + for (size_t i = 0; i < N; ++i) { + const auto &table_value = table[i]; + if (table_value.has_value() && table_value == value) { + placeholder = i; + return true; + } + } + return false; +} + +static constexpr uint8_t checksum(const uint8_t *bytes, size_t length) { + return static_cast(0xFC - std::accumulate(bytes, bytes + length, uint8_t{0})); +} + +template +static constexpr auto make_packet(uint8_t type, const std::array &payload) { + const size_t full_len = PayloadSize + HEADER_LEN + 1; + std::array packet{PREAMBLE, type, HEADER_BYTE_1, HEADER_BYTE_2, static_cast(PayloadSize)}; + std::copy_n(payload.begin(), PayloadSize, packet.begin() + HEADER_LEN); + packet.back() = checksum(packet.data(), packet.size() - 1); + return packet; +} + +static float decode_temperature(int temp_a, int temp_b, int delta) { + return temp_b != 0 ? (temp_b - 128) / 2.0f : delta + temp_a; +} + +static constexpr auto CONNECT_PACKET = make_packet(PACKET_TYPE_CONNECT_REQUEST, CONNECT_REQUEST_PAYLOAD); + +void MitsubishiCN105::initialize() { this->set_state_(State::CONNECTING); } + +bool MitsubishiCN105::update() { + if (const auto start = this->status_update_start_ms_) { + if (this->pending_updates_.any()) { + this->cancel_waiting_and_transition_to_(State::APPLYING_SETTINGS); + return false; + } + + if ((get_loop_time_ms() - *start) >= this->update_interval_ms_) { + this->cancel_waiting_and_transition_to_(State::UPDATING_STATUS); + return false; + } + } + + if (const auto start = this->write_timeout_start_ms_; start && (get_loop_time_ms() - *start) >= WRITE_TIMEOUT_MS) { + this->write_timeout_start_ms_.reset(); + this->frame_parser_.reset(); + this->set_state_(State::READ_TIMEOUT); + return false; + } + + return this->frame_parser_.read_and_parse(this->device_, [this](uint8_t type, const uint8_t *payload, size_t len) { + return this->process_rx_packet_(type, payload, len); + }); +} + +void MitsubishiCN105::set_state_(State new_state) { + if (should_transition(this->state_, new_state)) { + ESP_LOGV(TAG, "Did transition: %s -> %s", LOG_STR_ARG(state_to_string(this->state_)), + LOG_STR_ARG(state_to_string(new_state))); + this->state_ = new_state; + this->did_transition_(new_state); + } else { + ESP_LOGV(TAG, "Ignoring unexpected transition %s -> %s", LOG_STR_ARG(state_to_string(this->state_)), + LOG_STR_ARG(state_to_string(new_state))); + } +} + +bool MitsubishiCN105::should_transition(State from, State to) { + switch (to) { + case State::CONNECTING: + return from == State::NOT_CONNECTED || from == State::READ_TIMEOUT; + + case State::CONNECTED: + return from == State::CONNECTING; + + case State::UPDATING_STATUS: + return from == State::CONNECTED || from == State::STATUS_UPDATED || + from == State::WAITING_FOR_SCHEDULED_STATUS_UPDATE; + + case State::STATUS_UPDATED: + return from == State::UPDATING_STATUS; + + case State::SCHEDULE_NEXT_STATUS_UPDATE: + return from == State::STATUS_UPDATED || from == State::SETTINGS_APPLIED; + + case State::WAITING_FOR_SCHEDULED_STATUS_UPDATE: + return from == State::SCHEDULE_NEXT_STATUS_UPDATE; + + case State::APPLYING_SETTINGS: + return from == State::WAITING_FOR_SCHEDULED_STATUS_UPDATE || from == State::STATUS_UPDATED; + + case State::SETTINGS_APPLIED: + return from == State::APPLYING_SETTINGS; + + case State::READ_TIMEOUT: + return from == State::UPDATING_STATUS || from == State::APPLYING_SETTINGS || from == State::CONNECTING; + + default: + return false; + } +} + +void MitsubishiCN105::did_transition_(State to) { + switch (to) { + case State::CONNECTING: + this->send_packet_(CONNECT_PACKET); + break; + + case State::CONNECTED: + this->write_timeout_start_ms_.reset(); + this->current_status_msg_type_ = STATUS_MSG_SETTINGS; + this->set_state_(State::UPDATING_STATUS); + break; + + case State::UPDATING_STATUS: + this->update_status_(); + break; + + case State::STATUS_UPDATED: { + this->write_timeout_start_ms_.reset(); + if (this->pending_updates_.any() && this->is_status_initialized()) { + this->set_state_(State::APPLYING_SETTINGS); + } else if (this->current_status_msg_type_ == STATUS_MSG_SETTINGS && this->should_request_room_temperature_()) { + this->current_status_msg_type_ = STATUS_MSG_ROOM_TEMP; + this->set_state_(State::UPDATING_STATUS); + } else { + this->set_state_(State::SCHEDULE_NEXT_STATUS_UPDATE); + } + break; + } + + case State::SCHEDULE_NEXT_STATUS_UPDATE: + this->status_update_start_ms_ = get_loop_time_ms(); + this->current_status_msg_type_ = STATUS_MSG_SETTINGS; + this->set_state_(State::WAITING_FOR_SCHEDULED_STATUS_UPDATE); + break; + + case State::APPLYING_SETTINGS: + this->apply_settings_(); + this->pending_updates_.clear(); + break; + + case State::SETTINGS_APPLIED: + this->write_timeout_start_ms_.reset(); + this->set_state_(State::SCHEDULE_NEXT_STATUS_UPDATE); + break; + + case State::READ_TIMEOUT: + this->set_state_(State::CONNECTING); + break; + + default: + break; + } +} + +bool MitsubishiCN105::should_request_room_temperature_() const { + if (!this->is_room_temperature_enabled()) { + return false; + } + + if (!this->last_room_temperature_update_ms_.has_value()) { + return true; + } + + return (get_loop_time_ms() - *this->last_room_temperature_update_ms_) >= this->room_temperature_min_interval_ms_; +} + +void MitsubishiCN105::send_packet_(const uint8_t *packet, size_t len) { + FrameParser::dump_buffer_vv("TX", packet, len); + this->device_.write_array(packet, len); + this->write_timeout_start_ms_ = get_loop_time_ms(); +} + +void MitsubishiCN105::update_status_() { + std::array payload = {this->current_status_msg_type_}; + this->send_packet_(make_packet(PACKET_TYPE_STATUS_REQUEST, payload)); +} + +void MitsubishiCN105::cancel_waiting_and_transition_to_(State state) { + this->status_update_start_ms_.reset(); + this->set_state_(state); +} + +bool MitsubishiCN105::process_rx_packet_(uint8_t type, const uint8_t *payload, size_t len) { + switch (type) { + case PACKET_TYPE_CONNECT_RESPONSE: + this->set_state_(State::CONNECTED); + return false; + + case PACKET_TYPE_STATUS_RESPONSE: + return this->process_status_packet_(payload, len); + + case PACKET_TYPE_WRITE_SETTINGS_RESPONSE: + this->set_state_(State::SETTINGS_APPLIED); + return false; + + default: + ESP_LOGVV(TAG, "RX unknown packet type 0x%02X", type); + return false; + } +} + +bool MitsubishiCN105::process_status_packet_(const uint8_t *payload, size_t len) { + if (len == 0) { + ESP_LOGVV(TAG, "RX status packet too short"); + return false; + } + + const auto previous = this->status_; + const auto msg_type = payload[0]; + if (!this->parse_status_payload_(msg_type, payload + 1, len - 1)) { + return false; + } + + if (msg_type == this->current_status_msg_type_) { + this->set_state_(State::STATUS_UPDATED); + } + + bool changed = previous.power_on != this->status_.power_on || previous.mode != this->status_.mode || + previous.fan_mode != this->status_.fan_mode || + previous.target_temperature != this->status_.target_temperature; + + if (this->is_room_temperature_enabled()) { + changed |= previous.room_temperature != this->status_.room_temperature; + } + + return changed && this->is_status_initialized(); +} + +bool MitsubishiCN105::parse_status_payload_(uint8_t msg_type, const uint8_t *payload, size_t len) { + switch (msg_type) { + case STATUS_MSG_SETTINGS: + return this->parse_status_settings_(payload, len); + + case STATUS_MSG_ROOM_TEMP: + return this->parse_status_room_temperature_(payload, len); + + default: + ESP_LOGVV(TAG, "RX unsupported status msg type 0x%02X", msg_type); + return false; + } +} + +bool MitsubishiCN105::parse_status_settings_(const uint8_t *payload, size_t len) { + if (len <= 10) { + ESP_LOGVV(TAG, "RX settings payload too short"); + return false; + } + + if (!this->pending_updates_.has(UpdateFlag::POWER)) { + this->status_.power_on = payload[2] != 0; + } + + this->use_temperature_encoding_b_ = payload[10] != 0; + if (!this->pending_updates_.has(UpdateFlag::TEMPERATURE)) { + this->status_.target_temperature = decode_temperature(-payload[4], payload[10], TARGET_TEMPERATURE_ENC_A_OFFSET); + } + + if (!this->pending_updates_.has(UpdateFlag::MODE)) { + const bool i_see = payload[3] > 0x08; + this->status_.mode = lookup(PROTOCOL_MODE_MAP, payload[3] - (i_see ? 0x08 : 0)).value_or(Mode::UNKNOWN); + } + + if (!this->pending_updates_.has(UpdateFlag::FAN)) { + this->status_.fan_mode = lookup(PROTOCOL_FAN_MODE_MAP, payload[5]).value_or(FanMode::UNKNOWN); + } + + return true; +} + +bool MitsubishiCN105::parse_status_room_temperature_(const uint8_t *payload, size_t len) { + if (len <= 5) { + ESP_LOGVV(TAG, "RX room temperature payload too short"); + return false; + } + + this->status_.room_temperature = decode_temperature(payload[2], payload[5], 10); + this->last_room_temperature_update_ms_ = get_loop_time_ms(); + + return true; +} + +void MitsubishiCN105::set_power(bool power_on) { + this->status_.power_on = power_on; + this->pending_updates_.set(UpdateFlag::POWER); +} + +void MitsubishiCN105::set_target_temperature(float target_temperature) { + if (target_temperature < 16 || target_temperature > 31) { + ESP_LOGD(TAG, "Setting temperature out-of-range: %.1f", target_temperature); + return; + } + this->status_.target_temperature = std::round(target_temperature); + this->pending_updates_.set(UpdateFlag::TEMPERATURE); +} + +void MitsubishiCN105::set_mode(Mode mode) { + uint8_t placeholder; + if (!reverse_lookup(PROTOCOL_MODE_MAP, mode, placeholder)) { + ESP_LOGD(TAG, "Setting invalid mode: %u", static_cast(mode)); + return; + } + this->status_.mode = mode; + this->pending_updates_.set(UpdateFlag::MODE); +} + +void MitsubishiCN105::set_fan_mode(FanMode fan_mode) { + uint8_t placeholder; + if (!reverse_lookup(PROTOCOL_FAN_MODE_MAP, fan_mode, placeholder)) { + ESP_LOGD(TAG, "Setting invalid fan mode: %u", static_cast(fan_mode)); + return; + } + this->status_.fan_mode = fan_mode; + this->pending_updates_.set(UpdateFlag::FAN); +} + +void MitsubishiCN105::apply_settings_() { + std::array payload = {0x01}; + + if (this->pending_updates_.has(UpdateFlag::POWER)) { + payload[1] |= 0x01; + payload[3] = this->status_.power_on ? 0x01 : 0x00; + } + + if (this->pending_updates_.has(UpdateFlag::TEMPERATURE)) { + payload[1] |= 0x04; + if (this->use_temperature_encoding_b_) { + payload[14] = static_cast(this->status_.target_temperature * 2.0f + 128.0f); + } else { + payload[5] = static_cast(TARGET_TEMPERATURE_ENC_A_OFFSET - this->status_.target_temperature); + } + } + + if (this->pending_updates_.has(UpdateFlag::MODE) && + reverse_lookup(PROTOCOL_MODE_MAP, this->status_.mode, payload[4])) { + payload[1] |= 0x02; + } + + if (this->pending_updates_.has(UpdateFlag::FAN) && + reverse_lookup(PROTOCOL_FAN_MODE_MAP, this->status_.fan_mode, payload[6])) { + payload[1] |= 0x08; + } + + this->send_packet_(make_packet(PACKET_TYPE_WRITE_SETTINGS_REQUEST, payload)); +} + +const LogString *MitsubishiCN105::state_to_string(State state) { + switch (state) { + case State::NOT_CONNECTED: + return LOG_STR("Not connected"); + case State::CONNECTING: + return LOG_STR("Connecting"); + case State::CONNECTED: + return LOG_STR("Connected"); + case State::UPDATING_STATUS: + return LOG_STR("UpdatingStatus"); + case State::STATUS_UPDATED: + return LOG_STR("StatusUpdated"); + case State::SCHEDULE_NEXT_STATUS_UPDATE: + return LOG_STR("ScheduleNextStatusUpdate"); + case State::WAITING_FOR_SCHEDULED_STATUS_UPDATE: + return LOG_STR("WaitingForScheduledStatusUpdate"); + case State::APPLYING_SETTINGS: + return LOG_STR("ApplyingSettings"); + case State::SETTINGS_APPLIED: + return LOG_STR("SettingsApplied"); + case State::READ_TIMEOUT: + return LOG_STR("ReadTimeout"); + } + return LOG_STR("Unknown"); +} + +template +bool MitsubishiCN105::FrameParser::read_and_parse(uart::UARTDevice &device, Callback &&callback) { + uint8_t watchdog = 64; + while (device.available() > 0 && watchdog-- > 0) { + uint8_t &value = this->read_buffer_[this->read_pos_]; + if (!device.read_byte(&value)) { + ESP_LOGW(TAG, "UART read failed while data available"); + return false; + } + + switch (++this->read_pos_) { + case 1: + if (value != PREAMBLE) { + this->reset_and_dump_buffer_("RX ignoring preamble"); + } + continue; + + case 2: + continue; + + case 3: + if (value != HEADER_BYTE_1) { + this->reset_and_dump_buffer_("RX invalid: header 1 mismatch"); + } + continue; + + case 4: + if (value != HEADER_BYTE_2) { + this->reset_and_dump_buffer_("RX invalid: header 2 mismatch"); + } + continue; + + case HEADER_LEN: + static_assert(READ_BUFFER_SIZE > HEADER_LEN); + if (this->read_buffer_[HEADER_LEN - 1] >= READ_BUFFER_SIZE - HEADER_LEN) { + this->reset_and_dump_buffer_("RX invalid: payload too large"); + } + continue; + + default: + break; + } + + const size_t len_without_checksum = HEADER_LEN + static_cast(this->read_buffer_[HEADER_LEN - 1]); + if (this->read_pos_ <= len_without_checksum) { + continue; + } + + if (checksum(this->read_buffer_, len_without_checksum) != value) { + this->reset_and_dump_buffer_("RX invalid: checksum mismatch"); + continue; + } + + dump_buffer_vv("RX", this->read_buffer_, this->read_pos_); + const bool processed = + callback(this->read_buffer_[1], this->read_buffer_ + HEADER_LEN, len_without_checksum - HEADER_LEN); + this->read_pos_ = 0; + return processed; + } + + return false; +} + +void MitsubishiCN105::FrameParser::reset_and_dump_buffer_(const char *prefix) { + dump_buffer_vv(prefix, this->read_buffer_, this->read_pos_); + this->read_pos_ = 0; +} + +void MitsubishiCN105::FrameParser::dump_buffer_vv(const char *prefix, const uint8_t *data, size_t len) { +#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERY_VERBOSE + char buf[format_hex_pretty_size(READ_BUFFER_SIZE)]; + ESP_LOGVV(TAG, "%s (%zu): %s", prefix, len, format_hex_pretty_to(buf, data, len)); +#endif +} + +} // namespace esphome::mitsubishi_cn105 diff --git a/esphome/components/mitsubishi_cn105/mitsubishi_cn105.h b/esphome/components/mitsubishi_cn105/mitsubishi_cn105.h new file mode 100644 index 0000000000..68d98bf6d9 --- /dev/null +++ b/esphome/components/mitsubishi_cn105/mitsubishi_cn105.h @@ -0,0 +1,140 @@ +#pragma once + +#include +#include "esphome/components/uart/uart.h" + +namespace esphome::mitsubishi_cn105 { + +uint32_t get_loop_time_ms(); + +class MitsubishiCN105 { + public: + enum class Mode : uint8_t { + HEAT, + DRY, + COOL, + FAN_ONLY, + AUTO, + UNKNOWN, + }; + + enum class FanMode : uint8_t { + AUTO, + QUIET, + SPEED_1, + SPEED_2, + SPEED_3, + SPEED_4, + UNKNOWN, + }; + + struct Status { + bool power_on{false}; + float target_temperature{NAN}; + Mode mode{Mode::UNKNOWN}; + FanMode fan_mode{FanMode::UNKNOWN}; + float room_temperature{NAN}; + }; + + explicit MitsubishiCN105(uart::UARTDevice &device) : device_(device) {} + + void initialize(); + bool update(); + + uint32_t get_update_interval() const { return this->update_interval_ms_; } + void set_update_interval(uint32_t interval_ms) { this->update_interval_ms_ = interval_ms; } + + uint32_t get_room_temperature_min_interval() const { return this->room_temperature_min_interval_ms_; } + bool is_room_temperature_enabled() const { return this->room_temperature_min_interval_ms_ != SCHEDULER_DONT_RUN; } + void set_room_temperature_min_interval(uint32_t interval_ms) { + this->room_temperature_min_interval_ms_ = interval_ms; + } + + const Status &status() const { return this->status_; } + bool is_status_initialized() const { + return this->is_room_temperature_enabled() ? !std::isnan(this->status_.room_temperature) + : !std::isnan(this->status_.target_temperature); + } + + void set_power(bool power_on); + void set_target_temperature(float target_temperature); + void set_mode(Mode mode); + void set_fan_mode(FanMode fan_mode); + + protected: + enum class State : uint8_t { + NOT_CONNECTED, + CONNECTING, + CONNECTED, + UPDATING_STATUS, + STATUS_UPDATED, + SCHEDULE_NEXT_STATUS_UPDATE, + WAITING_FOR_SCHEDULED_STATUS_UPDATE, + APPLYING_SETTINGS, + SETTINGS_APPLIED, + READ_TIMEOUT + }; + + class FrameParser { + public: + template bool read_and_parse(uart::UARTDevice &device, Callback &&callback); + void reset() { read_pos_ = 0; } + static void dump_buffer_vv(const char *prefix, const uint8_t *data, size_t len); + + protected: + void reset_and_dump_buffer_(const char *prefix); + + private: + static constexpr size_t READ_BUFFER_SIZE = 32; + uint8_t read_buffer_[READ_BUFFER_SIZE]; + uint8_t read_pos_{0}; + }; + + enum class UpdateFlag : uint8_t { + TEMPERATURE = 1 << 0, + POWER = 1 << 1, + MODE = 1 << 2, + FAN = 1 << 3, + }; + + struct UpdateFlags { + void set(UpdateFlag f) { flags_ |= static_cast(f); } + void clear() { flags_ = 0; } + bool any() const { return flags_ != 0; } + bool has(UpdateFlag f) const { return (flags_ & static_cast(f)) != 0; } + + protected: + uint8_t flags_{0}; + }; + + void set_state_(State new_state); + void did_transition_(State to); + bool process_rx_packet_(uint8_t type, const uint8_t *payload, size_t len); + bool process_status_packet_(const uint8_t *payload, size_t len); + bool parse_status_payload_(uint8_t msg_type, const uint8_t *payload, size_t len); + bool parse_status_settings_(const uint8_t *payload, size_t len); + bool parse_status_room_temperature_(const uint8_t *payload, size_t len); + void send_packet_(const uint8_t *packet, size_t len); + void update_status_(); + void cancel_waiting_and_transition_to_(State state); + bool should_request_room_temperature_() const; + void apply_settings_(); + template void send_packet_(const T &packet) { this->send_packet_(packet.data(), packet.size()); } + static bool should_transition(State from, State to); + static const LogString *state_to_string(State state); + + uart::UARTDevice &device_; + uint32_t update_interval_ms_{1000}; + uint32_t room_temperature_min_interval_ms_{60000}; + std::optional write_timeout_start_ms_; + std::optional status_update_start_ms_; + std::optional last_room_temperature_update_ms_; + Status status_{}; + State state_{State::NOT_CONNECTED}; + UpdateFlags pending_updates_; + bool use_temperature_encoding_b_{false}; + uint8_t current_status_msg_type_{0}; + FrameParser frame_parser_; +}; + +} // namespace esphome::mitsubishi_cn105 diff --git a/esphome/components/mitsubishi_cn105/mitsubishi_cn105_climate.cpp b/esphome/components/mitsubishi_cn105/mitsubishi_cn105_climate.cpp new file mode 100644 index 0000000000..40ddb88a79 --- /dev/null +++ b/esphome/components/mitsubishi_cn105/mitsubishi_cn105_climate.cpp @@ -0,0 +1,159 @@ +#include +#include "mitsubishi_cn105_climate.h" +#include "esphome/core/log.h" + +namespace esphome::mitsubishi_cn105 { + +static const char *const TAG = "mitsubishi_cn105.climate"; + +static constexpr std::array MODE_MAP{ + std::pair{MitsubishiCN105::Mode::AUTO, climate::CLIMATE_MODE_AUTO}, + std::pair{MitsubishiCN105::Mode::HEAT, climate::CLIMATE_MODE_HEAT}, + std::pair{MitsubishiCN105::Mode::DRY, climate::CLIMATE_MODE_DRY}, + std::pair{MitsubishiCN105::Mode::COOL, climate::CLIMATE_MODE_COOL}, + std::pair{MitsubishiCN105::Mode::FAN_ONLY, climate::CLIMATE_MODE_FAN_ONLY}, +}; + +static constexpr std::array FAN_MODE_MAP{ + std::pair{MitsubishiCN105::FanMode::AUTO, climate::CLIMATE_FAN_AUTO}, + std::pair{MitsubishiCN105::FanMode::QUIET, climate::CLIMATE_FAN_QUIET}, + std::pair{MitsubishiCN105::FanMode::SPEED_1, climate::CLIMATE_FAN_LOW}, + std::pair{MitsubishiCN105::FanMode::SPEED_2, climate::CLIMATE_FAN_MEDIUM}, + std::pair{MitsubishiCN105::FanMode::SPEED_3, climate::CLIMATE_FAN_MIDDLE}, + std::pair{MitsubishiCN105::FanMode::SPEED_4, climate::CLIMATE_FAN_HIGH}, +}; + +template +static bool map_lookup(const std::array, N> &map, A key, B &out) { + for (const auto &[from, to] : map) { + if (from == key) { + out = to; + return true; + } + } + return false; +} + +template +static constexpr std::optional reverse_map_lookup(const std::array, N> &map, Right key) { + for (const auto &entry : map) { + if (entry.second == key) { + return entry.first; + } + } + return std::nullopt; +} + +template +static constexpr std::optional reverse_map_lookup(const std::array, N> &map, + const std::optional &key) { + return key.has_value() ? reverse_map_lookup(map, *key) : std::nullopt; +} + +void MitsubishiCN105Climate::dump_config() { + LOG_CLIMATE("", "Mitsubishi CN105 Climate", this); + if (this->hp_.is_room_temperature_enabled()) { + ESP_LOGCONFIG(TAG, " Current temperature min interval: %" PRIu32 " ms", + this->hp_.get_room_temperature_min_interval()); + } else { + ESP_LOGCONFIG(TAG, " Current temperature: disabled"); + } + ESP_LOGCONFIG(TAG, + " Update interval: %" PRIu32 " ms\n" + " UART: baud_rate=%" PRIu32 " data_bits=%u parity=%s stop_bits=%u", + this->hp_.get_update_interval(), this->parent_->get_baud_rate(), this->parent_->get_data_bits(), + LOG_STR_ARG(parity_to_str(this->parent_->get_parity())), this->parent_->get_stop_bits()); +} + +void MitsubishiCN105Climate::setup() { this->hp_.initialize(); } + +void MitsubishiCN105Climate::loop() { + if (this->hp_.update()) { + this->apply_values_(); + } +} + +climate::ClimateTraits MitsubishiCN105Climate::traits() { + climate::ClimateTraits traits; + + traits.set_supported_modes({ + climate::CLIMATE_MODE_OFF, + climate::CLIMATE_MODE_COOL, + climate::CLIMATE_MODE_HEAT, + climate::CLIMATE_MODE_DRY, + climate::CLIMATE_MODE_FAN_ONLY, + climate::CLIMATE_MODE_AUTO, + }); + + traits.set_supported_fan_modes({ + climate::CLIMATE_FAN_AUTO, + climate::CLIMATE_FAN_QUIET, + climate::CLIMATE_FAN_LOW, + climate::CLIMATE_FAN_MEDIUM, + climate::CLIMATE_FAN_MIDDLE, + climate::CLIMATE_FAN_HIGH, + }); + + traits.set_visual_min_temperature(16.0f); + traits.set_visual_max_temperature(31.0f); + traits.set_visual_temperature_step(1.0f); + + if (this->hp_.is_room_temperature_enabled()) { + traits.add_feature_flags(climate::CLIMATE_SUPPORTS_CURRENT_TEMPERATURE); + traits.set_visual_current_temperature_step(0.5f); + } + + return traits; +} + +void MitsubishiCN105Climate::control(const climate::ClimateCall &call) { + if (const auto target_temperature = call.get_target_temperature()) { + this->hp_.set_target_temperature(*target_temperature); + } + + if (const auto mode = call.get_mode()) { + if (*mode == climate::CLIMATE_MODE_OFF) { + this->hp_.set_power(false); + } else if (const auto mapped = reverse_map_lookup(MODE_MAP, *mode)) { + this->hp_.set_power(true); + this->hp_.set_mode(*mapped); + } + } + + if (const auto fan_mode = reverse_map_lookup(FAN_MODE_MAP, call.get_fan_mode())) { + this->hp_.set_fan_mode(*fan_mode); + } + + if (this->hp_.is_status_initialized()) { + this->apply_values_(); + } +} + +void MitsubishiCN105Climate::apply_values_() { + const auto &status = this->hp_.status(); + + this->target_temperature = status.target_temperature; + + if (this->hp_.is_room_temperature_enabled()) { + this->current_temperature = status.room_temperature; + } + + if (status.power_on) { + if (!map_lookup(MODE_MAP, status.mode, this->mode)) { + ESP_LOGD(TAG, "Unable to map mode"); + } + } else { + this->mode = climate::CLIMATE_MODE_OFF; + } + + climate::ClimateFanMode fan_mode; + if (map_lookup(FAN_MODE_MAP, status.fan_mode, fan_mode)) { + this->fan_mode = fan_mode; + } else { + ESP_LOGD(TAG, "Unable to map fan mode"); + } + + this->publish_state(); +} + +} // namespace esphome::mitsubishi_cn105 diff --git a/esphome/components/mitsubishi_cn105/mitsubishi_cn105_climate.h b/esphome/components/mitsubishi_cn105/mitsubishi_cn105_climate.h new file mode 100644 index 0000000000..eee4c20966 --- /dev/null +++ b/esphome/components/mitsubishi_cn105/mitsubishi_cn105_climate.h @@ -0,0 +1,30 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/components/climate/climate.h" +#include "esphome/components/uart/uart.h" +#include "mitsubishi_cn105.h" + +namespace esphome::mitsubishi_cn105 { + +class MitsubishiCN105Climate : public climate::Climate, public Component, public uart::UARTDevice { + public: + explicit MitsubishiCN105Climate() : hp_(*this) {} + + void setup() override; + void loop() override; + void dump_config() override; + + climate::ClimateTraits traits() override; + void control(const climate::ClimateCall &call) override; + + void set_update_interval(uint32_t ms) { hp_.set_update_interval(ms); } + void set_current_temperature_min_interval(uint32_t ms) { hp_.set_room_temperature_min_interval(ms); } + + protected: + void apply_values_(); + + MitsubishiCN105 hp_; +}; + +} // namespace esphome::mitsubishi_cn105 diff --git a/esphome/components/mitsubishi_cn105/mitsubishi_cn105_time.cpp b/esphome/components/mitsubishi_cn105/mitsubishi_cn105_time.cpp new file mode 100644 index 0000000000..0f3fcb5648 --- /dev/null +++ b/esphome/components/mitsubishi_cn105/mitsubishi_cn105_time.cpp @@ -0,0 +1,7 @@ +#include "esphome/core/application.h" + +namespace esphome::mitsubishi_cn105 { + +uint32_t __attribute__((weak)) get_loop_time_ms() { return App.get_loop_component_start_time(); } + +} // namespace esphome::mitsubishi_cn105 diff --git a/esphome/components/mixer/speaker/__init__.py b/esphome/components/mixer/speaker/__init__.py index 63b419cc98..59a80d9297 100644 --- a/esphome/components/mixer/speaker/__init__.py +++ b/esphome/components/mixer/speaker/__init__.py @@ -1,6 +1,6 @@ from esphome import automation import esphome.codegen as cg -from esphome.components import audio, esp32, socket, speaker +from esphome.components import audio, esp32, speaker import esphome.config_validation as cv from esphome.const import ( CONF_BITS_PER_SAMPLE, @@ -111,9 +111,6 @@ FINAL_VALIDATE_SCHEMA = cv.All( async def to_code(config): - # Enable wake_loop_threadsafe for immediate command processing from other tasks - socket.require_wake_loop_threadsafe() - var = cg.new_Pvariable(config[CONF_ID]) await cg.register_component(var, config) diff --git a/esphome/components/mixer/speaker/mixer_speaker.cpp b/esphome/components/mixer/speaker/mixer_speaker.cpp index 9d11abb327..741239a2dd 100644 --- a/esphome/components/mixer/speaker/mixer_speaker.cpp +++ b/esphome/components/mixer/speaker/mixer_speaker.cpp @@ -245,11 +245,9 @@ void SourceSpeaker::send_command_(uint32_t command_bit, bool wake_loop) { uint32_t event_bits = xEventGroupGetBits(this->event_group_); if (!(event_bits & command_bit)) { xEventGroupSetBits(this->event_group_, command_bit); -#if defined(USE_SOCKET_SELECT_SUPPORT) && defined(USE_WAKE_LOOP_THREADSAFE) if (wake_loop) { App.wake_loop_threadsafe(); } -#endif } } @@ -533,9 +531,7 @@ esp_err_t MixerSpeaker::start(audio::AudioStreamInfo &stream_info) { if (!(event_bits & MIXER_TASK_COMMAND_START)) { // Set MIXER_TASK_COMMAND_START bit if not already set, and then immediately wake for low latency xEventGroupSetBits(this->event_group_, MIXER_TASK_COMMAND_START); -#if defined(USE_SOCKET_SELECT_SUPPORT) && defined(USE_WAKE_LOOP_THREADSAFE) App.wake_loop_threadsafe(); -#endif } return ESP_OK; @@ -597,173 +593,173 @@ void MixerSpeaker::audio_mixer_task(void *params) { xEventGroupSetBits(this_mixer->event_group_, MIXER_TASK_STATE_STARTING); - std::unique_ptr output_transfer_buffer = audio::AudioSinkTransferBuffer::create( - this_mixer->audio_stream_info_.value().ms_to_bytes(TRANSFER_BUFFER_DURATION_MS)); + { // Ensure C++ objects fall out of scope to ensure proper cleanup before stopping the task + std::unique_ptr output_transfer_buffer = audio::AudioSinkTransferBuffer::create( + this_mixer->audio_stream_info_.value().ms_to_bytes(TRANSFER_BUFFER_DURATION_MS)); - if (output_transfer_buffer == nullptr) { - xEventGroupSetBits(this_mixer->event_group_, MIXER_TASK_STATE_STOPPED | MIXER_TASK_ERR_ESP_NO_MEM); + if (output_transfer_buffer == nullptr) { + xEventGroupSetBits(this_mixer->event_group_, MIXER_TASK_STATE_STOPPED | MIXER_TASK_ERR_ESP_NO_MEM); - vTaskSuspend(nullptr); // Suspend this task indefinitely until the loop method deletes it - } - - output_transfer_buffer->set_sink(this_mixer->output_speaker_); - - xEventGroupSetBits(this_mixer->event_group_, MIXER_TASK_STATE_RUNNING); - - bool sent_finished = false; - - // Pre-allocate vectors to avoid heap allocation in the loop (max 8 source speakers per schema) - FixedVector speakers_with_data; - FixedVector> transfer_buffers_with_data; - speakers_with_data.init(this_mixer->source_speakers_.size()); - transfer_buffers_with_data.init(this_mixer->source_speakers_.size()); - - while (true) { - uint32_t event_group_bits = xEventGroupGetBits(this_mixer->event_group_); - if (event_group_bits & MIXER_TASK_COMMAND_STOP) { - break; + vTaskSuspend(nullptr); // Suspend this task indefinitely until the loop method deletes it } - // Never shift the data in the output transfer buffer to avoid unnecessary, slow data moves - output_transfer_buffer->transfer_data_to_sink(pdMS_TO_TICKS(TASK_DELAY_MS), false); + output_transfer_buffer->set_sink(this_mixer->output_speaker_); - const uint32_t output_frames_free = - this_mixer->audio_stream_info_.value().bytes_to_frames(output_transfer_buffer->free()); + xEventGroupSetBits(this_mixer->event_group_, MIXER_TASK_STATE_RUNNING); - speakers_with_data.clear(); - transfer_buffers_with_data.clear(); + bool sent_finished = false; - for (auto &speaker : this_mixer->source_speakers_) { - if (speaker->is_running() && !speaker->get_pause_state()) { - // Speaker is running and not paused, so it possibly can provide audio data - std::shared_ptr transfer_buffer = speaker->get_transfer_buffer().lock(); - if (transfer_buffer.use_count() == 0) { - // No transfer buffer allocated, so skip processing this speaker - continue; - } - speaker->process_data_from_source(transfer_buffer, 0); // Transfers and ducks audio from source ring buffers + // Pre-allocate vectors to avoid heap allocation in the loop (max 8 source speakers per schema) + FixedVector speakers_with_data; + FixedVector> transfer_buffers_with_data; + speakers_with_data.init(this_mixer->source_speakers_.size()); + transfer_buffers_with_data.init(this_mixer->source_speakers_.size()); - if (transfer_buffer->available() > 0) { - // Store the locked transfer buffers in their own vector to avoid releasing ownership until after the loop - transfer_buffers_with_data.push_back(transfer_buffer); - speakers_with_data.push_back(speaker); + while (true) { + uint32_t event_group_bits = xEventGroupGetBits(this_mixer->event_group_); + if (event_group_bits & MIXER_TASK_COMMAND_STOP) { + break; + } + + // Never shift the data in the output transfer buffer to avoid unnecessary, slow data moves + output_transfer_buffer->transfer_data_to_sink(pdMS_TO_TICKS(TASK_DELAY_MS), false); + + const uint32_t output_frames_free = + this_mixer->audio_stream_info_.value().bytes_to_frames(output_transfer_buffer->free()); + + speakers_with_data.clear(); + transfer_buffers_with_data.clear(); + + for (auto &speaker : this_mixer->source_speakers_) { + if (speaker->is_running() && !speaker->get_pause_state()) { + // Speaker is running and not paused, so it possibly can provide audio data + std::shared_ptr transfer_buffer = speaker->get_transfer_buffer().lock(); + if (transfer_buffer.use_count() == 0) { + // No transfer buffer allocated, so skip processing this speaker + continue; + } + speaker->process_data_from_source(transfer_buffer, 0); // Transfers and ducks audio from source ring buffers + + if (transfer_buffer->available() > 0) { + // Store the locked transfer buffers in their own vector to avoid releasing ownership until after the loop + transfer_buffers_with_data.push_back(transfer_buffer); + speakers_with_data.push_back(speaker); + } } } - } - if (transfer_buffers_with_data.empty()) { - // No audio available for transferring, block task temporarily - delay(TASK_DELAY_MS); - continue; - } + if (transfer_buffers_with_data.empty()) { + // No audio available for transferring, block task temporarily + delay(TASK_DELAY_MS); + continue; + } - uint32_t frames_to_mix = output_frames_free; + uint32_t frames_to_mix = output_frames_free; - if ((transfer_buffers_with_data.size() == 1) || this_mixer->queue_mode_) { - // Only one speaker has audio data, just copy samples over + if ((transfer_buffers_with_data.size() == 1) || this_mixer->queue_mode_) { + // Only one speaker has audio data, just copy samples over - audio::AudioStreamInfo active_stream_info = speakers_with_data[0]->get_audio_stream_info(); + audio::AudioStreamInfo active_stream_info = speakers_with_data[0]->get_audio_stream_info(); - if (active_stream_info.get_sample_rate() == - this_mixer->output_speaker_->get_audio_stream_info().get_sample_rate()) { - // Speaker's sample rate matches the output speaker's, copy directly + if (active_stream_info.get_sample_rate() == + this_mixer->output_speaker_->get_audio_stream_info().get_sample_rate()) { + // Speaker's sample rate matches the output speaker's, copy directly - const uint32_t frames_available_in_buffer = - active_stream_info.bytes_to_frames(transfer_buffers_with_data[0]->available()); - frames_to_mix = std::min(frames_to_mix, frames_available_in_buffer); - copy_frames(reinterpret_cast(transfer_buffers_with_data[0]->get_buffer_start()), active_stream_info, - reinterpret_cast(output_transfer_buffer->get_buffer_end()), - this_mixer->audio_stream_info_.value(), frames_to_mix); + const uint32_t frames_available_in_buffer = + active_stream_info.bytes_to_frames(transfer_buffers_with_data[0]->available()); + frames_to_mix = std::min(frames_to_mix, frames_available_in_buffer); + copy_frames(reinterpret_cast(transfer_buffers_with_data[0]->get_buffer_start()), + active_stream_info, reinterpret_cast(output_transfer_buffer->get_buffer_end()), + this_mixer->audio_stream_info_.value(), frames_to_mix); - // Set playback delay for newly contributing source - if (!speakers_with_data[0]->has_contributed_.load(std::memory_order_acquire)) { - speakers_with_data[0]->playback_delay_frames_.store( - this_mixer->frames_in_pipeline_.load(std::memory_order_acquire), std::memory_order_release); - speakers_with_data[0]->has_contributed_.store(true, std::memory_order_release); + // Set playback delay for newly contributing source + if (!speakers_with_data[0]->has_contributed_.load(std::memory_order_acquire)) { + speakers_with_data[0]->playback_delay_frames_.store( + this_mixer->frames_in_pipeline_.load(std::memory_order_acquire), std::memory_order_release); + speakers_with_data[0]->has_contributed_.store(true, std::memory_order_release); + } + + // Update source speaker pending frames + speakers_with_data[0]->pending_playback_frames_.fetch_add(frames_to_mix, std::memory_order_release); + transfer_buffers_with_data[0]->decrease_buffer_length(active_stream_info.frames_to_bytes(frames_to_mix)); + + // Update output transfer buffer length and pipeline frame count + output_transfer_buffer->increase_buffer_length( + this_mixer->audio_stream_info_.value().frames_to_bytes(frames_to_mix)); + this_mixer->frames_in_pipeline_.fetch_add(frames_to_mix, std::memory_order_release); + } else { + // Speaker's stream info doesn't match the output speaker's, so it's a new source speaker + if (!this_mixer->output_speaker_->is_stopped()) { + if (!sent_finished) { + this_mixer->output_speaker_->finish(); + sent_finished = true; // Avoid repeatedly sending the finish command + } + } else { + // Speaker has finished writing the current audio, update the stream information and restart the speaker + this_mixer->audio_stream_info_ = + audio::AudioStreamInfo(active_stream_info.get_bits_per_sample(), this_mixer->output_channels_, + active_stream_info.get_sample_rate()); + this_mixer->output_speaker_->set_audio_stream_info(this_mixer->audio_stream_info_.value()); + this_mixer->output_speaker_->start(); + // Reset pipeline frame count since we're starting fresh with a new sample rate + this_mixer->frames_in_pipeline_.store(0, std::memory_order_release); + sent_finished = false; + } + } + } else { + // Determine how many frames to mix + for (size_t i = 0; i < transfer_buffers_with_data.size(); ++i) { + const uint32_t frames_available_in_buffer = speakers_with_data[i]->get_audio_stream_info().bytes_to_frames( + transfer_buffers_with_data[i]->available()); + frames_to_mix = std::min(frames_to_mix, frames_available_in_buffer); + } + int16_t *primary_buffer = reinterpret_cast(transfer_buffers_with_data[0]->get_buffer_start()); + audio::AudioStreamInfo primary_stream_info = speakers_with_data[0]->get_audio_stream_info(); + + // Mix two streams together + for (size_t i = 1; i < transfer_buffers_with_data.size(); ++i) { + mix_audio_samples(primary_buffer, primary_stream_info, + reinterpret_cast(transfer_buffers_with_data[i]->get_buffer_start()), + speakers_with_data[i]->get_audio_stream_info(), + reinterpret_cast(output_transfer_buffer->get_buffer_end()), + this_mixer->audio_stream_info_.value(), frames_to_mix); + + if (i != transfer_buffers_with_data.size() - 1) { + // Need to mix more streams together, point primary buffer and stream info to the already mixed output + primary_buffer = reinterpret_cast(output_transfer_buffer->get_buffer_end()); + primary_stream_info = this_mixer->audio_stream_info_.value(); + } } - // Update source speaker pending frames - speakers_with_data[0]->pending_playback_frames_.fetch_add(frames_to_mix, std::memory_order_release); - transfer_buffers_with_data[0]->decrease_buffer_length(active_stream_info.frames_to_bytes(frames_to_mix)); + // Get current pipeline depth for delay calculation (before incrementing) + uint32_t current_pipeline_frames = this_mixer->frames_in_pipeline_.load(std::memory_order_acquire); - // Update output transfer buffer length and pipeline frame count + // Update source transfer buffer lengths and add new audio durations to the source speaker pending playbacks + for (size_t i = 0; i < transfer_buffers_with_data.size(); ++i) { + // Set playback delay for newly contributing sources + if (!speakers_with_data[i]->has_contributed_.load(std::memory_order_acquire)) { + speakers_with_data[i]->playback_delay_frames_.store(current_pipeline_frames, std::memory_order_release); + speakers_with_data[i]->has_contributed_.store(true, std::memory_order_release); + } + + speakers_with_data[i]->pending_playback_frames_.fetch_add(frames_to_mix, std::memory_order_release); + transfer_buffers_with_data[i]->decrease_buffer_length( + speakers_with_data[i]->get_audio_stream_info().frames_to_bytes(frames_to_mix)); + } + + // Update output transfer buffer length and pipeline frame count (once, not per source) output_transfer_buffer->increase_buffer_length( this_mixer->audio_stream_info_.value().frames_to_bytes(frames_to_mix)); this_mixer->frames_in_pipeline_.fetch_add(frames_to_mix, std::memory_order_release); - } else { - // Speaker's stream info doesn't match the output speaker's, so it's a new source speaker - if (!this_mixer->output_speaker_->is_stopped()) { - if (!sent_finished) { - this_mixer->output_speaker_->finish(); - sent_finished = true; // Avoid repeatedly sending the finish command - } - } else { - // Speaker has finished writing the current audio, update the stream information and restart the speaker - this_mixer->audio_stream_info_ = - audio::AudioStreamInfo(active_stream_info.get_bits_per_sample(), this_mixer->output_channels_, - active_stream_info.get_sample_rate()); - this_mixer->output_speaker_->set_audio_stream_info(this_mixer->audio_stream_info_.value()); - this_mixer->output_speaker_->start(); - // Reset pipeline frame count since we're starting fresh with a new sample rate - this_mixer->frames_in_pipeline_.store(0, std::memory_order_release); - sent_finished = false; - } } - } else { - // Determine how many frames to mix - for (size_t i = 0; i < transfer_buffers_with_data.size(); ++i) { - const uint32_t frames_available_in_buffer = - speakers_with_data[i]->get_audio_stream_info().bytes_to_frames(transfer_buffers_with_data[i]->available()); - frames_to_mix = std::min(frames_to_mix, frames_available_in_buffer); - } - int16_t *primary_buffer = reinterpret_cast(transfer_buffers_with_data[0]->get_buffer_start()); - audio::AudioStreamInfo primary_stream_info = speakers_with_data[0]->get_audio_stream_info(); - - // Mix two streams together - for (size_t i = 1; i < transfer_buffers_with_data.size(); ++i) { - mix_audio_samples(primary_buffer, primary_stream_info, - reinterpret_cast(transfer_buffers_with_data[i]->get_buffer_start()), - speakers_with_data[i]->get_audio_stream_info(), - reinterpret_cast(output_transfer_buffer->get_buffer_end()), - this_mixer->audio_stream_info_.value(), frames_to_mix); - - if (i != transfer_buffers_with_data.size() - 1) { - // Need to mix more streams together, point primary buffer and stream info to the already mixed output - primary_buffer = reinterpret_cast(output_transfer_buffer->get_buffer_end()); - primary_stream_info = this_mixer->audio_stream_info_.value(); - } - } - - // Get current pipeline depth for delay calculation (before incrementing) - uint32_t current_pipeline_frames = this_mixer->frames_in_pipeline_.load(std::memory_order_acquire); - - // Update source transfer buffer lengths and add new audio durations to the source speaker pending playbacks - for (size_t i = 0; i < transfer_buffers_with_data.size(); ++i) { - // Set playback delay for newly contributing sources - if (!speakers_with_data[i]->has_contributed_.load(std::memory_order_acquire)) { - speakers_with_data[i]->playback_delay_frames_.store(current_pipeline_frames, std::memory_order_release); - speakers_with_data[i]->has_contributed_.store(true, std::memory_order_release); - } - - speakers_with_data[i]->pending_playback_frames_.fetch_add(frames_to_mix, std::memory_order_release); - transfer_buffers_with_data[i]->decrease_buffer_length( - speakers_with_data[i]->get_audio_stream_info().frames_to_bytes(frames_to_mix)); - } - - // Update output transfer buffer length and pipeline frame count (once, not per source) - output_transfer_buffer->increase_buffer_length( - this_mixer->audio_stream_info_.value().frames_to_bytes(frames_to_mix)); - this_mixer->frames_in_pipeline_.fetch_add(frames_to_mix, std::memory_order_release); } - } - xEventGroupSetBits(this_mixer->event_group_, MIXER_TASK_STATE_STOPPING); + xEventGroupSetBits(this_mixer->event_group_, MIXER_TASK_STATE_STOPPING); + } // Reset pipeline frame count since the task is stopping this_mixer->frames_in_pipeline_.store(0, std::memory_order_release); - output_transfer_buffer.reset(); - xEventGroupSetBits(this_mixer->event_group_, MIXER_TASK_STATE_STOPPED); vTaskSuspend(nullptr); // Suspend this task indefinitely until the loop method deletes it diff --git a/esphome/components/mlx90393/sensor.py b/esphome/components/mlx90393/sensor.py index d93c379506..a6330b1cc0 100644 --- a/esphome/components/mlx90393/sensor.py +++ b/esphome/components/mlx90393/sensor.py @@ -10,6 +10,7 @@ from esphome.const import ( CONF_RESOLUTION, CONF_TEMPERATURE, CONF_TEMPERATURE_COMPENSATION, + DEVICE_CLASS_TEMPERATURE, ICON_MAGNET, ICON_THERMOMETER, STATE_CLASS_MEASUREMENT, @@ -107,6 +108,7 @@ CONFIG_SCHEMA = cv.All( unit_of_measurement=UNIT_CELSIUS, accuracy_decimals=1, icon=ICON_THERMOMETER, + device_class=DEVICE_CLASS_TEMPERATURE, state_class=STATE_CLASS_MEASUREMENT, ).extend( cv.Schema( @@ -130,9 +132,6 @@ async def to_code(config): await cg.register_component(var, config) await i2c.register_i2c_device(var, config) - if CONF_DRDY_PIN in config: - pin = await cg.gpio_pin_expression(config[CONF_DRDY_PIN]) - cg.add(var.set_drdy_pin(pin)) cg.add(var.set_gain(GAIN[config[CONF_GAIN]])) cg.add(var.set_oversampling(config[CONF_OVERSAMPLING])) cg.add(var.set_filter(config[CONF_FILTER])) diff --git a/esphome/components/mmc5603/mmc5603.cpp b/esphome/components/mmc5603/mmc5603.cpp index 1cbc84191f..51b94eb767 100644 --- a/esphome/components/mmc5603/mmc5603.cpp +++ b/esphome/components/mmc5603/mmc5603.cpp @@ -126,21 +126,21 @@ void MMC5603Component::update() { int32_t raw_x = 0; raw_x |= buffer[0] << 12; raw_x |= buffer[1] << 4; - raw_x |= buffer[2] << 0; + raw_x |= buffer[2] & 0x0F; const float x = 0.00625 * (raw_x - 524288); int32_t raw_y = 0; raw_y |= buffer[3] << 12; raw_y |= buffer[4] << 4; - raw_y |= buffer[5] << 0; + raw_y |= buffer[5] & 0x0F; const float y = 0.00625 * (raw_y - 524288); int32_t raw_z = 0; raw_z |= buffer[6] << 12; raw_z |= buffer[7] << 4; - raw_z |= buffer[8] << 0; + raw_z |= buffer[8] & 0x0F; const float z = 0.00625 * (raw_z - 524288); diff --git a/esphome/components/mmc5603/sensor.py b/esphome/components/mmc5603/sensor.py index 5b3982cee6..6d2bafdd0e 100644 --- a/esphome/components/mmc5603/sensor.py +++ b/esphome/components/mmc5603/sensor.py @@ -45,6 +45,7 @@ heading_schema = sensor.sensor_schema( unit_of_measurement=UNIT_DEGREES, icon=ICON_SCREEN_ROTATION, accuracy_decimals=1, + state_class=STATE_CLASS_MEASUREMENT, ) CONFIG_SCHEMA = ( diff --git a/esphome/components/modbus/helpers.py b/esphome/components/modbus/helpers.py new file mode 100644 index 0000000000..6f97f1e605 --- /dev/null +++ b/esphome/components/modbus/helpers.py @@ -0,0 +1,83 @@ +import esphome.codegen as cg + +modbus_ns = cg.esphome_ns.namespace("modbus") +modbus_helpers_ns = modbus_ns.namespace("helpers") + +ModbusFunctionCode_ns = modbus_ns.namespace("ModbusFunctionCode") +ModbusFunctionCode = ModbusFunctionCode_ns.enum("ModbusFunctionCode") + +MODBUS_FUNCTION_CODE = { + "read_coils": ModbusFunctionCode.READ_COILS, + "read_discrete_inputs": ModbusFunctionCode.READ_DISCRETE_INPUTS, + "read_holding_registers": ModbusFunctionCode.READ_HOLDING_REGISTERS, + "read_input_registers": ModbusFunctionCode.READ_INPUT_REGISTERS, + "write_single_coil": ModbusFunctionCode.WRITE_SINGLE_COIL, + "write_single_register": ModbusFunctionCode.WRITE_SINGLE_REGISTER, + "write_multiple_coils": ModbusFunctionCode.WRITE_MULTIPLE_COILS, + "write_multiple_registers": ModbusFunctionCode.WRITE_MULTIPLE_REGISTERS, +} + +ModbusRegisterType_ns = modbus_ns.namespace("ModbusRegisterType") +ModbusRegisterType = ModbusRegisterType_ns.enum("ModbusRegisterType") + +MODBUS_WRITE_REGISTER_TYPE = { + "custom": ModbusRegisterType.CUSTOM, + "coil": ModbusRegisterType.COIL, + "holding": ModbusRegisterType.HOLDING, +} + +MODBUS_REGISTER_TYPE = { + **MODBUS_WRITE_REGISTER_TYPE, + "discrete_input": ModbusRegisterType.DISCRETE_INPUT, + "read": ModbusRegisterType.READ, +} + +SensorValueType_ns = modbus_helpers_ns.namespace("SensorValueType") +SensorValueType = SensorValueType_ns.enum("SensorValueType") +SENSOR_VALUE_TYPE = { + "RAW": SensorValueType.RAW, + "U_WORD": SensorValueType.U_WORD, + "S_WORD": SensorValueType.S_WORD, + "U_DWORD": SensorValueType.U_DWORD, + "U_DWORD_R": SensorValueType.U_DWORD_R, + "S_DWORD": SensorValueType.S_DWORD, + "S_DWORD_R": SensorValueType.S_DWORD_R, + "U_QWORD": SensorValueType.U_QWORD, + "U_QWORD_R": SensorValueType.U_QWORD_R, + "S_QWORD": SensorValueType.S_QWORD, + "S_QWORD_R": SensorValueType.S_QWORD_R, + "FP32": SensorValueType.FP32, + "FP32_R": SensorValueType.FP32_R, +} + +TYPE_REGISTER_MAP = { + "RAW": 1, + "U_WORD": 1, + "S_WORD": 1, + "U_DWORD": 2, + "U_DWORD_R": 2, + "S_DWORD": 2, + "S_DWORD_R": 2, + "U_QWORD": 4, + "U_QWORD_R": 4, + "S_QWORD": 4, + "S_QWORD_R": 4, + "FP32": 2, + "FP32_R": 2, +} + +CPP_TYPE_REGISTER_MAP = { + "RAW": cg.uint16, + "U_WORD": cg.uint16, + "S_WORD": cg.int16, + "U_DWORD": cg.uint32, + "U_DWORD_R": cg.uint32, + "S_DWORD": cg.int32, + "S_DWORD_R": cg.int32, + "U_QWORD": cg.uint64, + "U_QWORD_R": cg.uint64, + "S_QWORD": cg.int64, + "S_QWORD_R": cg.int64, + "FP32": cg.float_, + "FP32_R": cg.float_, +} diff --git a/esphome/components/modbus/modbus.cpp b/esphome/components/modbus/modbus.cpp index 4146a54c87..3b1a038be3 100644 --- a/esphome/components/modbus/modbus.cpp +++ b/esphome/components/modbus/modbus.cpp @@ -313,7 +313,7 @@ void Modbus::send_next_frame_() { this->last_send_ = millis(); this->tx_buffer_.pop_front(); if (!this->tx_buffer_.empty()) { - ESP_LOGV(TAG, "Write queue contains %" PRIu32 " items.", this->tx_buffer_.size()); + ESP_LOGV(TAG, "Write queue contains %zu items.", this->tx_buffer_.size()); } } diff --git a/esphome/components/modbus/modbus_helpers.cpp b/esphome/components/modbus/modbus_helpers.cpp new file mode 100644 index 0000000000..77190b2846 --- /dev/null +++ b/esphome/components/modbus/modbus_helpers.cpp @@ -0,0 +1,139 @@ +#include "modbus_helpers.h" +#include "esphome/core/log.h" + +namespace esphome::modbus::helpers { + +static const char *const TAG = "modbus_helpers"; + +void number_to_payload(std::vector &data, int64_t value, SensorValueType value_type) { + switch (value_type) { + case SensorValueType::U_WORD: + case SensorValueType::S_WORD: + data.push_back(value & 0xFFFF); + break; + case SensorValueType::U_DWORD: + case SensorValueType::S_DWORD: + case SensorValueType::FP32: + data.push_back((value & 0xFFFF0000) >> 16); + data.push_back(value & 0xFFFF); + break; + case SensorValueType::U_DWORD_R: + case SensorValueType::S_DWORD_R: + case SensorValueType::FP32_R: + data.push_back(value & 0xFFFF); + data.push_back((value & 0xFFFF0000) >> 16); + break; + case SensorValueType::U_QWORD: + case SensorValueType::S_QWORD: + data.push_back((value & 0xFFFF000000000000) >> 48); + data.push_back((value & 0xFFFF00000000) >> 32); + data.push_back((value & 0xFFFF0000) >> 16); + data.push_back(value & 0xFFFF); + break; + case SensorValueType::U_QWORD_R: + case SensorValueType::S_QWORD_R: + data.push_back(value & 0xFFFF); + data.push_back((value & 0xFFFF0000) >> 16); + data.push_back((value & 0xFFFF00000000) >> 32); + data.push_back((value & 0xFFFF000000000000) >> 48); + break; + default: + ESP_LOGE(TAG, "Invalid data type for modbus number to payload conversion: %d", static_cast(value_type)); + break; + } +} + +int64_t payload_to_number(const std::vector &data, SensorValueType sensor_value_type, uint8_t offset, + uint32_t bitmask) { + int64_t value = 0; // int64_t because it can hold signed and unsigned 32 bits + + if (offset > data.size()) { + ESP_LOGE(TAG, "not enough data for value"); + return value; + } + + size_t size = data.size() - offset; + bool error = false; + switch (sensor_value_type) { + case SensorValueType::U_WORD: + if (size >= 2) { + value = mask_and_shift_by_rightbit(get_data(data, offset), + bitmask); // default is 0xFFFF ; + } else { + error = true; + } + break; + case SensorValueType::U_DWORD: + case SensorValueType::FP32: + if (size >= 4) { + value = get_data(data, offset); + value = mask_and_shift_by_rightbit((uint32_t) value, bitmask); + } else { + error = true; + } + break; + case SensorValueType::U_DWORD_R: + case SensorValueType::FP32_R: + if (size >= 4) { + value = get_data(data, offset); + value = static_cast(value & 0xFFFF) << 16 | (value & 0xFFFF0000) >> 16; + value = mask_and_shift_by_rightbit((uint32_t) value, bitmask); + } else { + error = true; + } + break; + case SensorValueType::S_WORD: + if (size >= 2) { + value = mask_and_shift_by_rightbit(get_data(data, offset), + bitmask); // default is 0xFFFF ; + } else { + error = true; + } + break; + case SensorValueType::S_DWORD: + if (size >= 4) { + value = mask_and_shift_by_rightbit(get_data(data, offset), bitmask); + } else { + error = true; + } + break; + case SensorValueType::S_DWORD_R: { + if (size >= 4) { + value = get_data(data, offset); + // Currently the high word is at the low position + // the sign bit is therefore at low before the switch + uint32_t sign_bit = (value & 0x8000) << 16; + value = mask_and_shift_by_rightbit( + static_cast(((value & 0x7FFF) << 16 | (value & 0xFFFF0000) >> 16) | sign_bit), bitmask); + } else { + error = true; + } + } break; + case SensorValueType::U_QWORD: + case SensorValueType::S_QWORD: + // Ignore bitmask for QWORD + if (size >= 8) { + value = get_data(data, offset); + } else { + error = true; + } + break; + case SensorValueType::U_QWORD_R: + case SensorValueType::S_QWORD_R: { + // Ignore bitmask for QWORD + if (size >= 8) { + uint64_t tmp = get_data(data, offset); + value = (tmp << 48) | (tmp >> 48) | ((tmp & 0xFFFF0000) << 16) | ((tmp >> 16) & 0xFFFF0000); + } else { + error = true; + } + } break; + case SensorValueType::RAW: + default: + break; + } + if (error) + ESP_LOGE(TAG, "not enough data for value"); + return value; +} +} // namespace esphome::modbus::helpers diff --git a/esphome/components/modbus/modbus_helpers.h b/esphome/components/modbus/modbus_helpers.h new file mode 100644 index 0000000000..84897bcad3 --- /dev/null +++ b/esphome/components/modbus/modbus_helpers.h @@ -0,0 +1,207 @@ +#pragma once + +#include +#include +#include + +#include "esphome/core/helpers.h" +#include "esphome/components/modbus/modbus_definitions.h" + +namespace esphome::modbus::helpers { + +enum class SensorValueType : uint8_t { + RAW = 0x00, // variable length + U_WORD = 0x1, // 1 Register unsigned + U_DWORD = 0x2, // 2 Registers unsigned + S_WORD = 0x3, // 1 Register signed + S_DWORD = 0x4, // 2 Registers signed + BIT = 0x5, + U_DWORD_R = 0x6, // 2 Registers unsigned + S_DWORD_R = 0x7, // 2 Registers unsigned + U_QWORD = 0x8, + S_QWORD = 0x9, + U_QWORD_R = 0xA, + S_QWORD_R = 0xB, + FP32 = 0xC, + FP32_R = 0xD +}; + +inline bool value_type_is_float(SensorValueType v) { + return v == SensorValueType::FP32 || v == SensorValueType::FP32_R; +} + +inline ModbusFunctionCode modbus_register_read_function(ModbusRegisterType reg_type) { + switch (reg_type) { + case ModbusRegisterType::COIL: + return ModbusFunctionCode::READ_COILS; + case ModbusRegisterType::DISCRETE_INPUT: + return ModbusFunctionCode::READ_DISCRETE_INPUTS; + case ModbusRegisterType::HOLDING: + return ModbusFunctionCode::READ_HOLDING_REGISTERS; + case ModbusRegisterType::READ: + return ModbusFunctionCode::READ_INPUT_REGISTERS; + default: + return ModbusFunctionCode::CUSTOM; + } +} + +inline ModbusFunctionCode modbus_register_write_function(ModbusRegisterType reg_type) { + switch (reg_type) { + case ModbusRegisterType::COIL: + return ModbusFunctionCode::WRITE_SINGLE_COIL; + case ModbusRegisterType::DISCRETE_INPUT: + return ModbusFunctionCode::CUSTOM; + case ModbusRegisterType::HOLDING: + return ModbusFunctionCode::READ_WRITE_MULTIPLE_REGISTERS; + case ModbusRegisterType::READ: + default: + return ModbusFunctionCode::CUSTOM; + } +} + +inline uint8_t c_to_hex(char c) { return (c >= 'A') ? (c >= 'a') ? (c - 'a' + 10) : (c - 'A' + 10) : (c - '0'); } + +/** Get a byte from a hex string + * byte_from_hex_str("1122", 1) returns uint_8 value 0x22 == 34 + * byte_from_hex_str("1122", 0) returns 0x11 + * @param value string containing hex encoding + * @param position offset in bytes. Because each byte is encoded in 2 hex digits the position of the original byte in + * the hex string is byte_pos * 2 + * @return byte value + */ +inline uint8_t byte_from_hex_str(const std::string &value, uint8_t pos) { + if (value.length() < pos * 2 + 2) + return 0; + return (c_to_hex(value[pos * 2]) << 4) | c_to_hex(value[pos * 2 + 1]); +} + +/** Get a word from a hex string + * @param value string containing hex encoding + * @param position offset in bytes. Because each byte is encoded in 2 hex digits the position of the original byte in + * the hex string is byte_pos * 2 + * @return word value + */ +inline uint16_t word_from_hex_str(const std::string &value, uint8_t pos) { + return byte_from_hex_str(value, pos) << 8 | byte_from_hex_str(value, pos + 1); +} + +/** Get a dword from a hex string + * @param value string containing hex encoding + * @param position offset in bytes. Because each byte is encoded in 2 hex digits the position of the original byte in + * the hex string is byte_pos * 2 + * @return dword value + */ +inline uint32_t dword_from_hex_str(const std::string &value, uint8_t pos) { + return word_from_hex_str(value, pos) << 16 | word_from_hex_str(value, pos + 2); +} + +/** Get a qword from a hex string + * @param value string containing hex encoding + * @param position offset in bytes. Because each byte is encoded in 2 hex digits the position of the original byte in + * the hex string is byte_pos * 2 + * @return qword value + */ +inline uint64_t qword_from_hex_str(const std::string &value, uint8_t pos) { + return static_cast(dword_from_hex_str(value, pos)) << 32 | dword_from_hex_str(value, pos + 4); +} + +// Extract data from modbus response buffer +/** Extract data from modbus response buffer + * @param T one of supported integer data types int_8,int_16,int_32,int_64 + * @param data modbus response buffer (uint8_t) + * @param buffer_offset offset in bytes. + * @return value of type T extracted from buffer + */ +template T get_data(const std::vector &data, size_t buffer_offset) { + if (sizeof(T) == sizeof(uint8_t)) { + return T(data[buffer_offset]); + } + if (sizeof(T) == sizeof(uint16_t)) { + return T((uint16_t(data[buffer_offset + 0]) << 8) | (uint16_t(data[buffer_offset + 1]) << 0)); + } + + if (sizeof(T) == sizeof(uint32_t)) { + return static_cast(get_data(data, buffer_offset)) << 16 | + static_cast(get_data(data, buffer_offset + 2)); + } + + if (sizeof(T) == sizeof(uint64_t)) { + return static_cast(get_data(data, buffer_offset)) << 32 | + (static_cast(get_data(data, buffer_offset + 4))); + } + + static_assert(sizeof(T) == sizeof(uint8_t) || sizeof(T) == sizeof(uint16_t) || sizeof(T) == sizeof(uint32_t) || + sizeof(T) == sizeof(uint64_t), + "Unsupported type size in get_data; only 1, 2, 4, or 8-byte integer types are supported."); + + return T{}; +} + +/** Extract coil data from modbus response buffer + * Responses for coil are packed into bytes . + * coil 3 is bit 3 of the first response byte + * coil 9 is bit 2 of the second response byte + * @param coil number of the cil + * @param data modbus response buffer (uint8_t) + * @return content of coil register + */ +inline bool coil_from_vector(int coil, const std::vector &data) { + auto data_byte = coil / 8; + return (data[data_byte] & (1 << (coil % 8))) > 0; +} + +/** Extract bits from value and shift right according to the bitmask + * if the bitmask is 0x00F0 we want the values frrom bit 5 - 8. + * the result is then shifted right by the position if the first right set bit in the mask + * Useful for modbus data where more than one value is packed in a 16 bit register + * Example: on Epever the "Length of night" register 0x9065 encodes values of the whole night length of time as + * D15 - D8 = hour, D7 - D0 = minute + * To get the hours use mask 0xFF00 and 0x00FF for the minute + * @param data an integral value between 16 aand 32 bits, + * @param bitmask the bitmask to apply + */ +template N mask_and_shift_by_rightbit(N data, uint32_t mask) { + auto result = (mask & data); + if (result == 0 || mask == 0xFFFFFFFF) { + return result; + } + for (size_t pos = 0; pos < sizeof(N) << 3; pos++) { + if (pos < 32 && (mask & (1UL << pos)) != 0) + return result >> pos; + } + return 0; +} + +/** Convert float value to vector suitable for sending + * @param data target for payload + * @param value float value to convert + * @param value_type defines if 16/32 or FP32 is used + * @return vector containing the modbus register words in correct order + */ +void number_to_payload(std::vector &data, int64_t value, SensorValueType value_type); + +/** Convert vector response payload to number. + * @param data payload with the data to convert + * @param sensor_value_type defines if 16/32/64 bits or FP32 is used + * @param offset offset to the data in data + * @param bitmask bitmask used for masking and shifting + * @return 64-bit number of the payload + */ +int64_t payload_to_number(const std::vector &data, SensorValueType sensor_value_type, uint8_t offset, + uint32_t bitmask); + +inline std::vector float_to_payload(float value, SensorValueType value_type) { + int64_t val; + + if (value_type_is_float(value_type)) { + val = bit_cast(value); + } else { + val = llroundf(value); + } + + std::vector data; + number_to_payload(data, val, value_type); + return data; +} + +} // namespace esphome::modbus::helpers diff --git a/esphome/components/modbus_controller/__init__.py b/esphome/components/modbus_controller/__init__.py index aea79b2053..9e332425a6 100644 --- a/esphome/components/modbus_controller/__init__.py +++ b/esphome/components/modbus_controller/__init__.py @@ -4,15 +4,15 @@ from esphome import automation import esphome.codegen as cg from esphome.components import modbus from esphome.components.const import CONF_ENABLED -import esphome.config_validation as cv -from esphome.const import ( - CONF_ADDRESS, - CONF_ID, - CONF_LAMBDA, - CONF_NAME, - CONF_OFFSET, - CONF_TRIGGER_ID, +from esphome.components.modbus.helpers import ( + CPP_TYPE_REGISTER_MAP, + MODBUS_REGISTER_TYPE, + SENSOR_VALUE_TYPE, + TYPE_REGISTER_MAP, + ModbusRegisterType, ) +import esphome.config_validation as cv +from esphome.const import CONF_ADDRESS, CONF_ID, CONF_LAMBDA, CONF_NAME, CONF_OFFSET from esphome.cpp_helpers import logging from .const import ( @@ -48,7 +48,6 @@ CONF_SERVER_REGISTERS = "server_registers" MULTI_CONF = True modbus_controller_ns = cg.esphome_ns.namespace("modbus_controller") -modbus_ns = cg.esphome_ns.namespace("modbus") ModbusController = modbus_controller_ns.class_( "ModbusController", cg.PollingComponent, modbus.ModbusDevice ) @@ -57,96 +56,6 @@ SensorItem = modbus_controller_ns.struct("SensorItem") ServerCourtesyResponse = modbus_controller_ns.struct("ServerCourtesyResponse") ServerRegister = modbus_controller_ns.struct("ServerRegister") -ModbusFunctionCode_ns = modbus_ns.namespace("ModbusFunctionCode") -ModbusFunctionCode = ModbusFunctionCode_ns.enum("ModbusFunctionCode") -MODBUS_FUNCTION_CODE = { - "read_coils": ModbusFunctionCode.READ_COILS, - "read_discrete_inputs": ModbusFunctionCode.READ_DISCRETE_INPUTS, - "read_holding_registers": ModbusFunctionCode.READ_HOLDING_REGISTERS, - "read_input_registers": ModbusFunctionCode.READ_INPUT_REGISTERS, - "write_single_coil": ModbusFunctionCode.WRITE_SINGLE_COIL, - "write_single_register": ModbusFunctionCode.WRITE_SINGLE_REGISTER, - "write_multiple_coils": ModbusFunctionCode.WRITE_MULTIPLE_COILS, - "write_multiple_registers": ModbusFunctionCode.WRITE_MULTIPLE_REGISTERS, -} - -ModbusRegisterType_ns = modbus_controller_ns.namespace("ModbusRegisterType") -ModbusRegisterType = ModbusRegisterType_ns.enum("ModbusRegisterType") - -MODBUS_WRITE_REGISTER_TYPE = { - "custom": ModbusRegisterType.CUSTOM, - "coil": ModbusRegisterType.COIL, - "holding": ModbusRegisterType.HOLDING, -} - -MODBUS_REGISTER_TYPE = { - **MODBUS_WRITE_REGISTER_TYPE, - "discrete_input": ModbusRegisterType.DISCRETE_INPUT, - "read": ModbusRegisterType.READ, -} - -SensorValueType_ns = modbus_controller_ns.namespace("SensorValueType") -SensorValueType = SensorValueType_ns.enum("SensorValueType") -SENSOR_VALUE_TYPE = { - "RAW": SensorValueType.RAW, - "U_WORD": SensorValueType.U_WORD, - "S_WORD": SensorValueType.S_WORD, - "U_DWORD": SensorValueType.U_DWORD, - "U_DWORD_R": SensorValueType.U_DWORD_R, - "S_DWORD": SensorValueType.S_DWORD, - "S_DWORD_R": SensorValueType.S_DWORD_R, - "U_QWORD": SensorValueType.U_QWORD, - "U_QWORD_R": SensorValueType.U_QWORD_R, - "S_QWORD": SensorValueType.S_QWORD, - "S_QWORD_R": SensorValueType.S_QWORD_R, - "FP32": SensorValueType.FP32, - "FP32_R": SensorValueType.FP32_R, -} - -TYPE_REGISTER_MAP = { - "RAW": 1, - "U_WORD": 1, - "S_WORD": 1, - "U_DWORD": 2, - "U_DWORD_R": 2, - "S_DWORD": 2, - "S_DWORD_R": 2, - "U_QWORD": 4, - "U_QWORD_R": 4, - "S_QWORD": 4, - "S_QWORD_R": 4, - "FP32": 2, - "FP32_R": 2, -} - -CPP_TYPE_REGISTER_MAP = { - "RAW": cg.uint16, - "U_WORD": cg.uint16, - "S_WORD": cg.int16, - "U_DWORD": cg.uint32, - "U_DWORD_R": cg.uint32, - "S_DWORD": cg.int32, - "S_DWORD_R": cg.int32, - "U_QWORD": cg.uint64, - "U_QWORD_R": cg.uint64, - "S_QWORD": cg.int64, - "S_QWORD_R": cg.int64, - "FP32": cg.float_, - "FP32_R": cg.float_, -} - -ModbusCommandSentTrigger = modbus_controller_ns.class_( - "ModbusCommandSentTrigger", automation.Trigger.template(cg.int_, cg.int_) -) - -ModbusOnlineTrigger = modbus_controller_ns.class_( - "ModbusOnlineTrigger", automation.Trigger.template(cg.int_, cg.int_) -) - -ModbusOfflineTrigger = modbus_controller_ns.class_( - "ModbusOfflineTrigger", automation.Trigger.template(cg.int_, cg.int_) -) - _LOGGER = logging.getLogger(__name__) SERVER_COURTESY_RESPONSE_SCHEMA = cv.Schema( @@ -182,23 +91,9 @@ CONFIG_SCHEMA = cv.All( cv.Optional( CONF_SERVER_REGISTERS, ): cv.ensure_list(ModbusServerRegisterSchema), - cv.Optional(CONF_ON_COMMAND_SENT): automation.validate_automation( - { - cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id( - ModbusCommandSentTrigger - ), - } - ), - cv.Optional(CONF_ON_ONLINE): automation.validate_automation( - { - cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(ModbusOnlineTrigger), - } - ), - cv.Optional(CONF_ON_OFFLINE): automation.validate_automation( - { - cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(ModbusOfflineTrigger), - } - ), + cv.Optional(CONF_ON_COMMAND_SENT): automation.validate_automation({}), + cv.Optional(CONF_ON_ONLINE): automation.validate_automation({}), + cv.Optional(CONF_ON_OFFLINE): automation.validate_automation({}), } ) .extend(cv.polling_component_schema("60s")) @@ -363,19 +258,25 @@ async def to_code(config): cg.add(var.add_server_register(server_register_var)) await register_modbus_device(var, config) for conf in config.get(CONF_ON_COMMAND_SENT, []): - trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) - await automation.build_automation( - trigger, [(cg.int_, "function_code"), (cg.int_, "address")], conf + await automation.build_callback_automation( + var, + "add_on_command_sent_callback", + [(cg.int_, "function_code"), (cg.int_, "address")], + conf, ) for conf in config.get(CONF_ON_ONLINE, []): - trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) - await automation.build_automation( - trigger, [(cg.int_, "function_code"), (cg.int_, "address")], conf + await automation.build_callback_automation( + var, + "add_on_online_callback", + [(cg.int_, "function_code"), (cg.int_, "address")], + conf, ) for conf in config.get(CONF_ON_OFFLINE, []): - trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) - await automation.build_automation( - trigger, [(cg.int_, "function_code"), (cg.int_, "address")], conf + await automation.build_callback_automation( + var, + "add_on_offline_callback", + [(cg.int_, "function_code"), (cg.int_, "address")], + conf, ) @@ -388,7 +289,7 @@ async def register_modbus_device(var, config): def function_code_to_register(function_code): FUNCTION_CODE_TYPE_MAP = { "read_coils": ModbusRegisterType.COIL, - "read_discrete_inputs": ModbusRegisterType.DISCRETE, + "read_discrete_inputs": ModbusRegisterType.DISCRETE_INPUT, "read_holding_registers": ModbusRegisterType.HOLDING, "read_input_registers": ModbusRegisterType.READ, "write_single_coil": ModbusRegisterType.COIL, diff --git a/esphome/components/modbus_controller/automation.h b/esphome/components/modbus_controller/automation.h deleted file mode 100644 index b3338192cc..0000000000 --- a/esphome/components/modbus_controller/automation.h +++ /dev/null @@ -1,35 +0,0 @@ -#pragma once - -#include "esphome/core/component.h" -#include "esphome/core/automation.h" -#include "esphome/components/modbus_controller/modbus_controller.h" - -namespace esphome { -namespace modbus_controller { - -class ModbusCommandSentTrigger : public Trigger { - public: - ModbusCommandSentTrigger(ModbusController *a_modbuscontroller) { - a_modbuscontroller->add_on_command_sent_callback( - [this](int function_code, int address) { this->trigger(function_code, address); }); - } -}; - -class ModbusOnlineTrigger : public Trigger { - public: - ModbusOnlineTrigger(ModbusController *a_modbuscontroller) { - a_modbuscontroller->add_on_online_callback( - [this](int function_code, int address) { this->trigger(function_code, address); }); - } -}; - -class ModbusOfflineTrigger : public Trigger { - public: - ModbusOfflineTrigger(ModbusController *a_modbuscontroller) { - a_modbuscontroller->add_on_offline_callback( - [this](int function_code, int address) { this->trigger(function_code, address); }); - } -}; - -} // namespace modbus_controller -} // namespace esphome diff --git a/esphome/components/modbus_controller/binary_sensor/__init__.py b/esphome/components/modbus_controller/binary_sensor/__init__.py index 2ae008f630..18d017e13f 100644 --- a/esphome/components/modbus_controller/binary_sensor/__init__.py +++ b/esphome/components/modbus_controller/binary_sensor/__init__.py @@ -1,10 +1,10 @@ import esphome.codegen as cg from esphome.components import binary_sensor +from esphome.components.modbus.helpers import MODBUS_REGISTER_TYPE import esphome.config_validation as cv from esphome.const import CONF_ADDRESS, CONF_ID from .. import ( - MODBUS_REGISTER_TYPE, ModbusItemBaseSchema, SensorItem, add_modbus_base_properties, diff --git a/esphome/components/modbus_controller/binary_sensor/modbus_binarysensor.cpp b/esphome/components/modbus_controller/binary_sensor/modbus_binarysensor.cpp index c3eb3d4411..1ea3041b4d 100644 --- a/esphome/components/modbus_controller/binary_sensor/modbus_binarysensor.cpp +++ b/esphome/components/modbus_controller/binary_sensor/modbus_binarysensor.cpp @@ -15,10 +15,10 @@ void ModbusBinarySensor::parse_and_publish(const std::vector &data) { case ModbusRegisterType::DISCRETE_INPUT: case ModbusRegisterType::COIL: // offset for coil is the actual number of the coil not the byte offset - value = coil_from_vector(this->offset, data); + value = modbus::helpers::coil_from_vector(this->offset, data); break; default: - value = get_data(data, this->offset) & this->bitmask; + value = modbus::helpers::get_data(data, this->offset) & this->bitmask; break; } // Is there a lambda registered diff --git a/esphome/components/modbus_controller/modbus_controller.cpp b/esphome/components/modbus_controller/modbus_controller.cpp index ea6ba9d085..5c3b39c954 100644 --- a/esphome/components/modbus_controller/modbus_controller.cpp +++ b/esphome/components/modbus_controller/modbus_controller.cpp @@ -140,7 +140,7 @@ void ModbusController::on_modbus_read_registers(uint8_t function_code, uint16_t std::vector payload; payload.reserve(server_register->register_count * 2); - number_to_payload(payload, value, server_register->value_type); + modbus::helpers::number_to_payload(payload, value, server_register->value_type); sixteen_bit_response.insert(sixteen_bit_response.end(), payload.cbegin(), payload.cend()); current_address += server_register->register_count; found = true; @@ -258,7 +258,7 @@ void ModbusController::on_modbus_write_registers(uint8_t function_code, const st // Actually write to the registers: if (!for_each_register([&data](ServerRegister *server_register, uint16_t offset) { - int64_t number = payload_to_number(data, server_register->value_type, offset, 0xFFFFFFFF); + int64_t number = modbus::helpers::payload_to_number(data, server_register->value_type, offset, 0xFFFFFFFF); return server_register->write_lambda(number); })) { this->send_error(function_code, ModbusExceptionCode::SERVICE_DEVICE_FAILURE); @@ -376,7 +376,7 @@ size_t ModbusController::create_register_ranges_() { while (ix != this->sensorset_.end()) { SensorItem *curr = *ix; - ESP_LOGV(TAG, "Register: 0x%X %d %d %d offset=%u skip=%u addr=%p", curr->start_address, curr->register_count, + ESP_LOGV(TAG, "Register: 0x%X %d %d %zu offset=%u skip=%u addr=%p", curr->start_address, curr->register_count, curr->offset, curr->get_register_size(), curr->offset, curr->skip_updates, curr); if (r.register_count == 0) { @@ -484,18 +484,18 @@ void ModbusController::dump_config() { #if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE ESP_LOGCONFIG(TAG, "sensormap"); for (auto &it : this->sensorset_) { - ESP_LOGCONFIG(TAG, " Sensor type=%zu start=0x%X offset=0x%X count=%d size=%d", + ESP_LOGCONFIG(TAG, " Sensor type=%u start=0x%X offset=0x%X count=%d size=%zu", static_cast(it->register_type), it->start_address, it->offset, it->register_count, it->get_register_size()); } ESP_LOGCONFIG(TAG, "ranges"); for (auto &it : this->register_ranges_) { - ESP_LOGCONFIG(TAG, " Range type=%zu start=0x%X count=%d skip_updates=%d", static_cast(it.register_type), + ESP_LOGCONFIG(TAG, " Range type=%u start=0x%X count=%d skip_updates=%d", static_cast(it.register_type), it.start_address, it.register_count, it.skip_updates); } ESP_LOGCONFIG(TAG, "server registers"); for (auto &r : this->server_registers_) { - ESP_LOGCONFIG(TAG, " Address=0x%02X value_type=%zu register_count=%u", r->address, + ESP_LOGCONFIG(TAG, " Address=0x%02X value_type=%u register_count=%u", r->address, static_cast(r->value_type), r->register_count); } #endif @@ -517,13 +517,14 @@ void ModbusController::loop() { void ModbusController::on_write_register_response(ModbusRegisterType register_type, uint16_t start_address, const std::vector &data) { - ESP_LOGV(TAG, "Command ACK 0x%X %d ", get_data(data, 0), get_data(data, 1)); + ESP_LOGV(TAG, "Command ACK 0x%X %d ", modbus::helpers::get_data(data, 0), + modbus::helpers::get_data(data, 1)); } void ModbusController::dump_sensors_() { ESP_LOGV(TAG, "sensors"); for (auto &it : this->sensorset_) { - ESP_LOGV(TAG, " Sensor start=0x%X count=%d size=%d offset=%d", it->start_address, it->register_count, + ESP_LOGV(TAG, " Sensor start=0x%X count=%d size=%zu offset=%d", it->start_address, it->register_count, it->get_register_size(), it->offset); } } @@ -535,7 +536,7 @@ ModbusCommandItem ModbusCommandItem::create_read_command( ModbusCommandItem cmd; cmd.modbusdevice = modbusdevice; cmd.register_type = register_type; - cmd.function_code = modbus_register_read_function(register_type); + cmd.function_code = modbus::helpers::modbus_register_read_function(register_type); cmd.register_address = start_address; cmd.register_count = register_count; cmd.on_data_func = std::move(handler); @@ -548,7 +549,7 @@ ModbusCommandItem ModbusCommandItem::create_read_command(ModbusController *modbu ModbusCommandItem cmd; cmd.modbusdevice = modbusdevice; cmd.register_type = register_type; - cmd.function_code = modbus_register_read_function(register_type); + cmd.function_code = modbus::helpers::modbus_register_read_function(register_type); cmd.register_address = start_address; cmd.register_count = register_count; cmd.on_data_func = [modbusdevice](ModbusRegisterType register_type, uint16_t start_address, @@ -710,132 +711,5 @@ bool ModbusCommandItem::is_equal(const ModbusCommandItem &other) { other.register_type == this->register_type && other.function_code == this->function_code; } -void number_to_payload(std::vector &data, int64_t value, SensorValueType value_type) { - switch (value_type) { - case SensorValueType::U_WORD: - case SensorValueType::S_WORD: - data.push_back(value & 0xFFFF); - break; - case SensorValueType::U_DWORD: - case SensorValueType::S_DWORD: - case SensorValueType::FP32: - data.push_back((value & 0xFFFF0000) >> 16); - data.push_back(value & 0xFFFF); - break; - case SensorValueType::U_DWORD_R: - case SensorValueType::S_DWORD_R: - case SensorValueType::FP32_R: - data.push_back(value & 0xFFFF); - data.push_back((value & 0xFFFF0000) >> 16); - break; - case SensorValueType::U_QWORD: - case SensorValueType::S_QWORD: - data.push_back((value & 0xFFFF000000000000) >> 48); - data.push_back((value & 0xFFFF00000000) >> 32); - data.push_back((value & 0xFFFF0000) >> 16); - data.push_back(value & 0xFFFF); - break; - case SensorValueType::U_QWORD_R: - case SensorValueType::S_QWORD_R: - data.push_back(value & 0xFFFF); - data.push_back((value & 0xFFFF0000) >> 16); - data.push_back((value & 0xFFFF00000000) >> 32); - data.push_back((value & 0xFFFF000000000000) >> 48); - break; - default: - ESP_LOGE(TAG, "Invalid data type for modbus number to payload conversation: %d", - static_cast(value_type)); - break; - } -} - -int64_t payload_to_number(const std::vector &data, SensorValueType sensor_value_type, uint8_t offset, - uint32_t bitmask) { - int64_t value = 0; // int64_t because it can hold signed and unsigned 32 bits - - size_t size = data.size() - offset; - bool error = false; - switch (sensor_value_type) { - case SensorValueType::U_WORD: - if (size >= 2) { - value = mask_and_shift_by_rightbit(get_data(data, offset), bitmask); // default is 0xFFFF ; - } else { - error = true; - } - break; - case SensorValueType::U_DWORD: - case SensorValueType::FP32: - if (size >= 4) { - value = get_data(data, offset); - value = mask_and_shift_by_rightbit((uint32_t) value, bitmask); - } else { - error = true; - } - break; - case SensorValueType::U_DWORD_R: - case SensorValueType::FP32_R: - if (size >= 4) { - value = get_data(data, offset); - value = static_cast(value & 0xFFFF) << 16 | (value & 0xFFFF0000) >> 16; - value = mask_and_shift_by_rightbit((uint32_t) value, bitmask); - } else { - error = true; - } - break; - case SensorValueType::S_WORD: - if (size >= 2) { - value = mask_and_shift_by_rightbit(get_data(data, offset), - bitmask); // default is 0xFFFF ; - } else { - error = true; - } - break; - case SensorValueType::S_DWORD: - if (size >= 4) { - value = mask_and_shift_by_rightbit(get_data(data, offset), bitmask); - } else { - error = true; - } - break; - case SensorValueType::S_DWORD_R: { - if (size >= 4) { - value = get_data(data, offset); - // Currently the high word is at the low position - // the sign bit is therefore at low before the switch - uint32_t sign_bit = (value & 0x8000) << 16; - value = mask_and_shift_by_rightbit( - static_cast(((value & 0x7FFF) << 16 | (value & 0xFFFF0000) >> 16) | sign_bit), bitmask); - } else { - error = true; - } - } break; - case SensorValueType::U_QWORD: - case SensorValueType::S_QWORD: - // Ignore bitmask for QWORD - if (size >= 8) { - value = get_data(data, offset); - } else { - error = true; - } - break; - case SensorValueType::U_QWORD_R: - case SensorValueType::S_QWORD_R: { - // Ignore bitmask for QWORD - if (size >= 8) { - uint64_t tmp = get_data(data, offset); - value = (tmp << 48) | (tmp >> 48) | ((tmp & 0xFFFF0000) << 16) | ((tmp >> 16) & 0xFFFF0000); - } else { - error = true; - } - } break; - case SensorValueType::RAW: - default: - break; - } - if (error) - ESP_LOGE(TAG, "not enough data for value"); - return value; -} - } // namespace modbus_controller } // namespace esphome diff --git a/esphome/components/modbus_controller/modbus_controller.h b/esphome/components/modbus_controller/modbus_controller.h index 78c3b95965..6c6c748b73 100644 --- a/esphome/components/modbus_controller/modbus_controller.h +++ b/esphome/components/modbus_controller/modbus_controller.h @@ -3,6 +3,7 @@ #include "esphome/core/component.h" #include "esphome/components/modbus/modbus.h" +#include "esphome/components/modbus/modbus_helpers.h" #include "esphome/core/automation.h" #include @@ -19,188 +20,77 @@ class ModbusController; using modbus::ModbusFunctionCode; using modbus::ModbusRegisterType; using modbus::ModbusExceptionCode; +using modbus::helpers::SensorValueType; -enum class SensorValueType : uint8_t { - RAW = 0x00, // variable length - U_WORD = 0x1, // 1 Register unsigned - U_DWORD = 0x2, // 2 Registers unsigned - S_WORD = 0x3, // 1 Register signed - S_DWORD = 0x4, // 2 Registers signed - BIT = 0x5, - U_DWORD_R = 0x6, // 2 Registers unsigned - S_DWORD_R = 0x7, // 2 Registers unsigned - U_QWORD = 0x8, - S_QWORD = 0x9, - U_QWORD_R = 0xA, - S_QWORD_R = 0xB, - FP32 = 0xC, - FP32_R = 0xD -}; - -inline bool value_type_is_float(SensorValueType v) { - return v == SensorValueType::FP32 || v == SensorValueType::FP32_R; -} +// Remove before 2026.10.0 — these helpers have moved to modbus::helpers +ESPDEPRECATED("Use modbus::helpers::value_type_is_float() instead. Removed in 2026.10.0", "2026.4.0") +inline bool value_type_is_float(SensorValueType v) { return modbus::helpers::value_type_is_float(v); } +ESPDEPRECATED("Use modbus::helpers::modbus_register_read_function() instead. Removed in 2026.10.0", "2026.4.0") inline ModbusFunctionCode modbus_register_read_function(ModbusRegisterType reg_type) { - switch (reg_type) { - case ModbusRegisterType::COIL: - return ModbusFunctionCode::READ_COILS; - break; - case ModbusRegisterType::DISCRETE_INPUT: - return ModbusFunctionCode::READ_DISCRETE_INPUTS; - break; - case ModbusRegisterType::HOLDING: - return ModbusFunctionCode::READ_HOLDING_REGISTERS; - break; - case ModbusRegisterType::READ: - return ModbusFunctionCode::READ_INPUT_REGISTERS; - break; - default: - return ModbusFunctionCode::CUSTOM; - break; - } + return modbus::helpers::modbus_register_read_function(reg_type); } + +ESPDEPRECATED("Use modbus::helpers::modbus_register_write_function() instead. Removed in 2026.10.0", "2026.4.0") inline ModbusFunctionCode modbus_register_write_function(ModbusRegisterType reg_type) { - switch (reg_type) { - case ModbusRegisterType::COIL: - return ModbusFunctionCode::WRITE_SINGLE_COIL; - break; - case ModbusRegisterType::DISCRETE_INPUT: - return ModbusFunctionCode::CUSTOM; - break; - case ModbusRegisterType::HOLDING: - return ModbusFunctionCode::READ_WRITE_MULTIPLE_REGISTERS; - break; - case ModbusRegisterType::READ: - default: - return ModbusFunctionCode::CUSTOM; - break; - } + return modbus::helpers::modbus_register_write_function(reg_type); } -inline uint8_t c_to_hex(char c) { return (c >= 'A') ? (c >= 'a') ? (c - 'a' + 10) : (c - 'A' + 10) : (c - '0'); } +ESPDEPRECATED("Use modbus::helpers::c_to_hex() instead. Removed in 2026.10.0", "2026.4.0") +inline uint8_t c_to_hex(char c) { return modbus::helpers::c_to_hex(c); } -/** Get a byte from a hex string - * hex_byte_from_str("1122",1) returns uint_8 value 0x22 == 34 - * hex_byte_from_str("1122",0) returns 0x11 - * @param value string containing hex encoding - * @param position offset in bytes. Because each byte is encoded in 2 hex digits the position of the original byte in - * the hex string is byte_pos * 2 - * @return byte value - */ +ESPDEPRECATED("Use modbus::helpers::byte_from_hex_str() instead. Removed in 2026.10.0", "2026.4.0") inline uint8_t byte_from_hex_str(const std::string &value, uint8_t pos) { - if (value.length() < pos * 2 + 1) - return 0; - return (c_to_hex(value[pos * 2]) << 4) | c_to_hex(value[pos * 2 + 1]); + return modbus::helpers::byte_from_hex_str(value, pos); } -/** Get a word from a hex string - * @param value string containing hex encoding - * @param position offset in bytes. Because each byte is encoded in 2 hex digits the position of the original byte in - * the hex string is byte_pos * 2 - * @return word value - */ +ESPDEPRECATED("Use modbus::helpers::word_from_hex_str() instead. Removed in 2026.10.0", "2026.4.0") inline uint16_t word_from_hex_str(const std::string &value, uint8_t pos) { - return byte_from_hex_str(value, pos) << 8 | byte_from_hex_str(value, pos + 1); + return modbus::helpers::word_from_hex_str(value, pos); } -/** Get a dword from a hex string - * @param value string containing hex encoding - * @param position offset in bytes. Because each byte is encoded in 2 hex digits the position of the original byte in - * the hex string is byte_pos * 2 - * @return dword value - */ +ESPDEPRECATED("Use modbus::helpers::dword_from_hex_str() instead. Removed in 2026.10.0", "2026.4.0") inline uint32_t dword_from_hex_str(const std::string &value, uint8_t pos) { - return word_from_hex_str(value, pos) << 16 | word_from_hex_str(value, pos + 2); + return modbus::helpers::dword_from_hex_str(value, pos); } -/** Get a qword from a hex string - * @param value string containing hex encoding - * @param position offset in bytes. Because each byte is encoded in 2 hex digits the position of the original byte in - * the hex string is byte_pos * 2 - * @return qword value - */ +ESPDEPRECATED("Use modbus::helpers::qword_from_hex_str() instead. Removed in 2026.10.0", "2026.4.0") inline uint64_t qword_from_hex_str(const std::string &value, uint8_t pos) { - return static_cast(dword_from_hex_str(value, pos)) << 32 | dword_from_hex_str(value, pos + 4); + return modbus::helpers::qword_from_hex_str(value, pos); } -// Extract data from modbus response buffer -/** Extract data from modbus response buffer - * @param T one of supported integer data types int_8,int_16,int_32,int_64 - * @param data modbus response buffer (uint8_t) - * @param buffer_offset offset in bytes. - * @return value of type T extracted from buffer - */ -template T get_data(const std::vector &data, size_t buffer_offset) { - if (sizeof(T) == sizeof(uint8_t)) { - return T(data[buffer_offset]); - } - if (sizeof(T) == sizeof(uint16_t)) { - return T((uint16_t(data[buffer_offset + 0]) << 8) | (uint16_t(data[buffer_offset + 1]) << 0)); - } - - if (sizeof(T) == sizeof(uint32_t)) { - return get_data(data, buffer_offset) << 16 | get_data(data, (buffer_offset + 2)); - } - - if (sizeof(T) == sizeof(uint64_t)) { - return static_cast(get_data(data, buffer_offset)) << 32 | - (static_cast(get_data(data, buffer_offset + 4))); - } +template +ESPDEPRECATED("Use modbus::helpers::get_data() instead. Removed in 2026.10.0", "2026.4.0") +T get_data(const std::vector &data, size_t buffer_offset) { + return modbus::helpers::get_data(data, buffer_offset); } -/** Extract coil data from modbus response buffer - * Responses for coil are packed into bytes . - * coil 3 is bit 3 of the first response byte - * coil 9 is bit 2 of the second response byte - * @param coil number of the cil - * @param data modbus response buffer (uint8_t) - * @return content of coil register - */ +ESPDEPRECATED("Use modbus::helpers::coil_from_vector() instead. Removed in 2026.10.0", "2026.4.0") inline bool coil_from_vector(int coil, const std::vector &data) { - auto data_byte = coil / 8; - return (data[data_byte] & (1 << (coil % 8))) > 0; + return modbus::helpers::coil_from_vector(coil, data); } -/** Extract bits from value and shift right according to the bitmask - * if the bitmask is 0x00F0 we want the values frrom bit 5 - 8. - * the result is then shifted right by the position if the first right set bit in the mask - * Useful for modbus data where more than one value is packed in a 16 bit register - * Example: on Epever the "Length of night" register 0x9065 encodes values of the whole night length of time as - * D15 - D8 = hour, D7 - D0 = minute - * To get the hours use mask 0xFF00 and 0x00FF for the minute - * @param data an integral value between 16 aand 32 bits, - * @param bitmask the bitmask to apply - */ -template N mask_and_shift_by_rightbit(N data, uint32_t mask) { - auto result = (mask & data); - if (result == 0 || mask == 0xFFFFFFFF) { - return result; - } - for (size_t pos = 0; pos < sizeof(N) << 3; pos++) { - if ((mask & (1UL << pos)) != 0) - return result >> pos; - } - return 0; +template +ESPDEPRECATED("Use modbus::helpers::mask_and_shift_by_rightbit() instead. Removed in 2026.10.0", "2026.4.0") +N mask_and_shift_by_rightbit(N data, uint32_t mask) { + return modbus::helpers::mask_and_shift_by_rightbit(data, mask); } -/** Convert float value to vector suitable for sending - * @param data target for payload - * @param value float value to convert - * @param value_type defines if 16/32 or FP32 is used - * @return vector containing the modbus register words in correct order - */ -void number_to_payload(std::vector &data, int64_t value, SensorValueType value_type); +ESPDEPRECATED("Use modbus::helpers::number_to_payload() instead. Removed in 2026.10.0", "2026.4.0") +inline void number_to_payload(std::vector &data, int64_t value, SensorValueType value_type) { + modbus::helpers::number_to_payload(data, value, value_type); +} -/** Convert vector response payload to number. - * @param data payload with the data to convert - * @param sensor_value_type defines if 16/32/64 bits or FP32 is used - * @param offset offset to the data in data - * @param bitmask bitmask used for masking and shifting - * @return 64-bit number of the payload - */ -int64_t payload_to_number(const std::vector &data, SensorValueType sensor_value_type, uint8_t offset, - uint32_t bitmask); +ESPDEPRECATED("Use modbus::helpers::payload_to_number() instead. Removed in 2026.10.0", "2026.4.0") +inline int64_t payload_to_number(const std::vector &data, SensorValueType sensor_value_type, uint8_t offset, + uint32_t bitmask) { + return modbus::helpers::payload_to_number(data, sensor_value_type, offset, bitmask); +} + +ESPDEPRECATED("Use modbus::helpers::float_to_payload() instead. Removed in 2026.10.0", "2026.4.0") +inline std::vector float_to_payload(float value, SensorValueType value_type) { + return modbus::helpers::float_to_payload(value, value_type); +} class ModbusController; @@ -582,10 +472,10 @@ class ModbusController : public PollingComponent, public modbus::ModbusDevice { * @return float value of data */ inline float payload_to_float(const std::vector &data, const SensorItem &item) { - int64_t number = payload_to_number(data, item.sensor_value_type, item.offset, item.bitmask); + int64_t number = modbus::helpers::payload_to_number(data, item.sensor_value_type, item.offset, item.bitmask); float float_value; - if (value_type_is_float(item.sensor_value_type)) { + if (modbus::helpers::value_type_is_float(item.sensor_value_type)) { float_value = bit_cast(static_cast(number)); } else { float_value = static_cast(number); @@ -594,19 +484,5 @@ inline float payload_to_float(const std::vector &data, const SensorItem return float_value; } -inline std::vector float_to_payload(float value, SensorValueType value_type) { - int64_t val; - - if (value_type_is_float(value_type)) { - val = bit_cast(value); - } else { - val = llroundf(value); - } - - std::vector data; - number_to_payload(data, val, value_type); - return data; -} - } // namespace modbus_controller } // namespace esphome diff --git a/esphome/components/modbus_controller/number/__init__.py b/esphome/components/modbus_controller/number/__init__.py index b5efd7abf0..7563adfad9 100644 --- a/esphome/components/modbus_controller/number/__init__.py +++ b/esphome/components/modbus_controller/number/__init__.py @@ -1,5 +1,9 @@ import esphome.codegen as cg from esphome.components import number +from esphome.components.modbus.helpers import ( + MODBUS_WRITE_REGISTER_TYPE, + SENSOR_VALUE_TYPE, +) import esphome.config_validation as cv from esphome.const import ( CONF_ADDRESS, @@ -11,8 +15,6 @@ from esphome.const import ( ) from .. import ( - MODBUS_WRITE_REGISTER_TYPE, - SENSOR_VALUE_TYPE, ModbusItemBaseSchema, SensorItem, add_modbus_base_properties, diff --git a/esphome/components/modbus_controller/number/modbus_number.cpp b/esphome/components/modbus_controller/number/modbus_number.cpp index 4a3ec1fc41..ed5d91ec5b 100644 --- a/esphome/components/modbus_controller/number/modbus_number.cpp +++ b/esphome/components/modbus_controller/number/modbus_number.cpp @@ -62,7 +62,7 @@ void ModbusNumber::control(float value) { this->parent_->on_write_register_response(write_cmd.register_type, this->start_address, data); }); } else { - data = float_to_payload(write_value, this->sensor_value_type); + data = modbus::helpers::float_to_payload(write_value, this->sensor_value_type); ESP_LOGD(TAG, "Updating register: connected Sensor=%s start address=0x%X register count=%d new value=%.02f (val=%.02f)", diff --git a/esphome/components/modbus_controller/output/__init__.py b/esphome/components/modbus_controller/output/__init__.py index 1800a90d57..1ec4afd997 100644 --- a/esphome/components/modbus_controller/output/__init__.py +++ b/esphome/components/modbus_controller/output/__init__.py @@ -1,10 +1,10 @@ import esphome.codegen as cg from esphome.components import output +from esphome.components.modbus.helpers import SENSOR_VALUE_TYPE import esphome.config_validation as cv from esphome.const import CONF_ADDRESS, CONF_ID, CONF_MULTIPLY from .. import ( - SENSOR_VALUE_TYPE, ModbusItemBaseSchema, SensorItem, modbus_calc_properties, diff --git a/esphome/components/modbus_controller/output/modbus_output.cpp b/esphome/components/modbus_controller/output/modbus_output.cpp index f02d9397ca..e7f1a39716 100644 --- a/esphome/components/modbus_controller/output/modbus_output.cpp +++ b/esphome/components/modbus_controller/output/modbus_output.cpp @@ -34,7 +34,7 @@ void ModbusFloatOutput::write_state(float value) { } // lambda didn't set payload if (data.empty()) { - data = float_to_payload(value, this->sensor_value_type); + data = modbus::helpers::float_to_payload(value, this->sensor_value_type); } ESP_LOGD(TAG, "Updating register: start address=0x%X register count=%d new value=%.02f (val=%.02f)", diff --git a/esphome/components/modbus_controller/output/modbus_output.h b/esphome/components/modbus_controller/output/modbus_output.h index 0fb4bb89ea..3f3cadfe2f 100644 --- a/esphome/components/modbus_controller/output/modbus_output.h +++ b/esphome/components/modbus_controller/output/modbus_output.h @@ -15,7 +15,7 @@ class ModbusFloatOutput : public output::FloatOutput, public Component, public S this->register_type = ModbusRegisterType::HOLDING; this->start_address = start_address; this->offset = offset; - this->bitmask = bitmask; + this->bitmask = 0xFFFFFFFF; this->register_count = register_count; this->sensor_value_type = value_type; this->skip_updates = 0; @@ -47,7 +47,7 @@ class ModbusBinaryOutput : public output::BinaryOutput, public Component, public ModbusBinaryOutput(uint16_t start_address, uint8_t offset) { this->register_type = ModbusRegisterType::COIL; this->start_address = start_address; - this->bitmask = bitmask; + this->bitmask = 0xFFFFFFFF; this->sensor_value_type = SensorValueType::BIT; this->skip_updates = 0; this->register_count = 1; diff --git a/esphome/components/modbus_controller/select/__init__.py b/esphome/components/modbus_controller/select/__init__.py index c94532da51..334a4dfd76 100644 --- a/esphome/components/modbus_controller/select/__init__.py +++ b/esphome/components/modbus_controller/select/__init__.py @@ -1,15 +1,10 @@ import esphome.codegen as cg from esphome.components import select +from esphome.components.modbus.helpers import SENSOR_VALUE_TYPE, TYPE_REGISTER_MAP import esphome.config_validation as cv from esphome.const import CONF_ADDRESS, CONF_ID, CONF_LAMBDA, CONF_OPTIMISTIC -from .. import ( - SENSOR_VALUE_TYPE, - TYPE_REGISTER_MAP, - ModbusController, - SensorItem, - modbus_controller_ns, -) +from .. import ModbusController, SensorItem, modbus_controller_ns from ..const import ( CONF_FORCE_NEW_RANGE, CONF_MODBUS_CONTROLLER_ID, diff --git a/esphome/components/modbus_controller/select/modbus_select.cpp b/esphome/components/modbus_controller/select/modbus_select.cpp index e2a54d3f60..2cff7e89ee 100644 --- a/esphome/components/modbus_controller/select/modbus_select.cpp +++ b/esphome/components/modbus_controller/select/modbus_select.cpp @@ -9,7 +9,7 @@ static const char *const TAG = "modbus_controller.select"; void ModbusSelect::dump_config() { LOG_SELECT(TAG, "Modbus Controller Select", this); } void ModbusSelect::parse_and_publish(const std::vector &data) { - int64_t value = payload_to_number(data, this->sensor_value_type, this->offset, this->bitmask); + int64_t value = modbus::helpers::payload_to_number(data, this->sensor_value_type, this->offset, this->bitmask); ESP_LOGD(TAG, "New select value %lld from payload", value); @@ -61,7 +61,7 @@ void ModbusSelect::control(size_t index) { } if (data.empty()) { - number_to_payload(data, *mapval, this->sensor_value_type); + modbus::helpers::number_to_payload(data, *mapval, this->sensor_value_type); } else { ESP_LOGV(TAG, "Using payload from write lambda"); } diff --git a/esphome/components/modbus_controller/sensor/__init__.py b/esphome/components/modbus_controller/sensor/__init__.py index d8fce54ece..5b72586c66 100644 --- a/esphome/components/modbus_controller/sensor/__init__.py +++ b/esphome/components/modbus_controller/sensor/__init__.py @@ -1,11 +1,10 @@ import esphome.codegen as cg from esphome.components import sensor +from esphome.components.modbus.helpers import MODBUS_REGISTER_TYPE, SENSOR_VALUE_TYPE import esphome.config_validation as cv from esphome.const import CONF_ADDRESS, CONF_ID from .. import ( - MODBUS_REGISTER_TYPE, - SENSOR_VALUE_TYPE, ModbusItemBaseSchema, SensorItem, add_modbus_base_properties, diff --git a/esphome/components/modbus_controller/switch/__init__.py b/esphome/components/modbus_controller/switch/__init__.py index e325e6198e..a40c15ab92 100644 --- a/esphome/components/modbus_controller/switch/__init__.py +++ b/esphome/components/modbus_controller/switch/__init__.py @@ -1,10 +1,10 @@ import esphome.codegen as cg from esphome.components import switch +from esphome.components.modbus.helpers import MODBUS_REGISTER_TYPE import esphome.config_validation as cv from esphome.const import CONF_ADDRESS, CONF_ASSUMED_STATE, CONF_ID from .. import ( - MODBUS_REGISTER_TYPE, ModbusItemBaseSchema, SensorItem, add_modbus_base_properties, diff --git a/esphome/components/modbus_controller/switch/modbus_switch.cpp b/esphome/components/modbus_controller/switch/modbus_switch.cpp index 68aa37c9ed..dbaff04cc6 100644 --- a/esphome/components/modbus_controller/switch/modbus_switch.cpp +++ b/esphome/components/modbus_controller/switch/modbus_switch.cpp @@ -33,10 +33,10 @@ void ModbusSwitch::parse_and_publish(const std::vector &data) { case ModbusRegisterType::DISCRETE_INPUT: case ModbusRegisterType::COIL: // offset for coil is the actual number of the coil not the byte offset - value = coil_from_vector(this->offset, data); + value = modbus::helpers::coil_from_vector(this->offset, data); break; default: - value = get_data(data, this->offset) & this->bitmask; + value = modbus::helpers::get_data(data, this->offset) & this->bitmask; break; } diff --git a/esphome/components/modbus_controller/text_sensor/__init__.py b/esphome/components/modbus_controller/text_sensor/__init__.py index 35cae645e1..995357143e 100644 --- a/esphome/components/modbus_controller/text_sensor/__init__.py +++ b/esphome/components/modbus_controller/text_sensor/__init__.py @@ -1,10 +1,10 @@ import esphome.codegen as cg from esphome.components import text_sensor +from esphome.components.modbus.helpers import MODBUS_REGISTER_TYPE import esphome.config_validation as cv from esphome.const import CONF_ADDRESS, CONF_ID from .. import ( - MODBUS_REGISTER_TYPE, ModbusItemBaseSchema, SensorItem, add_modbus_base_properties, diff --git a/esphome/components/mopeka_pro_check/sensor.py b/esphome/components/mopeka_pro_check/sensor.py index 4e84fb708c..323175917d 100644 --- a/esphome/components/mopeka_pro_check/sensor.py +++ b/esphome/components/mopeka_pro_check/sensor.py @@ -115,6 +115,7 @@ CONFIG_SCHEMA = ( icon=ICON_COUNTER, accuracy_decimals=0, entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional(CONF_MINIMUM_SIGNAL_QUALITY, default="MEDIUM"): cv.enum( SIGNAL_QUALITIES, upper=True diff --git a/esphome/components/mqtt/__init__.py b/esphome/components/mqtt/__init__.py index 817f99375e..33a88c49cc 100644 --- a/esphome/components/mqtt/__init__.py +++ b/esphome/components/mqtt/__init__.py @@ -69,9 +69,6 @@ DEPENDENCIES = ["network"] def AUTO_LOAD(): if CORE.is_esp8266 or CORE.is_libretiny: return ["async_tcp", "json"] - # ESP32 needs socket for wake_loop_threadsafe() - if CORE.is_esp32: - return ["json", "socket"] return ["json"] @@ -348,10 +345,7 @@ async def to_code(config): # https://github.com/heman/async-mqtt-client/blob/master/library.json cg.add_library("heman/AsyncMqttClient-esphome", "2.0.0") - # MQTT on ESP32 uses wake_loop_threadsafe() to wake the main loop from the MQTT event handler - # This enables low-latency MQTT event processing instead of waiting for select() timeout if CORE.is_esp32: - socket.require_wake_loop_threadsafe() # Re-enable ESP-IDF's mqtt component (excluded by default to save compile time) # IDF 6.0 moved esp-mqtt to an external component if idf_version() >= cv.Version(6, 0, 0): diff --git a/esphome/components/mqtt/mqtt_alarm_control_panel.cpp b/esphome/components/mqtt/mqtt_alarm_control_panel.cpp index 74a60b3624..f059360e23 100644 --- a/esphome/components/mqtt/mqtt_alarm_control_panel.cpp +++ b/esphome/components/mqtt/mqtt_alarm_control_panel.cpp @@ -48,7 +48,8 @@ static bool apply_command(AlarmControlPanelCall &call, const char *state) { } void MQTTAlarmControlPanelComponent::setup() { - this->alarm_control_panel_->add_on_state_callback([this]() { this->publish_state(); }); + this->alarm_control_panel_->add_on_state_callback( + [this](AlarmControlPanelState /*state*/) { this->publish_state(); }); this->subscribe(this->get_command_topic_(), [this](const std::string &topic, const std::string &payload) { auto call = this->alarm_control_panel_->make_call(); if (!payload.empty() && payload[0] == '{') { diff --git a/esphome/components/mqtt/mqtt_backend_esp32.cpp b/esphome/components/mqtt/mqtt_backend_esp32.cpp index ab067c4418..499a330730 100644 --- a/esphome/components/mqtt/mqtt_backend_esp32.cpp +++ b/esphome/components/mqtt/mqtt_backend_esp32.cpp @@ -202,10 +202,8 @@ void MQTTBackendESP32::mqtt_event_handler(void *handler_args, esp_event_base_t b // allocate() returned non-null, the queue cannot be full. instance->mqtt_event_queue_.push(event); - // Wake main loop immediately to process MQTT event instead of waiting for select() timeout -#if defined(USE_SOCKET_SELECT_SUPPORT) && defined(USE_WAKE_LOOP_THREADSAFE) + // Wake main loop immediately to process MQTT event App.wake_loop_threadsafe(); -#endif } } diff --git a/esphome/components/mqtt/mqtt_lock.cpp b/esphome/components/mqtt/mqtt_lock.cpp index 45d8e4698f..7920187f92 100644 --- a/esphome/components/mqtt/mqtt_lock.cpp +++ b/esphome/components/mqtt/mqtt_lock.cpp @@ -28,7 +28,8 @@ void MQTTLockComponent::setup() { this->status_momentary_warning("state", 5000); } }); - this->lock_->add_on_state_callback([this]() { this->defer("send", [this]() { this->publish_state(); }); }); + this->lock_->add_on_state_callback( + [this](LockState /*state*/) { this->defer("send", [this]() { this->publish_state(); }); }); } void MQTTLockComponent::dump_config() { ESP_LOGCONFIG(TAG, "MQTT Lock '%s': ", this->lock_->get_name().c_str()); diff --git a/esphome/components/neopixelbus/_methods.py b/esphome/components/neopixelbus/_methods.py index 9072f78035..e1c327a2e0 100644 --- a/esphome/components/neopixelbus/_methods.py +++ b/esphome/components/neopixelbus/_methods.py @@ -344,7 +344,7 @@ def _spi_extra_validate(config): if CORE.is_esp32: return - if config[CONF_DATA_PIN] != 13 and config[CONF_CLOCK_PIN] != 14: + if config[CONF_DATA_PIN] != 13 or config[CONF_CLOCK_PIN] != 14: raise cv.Invalid( "SPI only supports pins GPIO13 for data and GPIO14 for clock on ESP8266" ) diff --git a/esphome/components/nextion/automation.h b/esphome/components/nextion/automation.h index 8e85e15823..17f6c77e17 100644 --- a/esphome/components/nextion/automation.h +++ b/esphome/components/nextion/automation.h @@ -2,52 +2,7 @@ #include "esphome/core/automation.h" #include "nextion.h" -namespace esphome { -namespace nextion { - -class BufferOverflowTrigger : public Trigger<> { - public: - explicit BufferOverflowTrigger(Nextion *nextion) { - nextion->add_buffer_overflow_event_callback([this]() { this->trigger(); }); - } -}; - -class SetupTrigger : public Trigger<> { - public: - explicit SetupTrigger(Nextion *nextion) { - nextion->add_setup_state_callback([this]() { this->trigger(); }); - } -}; - -class SleepTrigger : public Trigger<> { - public: - explicit SleepTrigger(Nextion *nextion) { - nextion->add_sleep_state_callback([this]() { this->trigger(); }); - } -}; - -class WakeTrigger : public Trigger<> { - public: - explicit WakeTrigger(Nextion *nextion) { - nextion->add_wake_state_callback([this]() { this->trigger(); }); - } -}; - -class PageTrigger : public Trigger { - public: - explicit PageTrigger(Nextion *nextion) { - nextion->add_new_page_callback([this](const uint8_t page_id) { this->trigger(page_id); }); - } -}; - -class TouchTrigger : public Trigger { - public: - explicit TouchTrigger(Nextion *nextion) { - nextion->add_touch_event_callback([this](uint8_t page_id, uint8_t component_id, bool touch_event) { - this->trigger(page_id, component_id, touch_event); - }); - } -}; +namespace esphome::nextion { template class NextionSetBrightnessAction : public Action { public: @@ -135,5 +90,4 @@ template class NextionPublishBoolAction : public Action { NextionComponent *component_; }; -} // namespace nextion -} // namespace esphome +} // namespace esphome::nextion diff --git a/esphome/components/nextion/binary_sensor/nextion_binarysensor.cpp b/esphome/components/nextion/binary_sensor/nextion_binarysensor.cpp index 3628ac2f63..08e7c58ef1 100644 --- a/esphome/components/nextion/binary_sensor/nextion_binarysensor.cpp +++ b/esphome/components/nextion/binary_sensor/nextion_binarysensor.cpp @@ -2,8 +2,7 @@ #include "esphome/core/util.h" #include "esphome/core/log.h" -namespace esphome { -namespace nextion { +namespace esphome::nextion { static const char *const TAG = "nextion_binarysensor"; @@ -64,5 +63,4 @@ void NextionBinarySensor::set_state(bool state, bool publish, bool send_to_nexti ESP_LOGN(TAG, "Write: %s=%s", this->variable_name_.c_str(), ONOFF(this->state)); } -} // namespace nextion -} // namespace esphome +} // namespace esphome::nextion diff --git a/esphome/components/nextion/binary_sensor/nextion_binarysensor.h b/esphome/components/nextion/binary_sensor/nextion_binarysensor.h index b6b23ada85..7637957222 100644 --- a/esphome/components/nextion/binary_sensor/nextion_binarysensor.h +++ b/esphome/components/nextion/binary_sensor/nextion_binarysensor.h @@ -4,8 +4,8 @@ #include "../nextion_component.h" #include "../nextion_base.h" -namespace esphome { -namespace nextion { +namespace esphome::nextion { + class NextionBinarySensor; class NextionBinarySensor : public NextionComponent, @@ -21,15 +21,15 @@ class NextionBinarySensor : public NextionComponent, void process_touch(uint8_t page_id, uint8_t component_id, bool state) override; // Set the components page id for Nextion Touch Component - void set_page_id(uint8_t page_id) { page_id_ = page_id; } + void set_page_id(uint8_t page_id) { this->page_id_ = page_id; } // Set the components component id for Nextion Touch Component - void set_component_id(uint8_t component_id) { component_id_ = component_id; } + void set_component_id(uint8_t component_id) { this->component_id_ = component_id; } void set_state(bool state) override { this->set_state(state, true, true); } void set_state(bool state, bool publish) override { this->set_state(state, publish, true); } void set_state(bool state, bool publish, bool send_to_nextion) override; - NextionQueueType get_queue_type() override { return NextionQueueType::BINARY_SENSOR; } + NextionQueueType get_queue_type() const override { return NextionQueueType::BINARY_SENSOR; } void set_state_from_string(const std::string &state_value, bool publish, bool send_to_nextion) override {} void set_state_from_int(int state_value, bool publish, bool send_to_nextion) override { this->set_state(state_value != 0, publish, send_to_nextion); @@ -38,5 +38,4 @@ class NextionBinarySensor : public NextionComponent, protected: uint8_t page_id_; }; -} // namespace nextion -} // namespace esphome +} // namespace esphome::nextion diff --git a/esphome/components/nextion/display.py b/esphome/components/nextion/display.py index 5b2dfc488d..506eb1202b 100644 --- a/esphome/components/nextion/display.py +++ b/esphome/components/nextion/display.py @@ -2,13 +2,7 @@ from esphome import automation import esphome.codegen as cg from esphome.components import display, esp32, uart import esphome.config_validation as cv -from esphome.const import ( - CONF_BRIGHTNESS, - CONF_ID, - CONF_LAMBDA, - CONF_ON_TOUCH, - CONF_TRIGGER_ID, -) +from esphome.const import CONF_BRIGHTNESS, CONF_ID, CONF_LAMBDA, CONF_ON_TOUCH from esphome.core import CORE, TimePeriod from . import ( # noqa: F401 pylint: disable=unused-import @@ -55,14 +49,6 @@ def AUTO_LOAD() -> list[str]: NextionSetBrightnessAction = nextion_ns.class_( "NextionSetBrightnessAction", automation.Action ) -SetupTrigger = nextion_ns.class_("SetupTrigger", automation.Trigger.template()) -SleepTrigger = nextion_ns.class_("SleepTrigger", automation.Trigger.template()) -WakeTrigger = nextion_ns.class_("WakeTrigger", automation.Trigger.template()) -PageTrigger = nextion_ns.class_("PageTrigger", automation.Trigger.template()) -TouchTrigger = nextion_ns.class_("TouchTrigger", automation.Trigger.template()) -BufferOverflowTrigger = nextion_ns.class_( - "BufferOverflowTrigger", automation.Trigger.template() -) def _validate_tft_upload(config): @@ -101,38 +87,12 @@ CONFIG_SCHEMA = cv.All( ), cv.Optional(CONF_MAX_COMMANDS_PER_LOOP): cv.uint16_t, cv.Optional(CONF_MAX_QUEUE_SIZE): cv.positive_int, - cv.Optional(CONF_ON_BUFFER_OVERFLOW): automation.validate_automation( - { - cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id( - BufferOverflowTrigger - ), - } - ), - cv.Optional(CONF_ON_PAGE): automation.validate_automation( - { - cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(PageTrigger), - } - ), - cv.Optional(CONF_ON_SETUP): automation.validate_automation( - { - cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(SetupTrigger), - } - ), - cv.Optional(CONF_ON_SLEEP): automation.validate_automation( - { - cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(SleepTrigger), - } - ), - cv.Optional(CONF_ON_TOUCH): automation.validate_automation( - { - cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(TouchTrigger), - } - ), - cv.Optional(CONF_ON_WAKE): automation.validate_automation( - { - cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(WakeTrigger), - } - ), + cv.Optional(CONF_ON_BUFFER_OVERFLOW): automation.validate_automation({}), + cv.Optional(CONF_ON_PAGE): automation.validate_automation({}), + cv.Optional(CONF_ON_SETUP): automation.validate_automation({}), + cv.Optional(CONF_ON_SLEEP): automation.validate_automation({}), + cv.Optional(CONF_ON_TOUCH): automation.validate_automation({}), + cv.Optional(CONF_ON_WAKE): automation.validate_automation({}), cv.Optional(CONF_SKIP_CONNECTION_HANDSHAKE, default=False): cv.boolean, cv.Optional(CONF_STARTUP_OVERRIDE_MS, default="8000ms"): cv.All( cv.positive_time_period_milliseconds, @@ -273,25 +233,25 @@ async def to_code(config): await display.register_display(var, config) for conf in config.get(CONF_ON_SETUP, []): - trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) - await automation.build_automation(trigger, [], conf) - + await automation.build_callback_automation( + var, "add_setup_state_callback", [], conf + ) for conf in config.get(CONF_ON_SLEEP, []): - trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) - await automation.build_automation(trigger, [], conf) - + await automation.build_callback_automation( + var, "add_sleep_state_callback", [], conf + ) for conf in config.get(CONF_ON_WAKE, []): - trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) - await automation.build_automation(trigger, [], conf) - + await automation.build_callback_automation( + var, "add_wake_state_callback", [], conf + ) for conf in config.get(CONF_ON_PAGE, []): - trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) - await automation.build_automation(trigger, [(cg.uint8, "x")], conf) - + await automation.build_callback_automation( + var, "add_new_page_callback", [(cg.uint8, "x")], conf + ) for conf in config.get(CONF_ON_TOUCH, []): - trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) - await automation.build_automation( - trigger, + await automation.build_callback_automation( + var, + "add_touch_event_callback", [ (cg.uint8, "page_id"), (cg.uint8, "component_id"), @@ -299,7 +259,7 @@ async def to_code(config): ], conf, ) - for conf in config.get(CONF_ON_BUFFER_OVERFLOW, []): - trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) - await automation.build_automation(trigger, [], conf) + await automation.build_callback_automation( + var, "add_buffer_overflow_event_callback", [], conf + ) diff --git a/esphome/components/nextion/nextion.cpp b/esphome/components/nextion/nextion.cpp index fa1582c209..4a15cbe64f 100644 --- a/esphome/components/nextion/nextion.cpp +++ b/esphome/components/nextion/nextion.cpp @@ -5,8 +5,7 @@ #include "esphome/core/log.h" #include "esphome/core/util.h" -namespace esphome { -namespace nextion { +namespace esphome::nextion { static const char *const TAG = "nextion"; @@ -143,9 +142,20 @@ void Nextion::reset_(bool reset_nextion) { while (this->available()) { // Clear receive buffer this->read_byte(&d); - }; + } + for (auto *entry : this->nextion_queue_) { + if (entry->component != nullptr && entry->component->get_queue_type() == NextionQueueType::NO_RESULT) { + delete entry->component; // NOLINT(cppcoreguidelines-owning-memory) + } + delete entry; // NOLINT(cppcoreguidelines-owning-memory) + } this->nextion_queue_.clear(); +#ifdef USE_NEXTION_WAVEFORM + for (auto *entry : this->waveform_queue_) { + delete entry; // NOLINT(cppcoreguidelines-owning-memory) + } this->waveform_queue_.clear(); +#endif // USE_NEXTION_WAVEFORM } void Nextion::dump_config() { @@ -241,7 +251,7 @@ bool Nextion::send_command(const char *command) { return false; if (this->send_command_(command)) { - this->add_no_result_to_queue_("send_command"); + this->add_no_result_to_queue_("command"); return true; } return false; @@ -262,7 +272,7 @@ bool Nextion::send_command_printf(const char *format, ...) { } if (this->send_command_(buffer)) { - this->add_no_result_to_queue_("send_command_printf"); + this->add_no_result_to_queue_("command_printf"); return true; } return false; @@ -281,7 +291,7 @@ void Nextion::print_queue_members_() { ESP_LOGN(TAG, "Queue null"); } else { ESP_LOGN(TAG, "Queue type: %d:%s, name: %s", i->component->get_queue_type(), - i->component->get_queue_type_string().c_str(), i->component->get_variable_name().c_str()); + i->component->get_queue_type_string(), i->component->get_variable_name().c_str()); } } ESP_LOGN(TAG, "*******************************************"); @@ -423,7 +433,7 @@ void Nextion::process_nextion_commands_() { DELIMITER_SIZE)) != std::string::npos) { #ifdef USE_NEXTION_MAX_COMMANDS_PER_LOOP if (++commands_processed > this->max_commands_per_loop_) { - ESP_LOGW(TAG, "Command processing limit exceeded"); + ESP_LOGV(TAG, "Command limit reached, deferring"); break; } #endif // USE_NEXTION_MAX_COMMANDS_PER_LOOP @@ -488,20 +498,21 @@ void Nextion::process_nextion_commands_() { ESP_LOGW(TAG, "Invalid baud rate"); break; case 0x12: // invalid Waveform ID or Channel # was used +#ifdef USE_NEXTION_WAVEFORM if (this->waveform_queue_.empty()) { ESP_LOGW(TAG, "Waveform ID/ch used but no sensor queued"); } else { auto &nb = this->waveform_queue_.front(); NextionComponentBase *component = nb->component; - ESP_LOGW(TAG, "Invalid waveform ID %d/ch %d", component->get_component_id(), component->get_wave_channel_id()); - ESP_LOGN(TAG, "Remove waveform ID %d/ch %d", component->get_component_id(), component->get_wave_channel_id()); - delete nb; // NOLINT(cppcoreguidelines-owning-memory) - this->waveform_queue_.pop_front(); + this->waveform_queue_.pop(); } +#else // USE_NEXTION_WAVEFORM + ESP_LOGW(TAG, "Waveform ID/ch error but waveform not enabled"); +#endif // USE_NEXTION_WAVEFORM break; case 0x1A: // variable name invalid ESP_LOGW(TAG, "Invalid variable name"); @@ -607,7 +618,7 @@ void Nextion::process_nextion_commands_() { ESP_LOGE(TAG, "String return but '%s' not text sensor", component->get_variable_name().c_str()); } else { ESP_LOGN(TAG, "String resp: '%s' id: %s type: %s", to_process.c_str(), component->get_variable_name().c_str(), - component->get_queue_type_string().c_str()); + component->get_queue_type_string()); } delete nb; // NOLINT(cppcoreguidelines-owning-memory) @@ -649,7 +660,7 @@ void Nextion::process_nextion_commands_() { component->get_queue_type()); } else { ESP_LOGN(TAG, "Numeric: %s type %d:%s val %d", component->get_variable_name().c_str(), - component->get_queue_type(), component->get_queue_type_string().c_str(), value); + component->get_queue_type(), component->get_queue_type_string(), value); component->set_state_from_int(value, true, false); } @@ -804,29 +815,30 @@ void Nextion::process_nextion_commands_() { } case 0xFD: { // data transparent transmit finished ESP_LOGVV(TAG, "Data transmit done"); +#ifdef USE_NEXTION_WAVEFORM this->check_pending_waveform_(); +#endif // USE_NEXTION_WAVEFORM break; } case 0xFE: { // data transparent transmit ready ESP_LOGVV(TAG, "Ready for transmit"); +#ifdef USE_NEXTION_WAVEFORM if (this->waveform_queue_.empty()) { ESP_LOGE(TAG, "No waveforms queued"); break; } - auto &nb = this->waveform_queue_.front(); auto *component = nb->component; - size_t buffer_to_send = component->get_wave_buffer_size() < 255 ? component->get_wave_buffer_size() - : 255; // ADDT command can only send 255 - + size_t buffer_to_send = component->get_wave_buffer_size() < 255 ? component->get_wave_buffer_size() : 255; this->write_array(component->get_wave_buffer().data(), static_cast(buffer_to_send)); - ESP_LOGN(TAG, "Send waveform: component id %d, waveform id %d, size %zu", component->get_component_id(), component->get_wave_channel_id(), buffer_to_send); - component->clear_wave_buffer(buffer_to_send); delete nb; // NOLINT(cppcoreguidelines-owning-memory) - this->waveform_queue_.pop_front(); + this->waveform_queue_.pop(); +#else // USE_NEXTION_WAVEFORM + ESP_LOGW(TAG, "Waveform transmit ready but waveform not enabled"); +#endif // USE_NEXTION_WAVEFORM break; } default: @@ -841,19 +853,10 @@ void Nextion::process_nextion_commands_() { if (this->max_q_age_ms_ > 0 && !this->nextion_queue_.empty() && ms - this->nextion_queue_.front()->queue_time > this->max_q_age_ms_) { - for (size_t i = 0; i < this->nextion_queue_.size(); i++) { - NextionComponentBase *component = this->nextion_queue_[i]->component; - if (ms - this->nextion_queue_[i]->queue_time > this->max_q_age_ms_) { - if (this->nextion_queue_[i]->queue_time == 0) { - ESP_LOGD(TAG, "Remove old queue '%s':'%s' (t=0)", component->get_queue_type_string().c_str(), - component->get_variable_name().c_str()); - } - - if (component->get_variable_name() == "sleep_wake") { - this->is_sleeping_ = false; - } - - ESP_LOGD(TAG, "Remove old queue '%s':'%s'", component->get_queue_type_string().c_str(), + for (auto it = this->nextion_queue_.begin(); it != this->nextion_queue_.end();) { + if (ms - (*it)->queue_time > this->max_q_age_ms_) { + NextionComponentBase *component = (*it)->component; + ESP_LOGV(TAG, "Remove old queue '%s':'%s'", component->get_queue_type_string(), component->get_variable_name().c_str()); if (component->get_queue_type() == NextionQueueType::NO_RESULT) { @@ -863,10 +866,8 @@ void Nextion::process_nextion_commands_() { delete component; // NOLINT(cppcoreguidelines-owning-memory) } - delete this->nextion_queue_[i]; // NOLINT(cppcoreguidelines-owning-memory) - - this->nextion_queue_.erase(this->nextion_queue_.begin() + i); - i--; + delete *it; // NOLINT(cppcoreguidelines-owning-memory) + it = this->nextion_queue_.erase(it); } else { break; @@ -937,8 +938,13 @@ void Nextion::all_components_send_state_(bool force_update) { binarysensortype->send_state_to_nextion(); } for (auto *sensortype : this->sensortype_) { - if ((force_update || sensortype->get_needs_to_send_update()) && sensortype->get_wave_chan_id() == 0) +#ifdef USE_NEXTION_WAVEFORM + if ((force_update || sensortype->get_needs_to_send_update()) && sensortype->get_wave_channel_id() == UINT8_MAX) { +#else // USE_NEXTION_WAVEFORM + if (force_update || sensortype->get_needs_to_send_update()) { +#endif // USE_NEXTION_WAVEFORM sensortype->send_state_to_nextion(); + } } for (auto *switchtype : this->switchtype_) { if (force_update || switchtype->get_needs_to_send_update()) @@ -1045,7 +1051,7 @@ void Nextion::add_no_result_to_queue_(const std::string &variable_name) { nextion_queue->component = new nextion::NextionComponentBase; nextion_queue->component->set_variable_name(variable_name); - nextion_queue->queue_time = millis(); + nextion_queue->queue_time = App.get_loop_component_start_time(); this->nextion_queue_.push_back(nextion_queue); @@ -1233,7 +1239,7 @@ void Nextion::add_to_get_queue(NextionComponentBase *component) { nextion_queue->component = component; nextion_queue->queue_time = App.get_loop_component_start_time(); - ESP_LOGN(TAG, "Queue %s: %s", component->get_queue_type_string().c_str(), component->get_variable_name().c_str()); + ESP_LOGN(TAG, "Queue %s: %s", component->get_queue_type_string(), component->get_variable_name().c_str()); std::string command = "get " + component->get_variable_name_to_send(); @@ -1242,13 +1248,11 @@ void Nextion::add_to_get_queue(NextionComponentBase *component) { } } +#ifdef USE_NEXTION_WAVEFORM /** - * @brief Add addt command to the queue + * @brief Add addt command to the waveform queue. * - * @param component_id The waveform component id - * @param wave_chan_id The waveform channel to send it to - * @param buffer_to_send The buffer size - * @param buffer_size The buffer data + * @param component Pointer to the Nextion component with waveform data to send. */ void Nextion::add_addt_command_to_queue(NextionComponentBase *component) { if ((!this->is_setup() && !this->connection_state_.ignore_is_setup_) || this->is_sleeping()) @@ -1265,7 +1269,11 @@ void Nextion::add_addt_command_to_queue(NextionComponentBase *component) { nextion_queue->component = component; nextion_queue->queue_time = App.get_loop_component_start_time(); - this->waveform_queue_.push_back(nextion_queue); + if (!this->waveform_queue_.push(nextion_queue)) { + ESP_LOGW(TAG, "Waveform queue full, drop"); + delete nextion_queue; // NOLINT(cppcoreguidelines-owning-memory) + return; + } if (this->waveform_queue_.size() == 1) this->check_pending_waveform_(); } @@ -1276,21 +1284,20 @@ void Nextion::check_pending_waveform_() { auto *nb = this->waveform_queue_.front(); auto *component = nb->component; - size_t buffer_to_send = component->get_wave_buffer_size() < 255 ? component->get_wave_buffer_size() - : 255; // ADDT command can only send 255 + size_t buffer_to_send = component->get_wave_buffer_size() < 255 ? component->get_wave_buffer_size() : 255; char command[24]; // "addt " + uint8 + "," + uint8 + "," + uint8 + null = max 17 chars buf_append_printf(command, sizeof(command), 0, "addt %u,%u,%zu", component->get_component_id(), component->get_wave_channel_id(), buffer_to_send); if (!this->send_command_(command)) { delete nb; // NOLINT(cppcoreguidelines-owning-memory) - this->waveform_queue_.pop_front(); + this->waveform_queue_.pop(); } } +#endif // USE_NEXTION_WAVEFORM void Nextion::set_writer(const nextion_writer_t &writer) { this->writer_ = writer; } bool Nextion::is_updating() { return this->connection_state_.is_updating_; } -} // namespace nextion -} // namespace esphome +} // namespace esphome::nextion diff --git a/esphome/components/nextion/nextion.h b/esphome/components/nextion/nextion.h index 217d2e605d..d910389289 100644 --- a/esphome/components/nextion/nextion.h +++ b/esphome/components/nextion/nextion.h @@ -1,16 +1,20 @@ #pragma once -#include +#include #include +#include "esphome/components/display/display.h" +#include "esphome/components/display/display_color_utils.h" +#include "esphome/components/uart/uart.h" #include "esphome/core/defines.h" #include "esphome/core/time.h" -#include "esphome/components/uart/uart.h" +#ifdef USE_NEXTION_WAVEFORM +#include "esphome/core/helpers.h" +#endif // USE_NEXTION_WAVEFORM + #include "nextion_base.h" #include "nextion_component.h" -#include "esphome/components/display/display.h" -#include "esphome/components/display/display_color_utils.h" #ifdef USE_NEXTION_TFT_UPLOAD #ifdef USE_ESP32 @@ -21,8 +25,7 @@ #endif // USE_ESP32 vs USE_ESP8266 #endif // USE_NEXTION_TFT_UPLOAD -namespace esphome { -namespace nextion { +namespace esphome::nextion { class Nextion; class NextionComponentBase; @@ -603,6 +606,7 @@ class Nextion : public NextionBase, public PollingComponent, public uart::UARTDe */ void disable_component_touch(const char *component); +#ifdef USE_NEXTION_WAVEFORM /** * Add waveform data to a waveform component * @param component_id The integer component id. @@ -612,6 +616,7 @@ class Nextion : public NextionBase, public PollingComponent, public uart::UARTDe void add_waveform_data(uint8_t component_id, uint8_t channel_number, uint8_t value); void open_waveform_channel(uint8_t component_id, uint8_t channel_number, uint8_t value); +#endif // USE_NEXTION_WAVEFORM /** * Display a picture at coordinates. @@ -1206,7 +1211,9 @@ class Nextion : public NextionBase, public PollingComponent, public uart::UARTDe void add_to_get_queue(NextionComponentBase *component) override; +#ifdef USE_NEXTION_WAVEFORM void add_addt_command_to_queue(NextionComponentBase *component) override; +#endif // USE_NEXTION_WAVEFORM void update_components_by_prefix(const std::string &prefix); @@ -1391,8 +1398,12 @@ class Nextion : public NextionBase, public PollingComponent, public uart::UARTDe void process_pending_in_queue_(); #endif // USE_NEXTION_COMMAND_SPACING - std::deque nextion_queue_; - std::deque waveform_queue_; + std::list nextion_queue_; +#ifdef USE_NEXTION_WAVEFORM + /// Fixed-size ring buffer for waveform queue. Nextion supports at most 4 waveform + /// channels (IDs 0-3), so 4 entries is both the correct maximum and a safe default. + StaticRingBuffer waveform_queue_; +#endif // USE_NEXTION_WAVEFORM uint16_t recv_ret_string_(std::string &response, uint32_t timeout, bool recv_flag); void all_components_send_state_(bool force_update = false); uint32_t comok_sent_ = 0; @@ -1461,7 +1472,9 @@ class Nextion : public NextionBase, public PollingComponent, public uart::UARTDe const std::string &variable_name_to_send, const std::string &state_value, bool is_sleep_safe = false); +#ifdef USE_NEXTION_WAVEFORM void check_pending_waveform_(); +#endif // USE_NEXTION_WAVEFORM #ifdef USE_NEXTION_TFT_UPLOAD #ifdef USE_ESP8266 @@ -1547,5 +1560,4 @@ class Nextion : public NextionBase, public PollingComponent, public uart::UARTDe uint16_t max_q_age_ms_ = 8000; ///< Maximum age for queue items in ms }; -} // namespace nextion -} // namespace esphome +} // namespace esphome::nextion diff --git a/esphome/components/nextion/nextion_base.h b/esphome/components/nextion/nextion_base.h index d46cd9a185..4a2dc90d40 100644 --- a/esphome/components/nextion/nextion_base.h +++ b/esphome/components/nextion/nextion_base.h @@ -2,8 +2,8 @@ #include "esphome/core/defines.h" #include "esphome/core/color.h" #include "nextion_component_base.h" -namespace esphome { -namespace nextion { + +namespace esphome::nextion { #ifdef ESPHOME_LOG_HAS_VERY_VERBOSE #define NEXTION_PROTOCOL_LOG @@ -33,7 +33,9 @@ class NextionBase { const std::string &variable_name_to_send, const std::string &state_value) = 0; +#ifdef USE_NEXTION_WAVEFORM virtual void add_addt_command_to_queue(NextionComponentBase *component) = 0; +#endif // USE_NEXTION_WAVEFORM virtual void add_to_get_queue(NextionComponentBase *component) = 0; @@ -61,5 +63,4 @@ class NextionBase { bool is_detected_ = false; }; -} // namespace nextion -} // namespace esphome +} // namespace esphome::nextion diff --git a/esphome/components/nextion/nextion_commands.cpp b/esphome/components/nextion/nextion_commands.cpp index 4ddbfbee6a..a332d342ee 100644 --- a/esphome/components/nextion/nextion_commands.cpp +++ b/esphome/components/nextion/nextion_commands.cpp @@ -3,8 +3,8 @@ #include "esphome/core/log.h" #include -namespace esphome { -namespace nextion { +namespace esphome::nextion { + static const char *const TAG = "nextion"; // Sleep safe commands @@ -12,7 +12,7 @@ void Nextion::soft_reset() { this->send_command_("rest"); } void Nextion::set_wake_up_page(uint8_t wake_up_page) { this->wake_up_page_ = wake_up_page; - this->add_no_result_to_queue_with_set_internal_("wake_up_page", "wup", wake_up_page, true); + this->add_no_result_to_queue_with_set_internal_("wup", "wup", wake_up_page, true); } void Nextion::set_touch_sleep_timeout(const uint16_t touch_sleep_timeout) { @@ -23,7 +23,7 @@ void Nextion::set_touch_sleep_timeout(const uint16_t touch_sleep_timeout) { this->touch_sleep_timeout_ = touch_sleep_timeout; } - this->add_no_result_to_queue_with_set_internal_("touch_sleep_timeout", "thsp", this->touch_sleep_timeout_, true); + this->add_no_result_to_queue_with_set_internal_("thsp", "thsp", this->touch_sleep_timeout_, true); } void Nextion::sleep(bool sleep) { @@ -58,115 +58,107 @@ bool Nextion::set_protocol_reparse_mode(bool active_mode) { // Set Colors - Background void Nextion::set_component_background_color(const char *component, uint16_t color) { - this->add_no_result_to_queue_with_printf_("set_component_background_color", "%s.bco=%" PRIu16, component, color); + this->add_no_result_to_queue_with_printf_(".bco", "%s.bco=%" PRIu16, component, color); } void Nextion::set_component_background_color(const char *component, const char *color) { - this->add_no_result_to_queue_with_printf_("set_component_background_color", "%s.bco=%s", component, color); + this->add_no_result_to_queue_with_printf_(".bco", "%s.bco=%s", component, color); } void Nextion::set_component_background_color(const char *component, Color color) { - this->add_no_result_to_queue_with_printf_("set_component_background_color", "%s.bco=%d", component, - display::ColorUtil::color_to_565(color)); + this->add_no_result_to_queue_with_printf_(".bco", "%s.bco=%d", component, display::ColorUtil::color_to_565(color)); } // Set Colors - Background (pressed) void Nextion::set_component_pressed_background_color(const char *component, uint16_t color) { - this->add_no_result_to_queue_with_printf_("set_component_pressed_background_color", "%s.bco2=%" PRIu16, component, - color); + this->add_no_result_to_queue_with_printf_(".bco2", "%s.bco2=%" PRIu16, component, color); } void Nextion::set_component_pressed_background_color(const char *component, const char *color) { - this->add_no_result_to_queue_with_printf_("set_component_pressed_background_color", "%s.bco2=%s", component, color); + this->add_no_result_to_queue_with_printf_(".bco2", "%s.bco2=%s", component, color); } void Nextion::set_component_pressed_background_color(const char *component, Color color) { - this->add_no_result_to_queue_with_printf_("set_component_pressed_background_color", "%s.bco2=%d", component, - display::ColorUtil::color_to_565(color)); + this->add_no_result_to_queue_with_printf_(".bco2", "%s.bco2=%d", component, display::ColorUtil::color_to_565(color)); } // Set Colors - Foreground void Nextion::set_component_foreground_color(const char *component, uint16_t color) { - this->add_no_result_to_queue_with_printf_("set_component_foreground_color", "%s.pco=%" PRIu16, component, color); + this->add_no_result_to_queue_with_printf_(".pco", "%s.pco=%" PRIu16, component, color); } void Nextion::set_component_foreground_color(const char *component, const char *color) { - this->add_no_result_to_queue_with_printf_("set_component_foreground_color", "%s.pco=%s", component, color); + this->add_no_result_to_queue_with_printf_(".pco", "%s.pco=%s", component, color); } void Nextion::set_component_foreground_color(const char *component, Color color) { - this->add_no_result_to_queue_with_printf_("set_component_foreground_color", "%s.pco=%d", component, - display::ColorUtil::color_to_565(color)); + this->add_no_result_to_queue_with_printf_(".pco", "%s.pco=%d", component, display::ColorUtil::color_to_565(color)); } // Set Colors - Foreground (pressed) void Nextion::set_component_pressed_foreground_color(const char *component, uint16_t color) { - this->add_no_result_to_queue_with_printf_("set_component_pressed_foreground_color", "%s.pco2=%" PRIu16, component, - color); + this->add_no_result_to_queue_with_printf_(".pco2", "%s.pco2=%" PRIu16, component, color); } void Nextion::set_component_pressed_foreground_color(const char *component, const char *color) { - this->add_no_result_to_queue_with_printf_("set_component_pressed_foreground_color", "%s.pco2=%s", component, color); + this->add_no_result_to_queue_with_printf_(".pco2", "%s.pco2=%s", component, color); } void Nextion::set_component_pressed_foreground_color(const char *component, Color color) { - this->add_no_result_to_queue_with_printf_("set_component_pressed_foreground_color", "%s.pco2=%d", component, - display::ColorUtil::color_to_565(color)); + this->add_no_result_to_queue_with_printf_(".pco2", "%s.pco2=%d", component, display::ColorUtil::color_to_565(color)); } // Set Colors - Font void Nextion::set_component_font_color(const char *component, uint16_t color) { - this->add_no_result_to_queue_with_printf_("set_component_font_color", "%s.pco=%" PRIu16, component, color); + this->add_no_result_to_queue_with_printf_(".pco", "%s.pco=%" PRIu16, component, color); } void Nextion::set_component_font_color(const char *component, const char *color) { - this->add_no_result_to_queue_with_printf_("set_component_font_color", "%s.pco=%s", component, color); + this->add_no_result_to_queue_with_printf_(".pco", "%s.pco=%s", component, color); } void Nextion::set_component_font_color(const char *component, Color color) { - this->add_no_result_to_queue_with_printf_("set_component_font_color", "%s.pco=%d", component, - display::ColorUtil::color_to_565(color)); + this->add_no_result_to_queue_with_printf_(".pco", "%s.pco=%d", component, display::ColorUtil::color_to_565(color)); } // Set Colors - Font (pressed) void Nextion::set_component_pressed_font_color(const char *component, uint16_t color) { - this->add_no_result_to_queue_with_printf_("set_component_pressed_font_color", "%s.pco2=%" PRIu16, component, color); + this->add_no_result_to_queue_with_printf_(".pco2", "%s.pco2=%" PRIu16, component, color); } void Nextion::set_component_pressed_font_color(const char *component, const char *color) { - this->add_no_result_to_queue_with_printf_("set_component_pressed_font_color", "%s.pco2=%s", component, color); + this->add_no_result_to_queue_with_printf_(".pco2", "%s.pco2=%s", component, color); } void Nextion::set_component_pressed_font_color(const char *component, Color color) { - this->add_no_result_to_queue_with_printf_("set_component_pressed_font_color", "%s.pco2=%d", component, - display::ColorUtil::color_to_565(color)); + this->add_no_result_to_queue_with_printf_(".pco2", "%s.pco2=%d", component, display::ColorUtil::color_to_565(color)); } // Set picture void Nextion::set_component_pic(const char *component, uint16_t pic_id) { - this->add_no_result_to_queue_with_printf_("set_component_pic", "%s.pic=%" PRIu16, component, pic_id); + this->add_no_result_to_queue_with_printf_(".pic", "%s.pic=%" PRIu16, component, pic_id); } void Nextion::set_component_picc(const char *component, uint16_t pic_id) { - this->add_no_result_to_queue_with_printf_("set_component_picc", "%s.picc=%" PRIu16, component, pic_id); + this->add_no_result_to_queue_with_printf_(".picc", "%s.picc=%" PRIu16, component, pic_id); } // Set video void Nextion::set_component_vid(const char *component, uint8_t vid_id) { - this->add_no_result_to_queue_with_printf_("set_component_vid", "%s.vid=%" PRIu8, component, vid_id); + this->add_no_result_to_queue_with_printf_(".vid", "%s.vid=%" PRIu8, component, vid_id); } void Nextion::set_component_drag(const char *component, bool drag) { - this->add_no_result_to_queue_with_printf_("set_component_drag", "%s.drag=%i", component, drag ? 1 : 0); + this->add_no_result_to_queue_with_printf_(".drag", "%s.drag=%i", component, drag ? 1 : 0); } void Nextion::set_component_aph(const char *component, uint8_t aph) { - this->add_no_result_to_queue_with_printf_("set_component_aph", "%s.aph=%" PRIu8, component, aph); + this->add_no_result_to_queue_with_printf_(".aph", "%s.aph=%" PRIu8, component, aph); } void Nextion::set_component_position(const char *component, uint32_t x, uint32_t y) { - this->add_no_result_to_queue_with_printf_("set_component_position_x", "%s.x=%" PRIu32, component, x); - this->add_no_result_to_queue_with_printf_("set_component_position_y", "%s.y=%" PRIu32, component, y); + this->add_no_result_to_queue_with_printf_(".x", "%s.x=%" PRIu32, component, x); + this->add_no_result_to_queue_with_printf_(".y", "%s.y=%" PRIu32, component, y); } void Nextion::set_component_text_printf(const char *component, const char *format, ...) { @@ -180,29 +172,29 @@ void Nextion::set_component_text_printf(const char *component, const char *forma } // General Nextion -void Nextion::goto_page(const char *page) { this->add_no_result_to_queue_with_printf_("goto_page", "page %s", page); } -void Nextion::goto_page(uint8_t page) { this->add_no_result_to_queue_with_printf_("goto_page", "page %i", page); } +void Nextion::goto_page(const char *page) { this->add_no_result_to_queue_with_printf_("page", "page %s", page); } +void Nextion::goto_page(uint8_t page) { this->add_no_result_to_queue_with_printf_("page", "page %i", page); } void Nextion::set_backlight_brightness(float brightness) { if (brightness < 0 || brightness > 1.0) { ESP_LOGD(TAG, "Brightness out of bounds (0-1.0)"); return; } - this->add_no_result_to_queue_with_printf_("backlight_brightness", "dim=%d", static_cast(brightness * 100)); + this->add_no_result_to_queue_with_printf_("dim", "dim=%d", static_cast(brightness * 100)); } void Nextion::set_auto_wake_on_touch(bool auto_wake_on_touch) { this->connection_state_.auto_wake_on_touch_ = auto_wake_on_touch; - this->add_no_result_to_queue_with_set("auto_wake_on_touch", "thup", auto_wake_on_touch ? 1 : 0); + this->add_no_result_to_queue_with_set("thup", "thup", auto_wake_on_touch ? 1 : 0); } // General Component void Nextion::set_component_font(const char *component, uint8_t font_id) { - this->add_no_result_to_queue_with_printf_("set_component_font", "%s.font=%" PRIu8, component, font_id); + this->add_no_result_to_queue_with_printf_(".font", "%s.font=%" PRIu8, component, font_id); } void Nextion::set_component_visibility(const char *component, bool show) { - this->add_no_result_to_queue_with_printf_("set_component_visibility", "vis %s,%d", component, show ? 1 : 0); + this->add_no_result_to_queue_with_printf_("vis", "vis %s,%d", component, show ? 1 : 0); } void Nextion::hide_component(const char *component) { this->set_component_visibility(component, false); } @@ -210,56 +202,57 @@ void Nextion::hide_component(const char *component) { this->set_component_visibi void Nextion::show_component(const char *component) { this->set_component_visibility(component, true); } void Nextion::enable_component_touch(const char *component) { - this->add_no_result_to_queue_with_printf_("enable_component_touch", "tsw %s,1", component); + this->add_no_result_to_queue_with_printf_("tsw", "tsw %s,1", component); } void Nextion::disable_component_touch(const char *component) { - this->add_no_result_to_queue_with_printf_("disable_component_touch", "tsw %s,0", component); + this->add_no_result_to_queue_with_printf_("tsw", "tsw %s,0", component); } void Nextion::set_component_text(const char *component, const char *text) { - this->add_no_result_to_queue_with_printf_("set_component_text", "%s.txt=\"%s\"", component, text); + this->add_no_result_to_queue_with_printf_(".txt", "%s.txt=\"%s\"", component, text); } void Nextion::set_component_value(const char *component, int32_t value) { - this->add_no_result_to_queue_with_printf_("set_component_value", "%s.val=%" PRId32, component, value); + this->add_no_result_to_queue_with_printf_(".val", "%s.val=%" PRId32, component, value); } +#ifdef USE_NEXTION_WAVEFORM void Nextion::add_waveform_data(uint8_t component_id, uint8_t channel_number, uint8_t value) { - this->add_no_result_to_queue_with_printf_("add_waveform_data", "add %" PRIu8 ",%" PRIu8 ",%" PRIu8, component_id, - channel_number, value); + this->add_no_result_to_queue_with_printf_("add", "add %" PRIu8 ",%" PRIu8 ",%" PRIu8, component_id, channel_number, + value); } void Nextion::open_waveform_channel(uint8_t component_id, uint8_t channel_number, uint8_t value) { - this->add_no_result_to_queue_with_printf_("open_waveform_channel", "addt %" PRIu8 ",%" PRIu8 ",%" PRIu8, component_id, - channel_number, value); + this->add_no_result_to_queue_with_printf_("addt", "addt %" PRIu8 ",%" PRIu8 ",%" PRIu8, component_id, channel_number, + value); } +#endif // USE_NEXTION_WAVEFORM void Nextion::set_component_coordinates(const char *component, uint16_t x, uint16_t y) { - this->add_no_result_to_queue_with_printf_("set_component_coordinates command 1", "%s.xcen=%" PRIu16, component, x); - this->add_no_result_to_queue_with_printf_("set_component_coordinates command 2", "%s.ycen=%" PRIu16, component, y); + this->add_no_result_to_queue_with_printf_(".xcen", "%s.xcen=%" PRIu16, component, x); + this->add_no_result_to_queue_with_printf_(".ycen", "%s.ycen=%" PRIu16, component, y); } // Drawing void Nextion::display_picture(uint16_t picture_id, uint16_t x_start, uint16_t y_start) { - this->add_no_result_to_queue_with_printf_("display_picture", "pic %" PRIu16 ", %" PRIu16 ", %" PRIu16, x_start, - y_start, picture_id); + this->add_no_result_to_queue_with_printf_("pic", "pic %" PRIu16 ", %" PRIu16 ", %" PRIu16, x_start, y_start, + picture_id); } void Nextion::fill_area(uint16_t x1, uint16_t y1, uint16_t width, uint16_t height, uint16_t color) { - this->add_no_result_to_queue_with_printf_( - "fill_area", "fill %" PRIu16 ",%" PRIu16 ",%" PRIu16 ",%" PRIu16 ",%" PRIu16, x1, y1, width, height, color); -} - -void Nextion::fill_area(uint16_t x1, uint16_t y1, uint16_t width, uint16_t height, const char *color) { - this->add_no_result_to_queue_with_printf_("fill_area", "fill %" PRIu16 ",%" PRIu16 ",%" PRIu16 ",%" PRIu16 ",%s", x1, + this->add_no_result_to_queue_with_printf_("fill", "fill %" PRIu16 ",%" PRIu16 ",%" PRIu16 ",%" PRIu16 ",%" PRIu16, x1, y1, width, height, color); } +void Nextion::fill_area(uint16_t x1, uint16_t y1, uint16_t width, uint16_t height, const char *color) { + this->add_no_result_to_queue_with_printf_("fill", "fill %" PRIu16 ",%" PRIu16 ",%" PRIu16 ",%" PRIu16 ",%s", x1, y1, + width, height, color); +} + void Nextion::fill_area(uint16_t x1, uint16_t y1, uint16_t width, uint16_t height, Color color) { - this->add_no_result_to_queue_with_printf_("fill_area", - "fill %" PRIu16 ",%" PRIu16 ",%" PRIu16 ",%" PRIu16 ",%" PRIu16, x1, y1, - width, height, display::ColorUtil::color_to_565(color)); + this->add_no_result_to_queue_with_printf_("fill", "fill %" PRIu16 ",%" PRIu16 ",%" PRIu16 ",%" PRIu16 ",%" PRIu16, x1, + y1, width, height, display::ColorUtil::color_to_565(color)); } void Nextion::line(uint16_t x1, uint16_t y1, uint16_t x2, uint16_t y2, uint16_t color) { @@ -328,14 +321,14 @@ void Nextion::filled_circle(uint16_t center_x, uint16_t center_y, uint16_t radiu void Nextion::qrcode(uint16_t x1, uint16_t y1, const char *content, uint16_t size, uint16_t background_color, uint16_t foreground_color, int32_t logo_pic, uint8_t border_width) { this->add_no_result_to_queue_with_printf_( - "qrcode", "qrcode %" PRIu16 ",%" PRIu16 ",%" PRIu16 ",%" PRIu16 ",%" PRIu16 ",%" PRIu8 ",%" PRIu8 ",\"%s\"", x1, + "qrcode", "qrcode %" PRIu16 ",%" PRIu16 ",%" PRIu16 ",%" PRIu16 ",%" PRIu16 ",%" PRId32 ",%" PRIu8 ",\"%s\"", x1, y1, size, background_color, foreground_color, logo_pic, border_width, content); } void Nextion::qrcode(uint16_t x1, uint16_t y1, const char *content, uint16_t size, Color background_color, Color foreground_color, int32_t logo_pic, uint8_t border_width) { this->add_no_result_to_queue_with_printf_( - "qrcode", "qrcode %" PRIu16 ",%" PRIu16 ",%" PRIu16 ",%" PRIu16 ",%" PRIu16 ",%" PRIu8 ",%" PRIu8 ",\"%s\"", x1, + "qrcode", "qrcode %" PRIu16 ",%" PRIu16 ",%" PRIu16 ",%" PRIu16 ",%" PRIu16 ",%" PRId32 ",%" PRIu8 ",\"%s\"", x1, y1, size, display::ColorUtil::color_to_565(background_color), display::ColorUtil::color_to_565(foreground_color), logo_pic, border_width, content); } @@ -349,5 +342,4 @@ void Nextion::set_nextion_rtc_time(ESPTime time) { this->add_no_result_to_queue_with_printf_("rtc5", "rtc5=%u", time.second); } -} // namespace nextion -} // namespace esphome +} // namespace esphome::nextion diff --git a/esphome/components/nextion/nextion_component.cpp b/esphome/components/nextion/nextion_component.cpp index 30c8b80524..f457125616 100644 --- a/esphome/components/nextion/nextion_component.cpp +++ b/esphome/components/nextion/nextion_component.cpp @@ -1,7 +1,6 @@ #include "nextion_component.h" -namespace esphome { -namespace nextion { +namespace esphome::nextion { void NextionComponent::set_background_color(Color bco) { if (this->variable_name_ == this->variable_name_to_send_) { @@ -110,5 +109,4 @@ void NextionComponent::update_component_settings(bool force_update) { this->component_flags_.font_id_needs_update = false; } } -} // namespace nextion -} // namespace esphome +} // namespace esphome::nextion diff --git a/esphome/components/nextion/nextion_component.h b/esphome/components/nextion/nextion_component.h index add9e11cf1..068cf51361 100644 --- a/esphome/components/nextion/nextion_component.h +++ b/esphome/components/nextion/nextion_component.h @@ -3,8 +3,8 @@ #include "esphome/core/color.h" #include "nextion_base.h" -namespace esphome { -namespace nextion { +namespace esphome::nextion { + class NextionComponent; class NextionComponent : public NextionComponentBase { @@ -80,5 +80,4 @@ class NextionComponent : public NextionComponentBase { uint16_t reserved : 3; } component_flags_; }; -} // namespace nextion -} // namespace esphome +} // namespace esphome::nextion diff --git a/esphome/components/nextion/nextion_component_base.h b/esphome/components/nextion/nextion_component_base.h index fe0692b875..6676d01920 100644 --- a/esphome/components/nextion/nextion_component_base.h +++ b/esphome/components/nextion/nextion_component_base.h @@ -1,10 +1,11 @@ #pragma once + +#include #include #include #include "esphome/core/defines.h" -namespace esphome { -namespace nextion { +namespace esphome::nextion { enum NextionQueueType { NO_RESULT = 0, @@ -35,12 +36,8 @@ class NextionComponentBase { virtual ~NextionComponentBase() = default; void set_variable_name(const std::string &variable_name, const std::string &variable_name_to_send = "") { - variable_name_ = variable_name; - if (variable_name_to_send.empty()) { - variable_name_to_send_ = variable_name; - } else { - variable_name_to_send_ = variable_name_to_send; - } + this->variable_name_ = variable_name; + this->variable_name_to_send_ = variable_name_to_send.empty() ? variable_name : variable_name_to_send; } virtual void update_component_settings(){}; @@ -64,14 +61,15 @@ class NextionComponentBase { virtual void set_state(const std::string &state, bool publish) {} virtual void set_state(const std::string &state, bool publish, bool send_to_nextion){}; - uint8_t get_component_id() { return this->component_id_; } - void set_component_id(uint8_t component_id) { component_id_ = component_id; } + uint8_t get_component_id() const { return this->component_id_; } + void set_component_id(uint8_t component_id) { this->component_id_ = component_id; } - uint8_t get_wave_channel_id() { return this->wave_chan_id_; } +#ifdef USE_NEXTION_WAVEFORM + uint8_t get_wave_channel_id() const { return this->wave_chan_id_; } void set_wave_channel_id(uint8_t wave_chan_id) { this->wave_chan_id_ = wave_chan_id; } - std::vector get_wave_buffer() { return this->wave_buffer_; } - size_t get_wave_buffer_size() { return this->wave_buffer_.size(); } + const std::vector &get_wave_buffer() const { return this->wave_buffer_; } + size_t get_wave_buffer_size() const { return this->wave_buffer_.size(); } void clear_wave_buffer(size_t buffer_sent) { if (this->wave_buffer_.size() <= buffer_sent) { this->wave_buffer_.clear(); @@ -79,28 +77,34 @@ class NextionComponentBase { this->wave_buffer_.erase(this->wave_buffer_.begin(), this->wave_buffer_.begin() + buffer_sent); } } +#endif // USE_NEXTION_WAVEFORM - std::string get_variable_name() { return this->variable_name_; } - std::string get_variable_name_to_send() { return this->variable_name_to_send_; } - virtual NextionQueueType get_queue_type() { return NextionQueueType::NO_RESULT; } - virtual std::string get_queue_type_string() { return NEXTION_QUEUE_TYPE_STRINGS[this->get_queue_type()]; } + const std::string &get_variable_name() const { return this->variable_name_; } + const std::string &get_variable_name_to_send() const { return this->variable_name_to_send_; } + virtual NextionQueueType get_queue_type() const { return NextionQueueType::NO_RESULT; } + virtual const char *get_queue_type_string() const { return NEXTION_QUEUE_TYPE_STRINGS[this->get_queue_type()]; } virtual void set_state_from_int(int state_value, bool publish, bool send_to_nextion){}; virtual void set_state_from_string(const std::string &state_value, bool publish, bool send_to_nextion){}; virtual void send_state_to_nextion(){}; - bool get_needs_to_send_update() { return this->needs_to_send_update_; } - uint8_t get_wave_chan_id() { return this->wave_chan_id_; } + bool get_needs_to_send_update() const { return this->needs_to_send_update_; } +#ifdef USE_NEXTION_WAVEFORM + // Remove before 2026.10.0 + ESPDEPRECATED("Use get_wave_channel_id() instead. Will be removed in 2026.10.0", "2026.4.0") + uint8_t get_wave_chan_id() const { return this->get_wave_channel_id(); } void set_wave_max_length(int wave_max_length) { this->wave_max_length_ = wave_max_length; } +#endif // USE_NEXTION_WAVEFORM protected: std::string variable_name_; std::string variable_name_to_send_; uint8_t component_id_ = 0; +#ifdef USE_NEXTION_WAVEFORM uint8_t wave_chan_id_ = UINT8_MAX; std::vector wave_buffer_; int wave_max_length_ = 255; +#endif // USE_NEXTION_WAVEFORM bool needs_to_send_update_; }; -} // namespace nextion -} // namespace esphome +} // namespace esphome::nextion diff --git a/esphome/components/nextion/nextion_upload.cpp b/esphome/components/nextion/nextion_upload.cpp index 7ddd7a2f08..a49e7f18d6 100644 --- a/esphome/components/nextion/nextion_upload.cpp +++ b/esphome/components/nextion/nextion_upload.cpp @@ -4,8 +4,8 @@ #include "esphome/core/application.h" -namespace esphome { -namespace nextion { +namespace esphome::nextion { + static const char *const TAG = "nextion.upload"; bool Nextion::upload_end_(bool successful) { @@ -33,7 +33,6 @@ bool Nextion::upload_end_(bool successful) { return successful; } -} // namespace nextion -} // namespace esphome +} // namespace esphome::nextion #endif // USE_NEXTION_TFT_UPLOAD diff --git a/esphome/components/nextion/nextion_upload_arduino.cpp b/esphome/components/nextion/nextion_upload_arduino.cpp index f59b708002..c79c68552e 100644 --- a/esphome/components/nextion/nextion_upload_arduino.cpp +++ b/esphome/components/nextion/nextion_upload_arduino.cpp @@ -11,8 +11,8 @@ #include "esphome/core/log.h" #include "esphome/core/util.h" -namespace esphome { -namespace nextion { +namespace esphome::nextion { + static const char *const TAG = "nextion.upload.arduino"; static constexpr size_t NEXTION_MAX_RESPONSE_LOG_BYTES = 16; @@ -342,8 +342,7 @@ WiFiClient *Nextion::get_wifi_client_() { } #endif // USE_ESP8266 -} // namespace nextion -} // namespace esphome +} // namespace esphome::nextion #endif // NOT USE_ESP32 #endif // USE_NEXTION_TFT_UPLOAD diff --git a/esphome/components/nextion/nextion_upload_esp32.cpp b/esphome/components/nextion/nextion_upload_esp32.cpp index 166bbcc86a..40a284dc46 100644 --- a/esphome/components/nextion/nextion_upload_esp32.cpp +++ b/esphome/components/nextion/nextion_upload_esp32.cpp @@ -14,8 +14,8 @@ #include "esphome/core/log.h" #include "esphome/core/util.h" -namespace esphome { -namespace nextion { +namespace esphome::nextion { + static const char *const TAG = "nextion.upload.esp32"; static constexpr size_t NEXTION_MAX_RESPONSE_LOG_BYTES = 16; @@ -344,8 +344,7 @@ bool Nextion::upload_tft(uint32_t baud_rate, bool exit_reparse) { return this->upload_end_(true); } -} // namespace nextion -} // namespace esphome +} // namespace esphome::nextion #endif // USE_ESP32 #endif // USE_NEXTION_TFT_UPLOAD diff --git a/esphome/components/nextion/sensor/__init__.py b/esphome/components/nextion/sensor/__init__.py index cab531f1db..7351d8f1d5 100644 --- a/esphome/components/nextion/sensor/__init__.py +++ b/esphome/components/nextion/sensor/__init__.py @@ -85,16 +85,16 @@ async def to_code(config): cg.add(var.set_component_id(config[CONF_COMPONENT_ID])) if CONF_WAVE_CHANNEL_ID in config: + cg.add_define("USE_NEXTION_WAVEFORM") cg.add(var.set_wave_channel_id(config[CONF_WAVE_CHANNEL_ID])) - - if CONF_WAVEFORM_SEND_LAST_VALUE in config: - cg.add(var.set_waveform_send_last_value(config[CONF_WAVEFORM_SEND_LAST_VALUE])) - - if CONF_WAVE_MAX_VALUE in config: - cg.add(var.set_wave_max_value(config[CONF_WAVE_MAX_VALUE])) - - if CONF_WAVE_MAX_LENGTH in config: - cg.add(var.set_wave_max_length(config[CONF_WAVE_MAX_LENGTH])) + if CONF_WAVEFORM_SEND_LAST_VALUE in config: + cg.add( + var.set_waveform_send_last_value(config[CONF_WAVEFORM_SEND_LAST_VALUE]) + ) + if CONF_WAVE_MAX_VALUE in config: + cg.add(var.set_wave_max_value(config[CONF_WAVE_MAX_VALUE])) + if CONF_WAVE_MAX_LENGTH in config: + cg.add(var.set_wave_max_length(config[CONF_WAVE_MAX_LENGTH])) @automation.register_action( diff --git a/esphome/components/nextion/sensor/nextion_sensor.cpp b/esphome/components/nextion/sensor/nextion_sensor.cpp index 9ea12cf808..ca657522f9 100644 --- a/esphome/components/nextion/sensor/nextion_sensor.cpp +++ b/esphome/components/nextion/sensor/nextion_sensor.cpp @@ -2,8 +2,7 @@ #include "esphome/core/util.h" #include "esphome/core/log.h" -namespace esphome { -namespace nextion { +namespace esphome::nextion { static const char *const TAG = "nextion_sensor"; @@ -11,37 +10,44 @@ void NextionSensor::process_sensor(const std::string &variable_name, int state) if (!this->nextion_->is_setup()) return; - if (this->wave_chan_id_ == UINT8_MAX && this->variable_name_ == variable_name) { +#ifdef USE_NEXTION_WAVEFORM + if (this->wave_chan_id_ == UINT8_MAX && this->variable_name_ == variable_name) +#else // USE_NEXTION_WAVEFORM + if (this->variable_name_ == variable_name) +#endif // USE_NEXTION_WAVEFORM + { this->publish_state(state); ESP_LOGD(TAG, "Sensor: %s=%d", variable_name.c_str(), state); } } +#ifdef USE_NEXTION_WAVEFORM void NextionSensor::add_to_wave_buffer(float state) { this->needs_to_send_update_ = true; - int wave_state = (int) ((state / (float) this->wave_maxvalue_) * 100); - - wave_buffer_.push_back(wave_state); - + this->wave_buffer_.push_back(wave_state); if (this->wave_buffer_.size() > (size_t) this->wave_max_length_) { this->wave_buffer_.erase(this->wave_buffer_.begin()); } } +#endif // USE_NEXTION_WAVEFORM void NextionSensor::update() { if (!this->nextion_->is_setup() || this->nextion_->is_updating()) return; +#ifdef USE_NEXTION_WAVEFORM if (this->wave_chan_id_ == UINT8_MAX) { this->nextion_->add_to_get_queue(this); } else { if (this->send_last_value_) { this->add_to_wave_buffer(this->last_value_); } - this->wave_update_(); } +#else // USE_NEXTION_WAVEFORM + this->nextion_->add_to_get_queue(this); +#endif // USE_NEXTION_WAVEFORM } void NextionSensor::set_state(float state, bool publish, bool send_to_nextion) { @@ -51,62 +57,60 @@ void NextionSensor::set_state(float state, bool publish, bool send_to_nextion) { if (std::isnan(state)) return; - if (this->wave_chan_id_ == UINT8_MAX) { - if (send_to_nextion) { - if (this->nextion_->is_sleeping() || !this->component_flags_.visible) { - this->needs_to_send_update_ = true; - } else { - this->needs_to_send_update_ = false; - - if (this->precision_ > 0) { - double to_multiply = pow(10, this->precision_); - int state_value = (int) (state * to_multiply); - - this->nextion_->add_no_result_to_queue_with_set(this, (int) state_value); - } else { - this->nextion_->add_no_result_to_queue_with_set(this, (int) state); - } - } - } - } else { +#ifdef USE_NEXTION_WAVEFORM + if (this->wave_chan_id_ != UINT8_MAX) { + // Waveform sensor — buffer the value, don't send directly. if (this->send_last_value_) { this->last_value_ = state; // Update will handle setting the buffer } else { this->add_to_wave_buffer(state); } + this->update_component_settings(); + return; + } +#endif // USE_NEXTION_WAVEFORM + + if (send_to_nextion) { + if (this->nextion_->is_sleeping() || !this->component_flags_.visible) { + this->needs_to_send_update_ = true; + } else { + this->needs_to_send_update_ = false; + if (this->precision_ > 0) { + double to_multiply = pow(10, this->precision_); + int state_value = (int) (state * to_multiply); + this->nextion_->add_no_result_to_queue_with_set(this, (int) state_value); + } else { + this->nextion_->add_no_result_to_queue_with_set(this, (int) state); + } + } } float published_state = state; - if (this->wave_chan_id_ == UINT8_MAX) { - if (publish) { - if (this->precision_ > 0) { - double to_multiply = pow(10, -this->precision_); - published_state = (float) (state * to_multiply); - } - - this->publish_state(published_state); + if (publish) { + if (this->precision_ > 0) { + double to_multiply = pow(10, -this->precision_); + published_state = (float) (state * to_multiply); } + this->publish_state(published_state); } this->update_component_settings(); ESP_LOGN(TAG, "Write: %s=%lf", this->variable_name_.c_str(), published_state); } +#ifdef USE_NEXTION_WAVEFORM void NextionSensor::wave_update_() { if (this->nextion_->is_sleeping() || this->wave_buffer_.empty()) { return; } - #ifdef NEXTION_PROTOCOL_LOG size_t buffer_to_send = this->wave_buffer_.size() < 255 ? this->wave_buffer_.size() : 255; // ADDT command can only send 255 - ESP_LOGN(TAG, "Wave update: %zu/%zu vals to comp %d ch %d", buffer_to_send, this->wave_buffer_.size(), this->component_id_, this->wave_chan_id_); -#endif - +#endif // NEXTION_PROTOCOL_LOG this->nextion_->add_addt_command_to_queue(this); } +#endif // USE_NEXTION_WAVEFORM -} // namespace nextion -} // namespace esphome +} // namespace esphome::nextion diff --git a/esphome/components/nextion/sensor/nextion_sensor.h b/esphome/components/nextion/sensor/nextion_sensor.h index e4dde9a513..72e3982b3a 100644 --- a/esphome/components/nextion/sensor/nextion_sensor.h +++ b/esphome/components/nextion/sensor/nextion_sensor.h @@ -4,8 +4,8 @@ #include "../nextion_component.h" #include "../nextion_base.h" -namespace esphome { -namespace nextion { +namespace esphome::nextion { + class NextionSensor; class NextionSensor : public NextionComponent, public sensor::Sensor, public PollingComponent { @@ -15,23 +15,30 @@ class NextionSensor : public NextionComponent, public sensor::Sensor, public Pol void update_component() override { this->update(); } void update() override; - void add_to_wave_buffer(float state); void set_precision(uint8_t precision) { this->precision_ = precision; } - void set_component_id(uint8_t component_id) { component_id_ = component_id; } - void set_wave_channel_id(uint8_t wave_chan_id) { this->wave_chan_id_ = wave_chan_id; } - void set_wave_max_value(uint32_t wave_maxvalue) { this->wave_maxvalue_ = wave_maxvalue; } + void set_component_id(uint8_t component_id) { this->component_id_ = component_id; } void process_sensor(const std::string &variable_name, int state) override; void set_state(float state) override { this->set_state(state, true, true); } void set_state(float state, bool publish) override { this->set_state(state, publish, true); } void set_state(float state, bool publish, bool send_to_nextion) override; - void set_waveform_send_last_value(bool send_last_value) { this->send_last_value_ = send_last_value; } - uint8_t get_wave_chan_id() { return this->wave_chan_id_; } - void set_wave_max_length(int wave_max_length) { this->wave_max_length_ = wave_max_length; } - NextionQueueType get_queue_type() override { + NextionQueueType get_queue_type() const override { +#ifdef USE_NEXTION_WAVEFORM return this->wave_chan_id_ == UINT8_MAX ? NextionQueueType::SENSOR : NextionQueueType::WAVEFORM_SENSOR; +#else // USE_NEXTION_WAVEFORM + return NextionQueueType::SENSOR; +#endif // USE_NEXTION_WAVEFORM } + +#ifdef USE_NEXTION_WAVEFORM + void add_to_wave_buffer(float state); + void set_wave_channel_id(uint8_t wave_chan_id) { this->wave_chan_id_ = wave_chan_id; } + void set_wave_max_value(uint32_t wave_maxvalue) { this->wave_maxvalue_ = wave_maxvalue; } + void set_waveform_send_last_value(bool send_last_value) { this->send_last_value_ = send_last_value; } + void set_wave_max_length(int wave_max_length) { this->wave_max_length_ = wave_max_length; } +#endif // USE_NEXTION_WAVEFORM + void set_state_from_string(const std::string &state_value, bool publish, bool send_to_nextion) override {} void set_state_from_int(int state_value, bool publish, bool send_to_nextion) override { this->set_state(state_value, publish, send_to_nextion); @@ -39,11 +46,11 @@ class NextionSensor : public NextionComponent, public sensor::Sensor, public Pol protected: uint8_t precision_ = 0; +#ifdef USE_NEXTION_WAVEFORM uint32_t wave_maxvalue_ = 255; - float last_value_ = 0; bool send_last_value_ = true; void wave_update_(); +#endif // USE_NEXTION_WAVEFORM }; -} // namespace nextion -} // namespace esphome +} // namespace esphome::nextion diff --git a/esphome/components/nextion/switch/nextion_switch.cpp b/esphome/components/nextion/switch/nextion_switch.cpp index 21636f2bfa..0018cff005 100644 --- a/esphome/components/nextion/switch/nextion_switch.cpp +++ b/esphome/components/nextion/switch/nextion_switch.cpp @@ -2,8 +2,7 @@ #include "esphome/core/util.h" #include "esphome/core/log.h" -namespace esphome { -namespace nextion { +namespace esphome::nextion { static const char *const TAG = "nextion_switch"; @@ -48,5 +47,4 @@ void NextionSwitch::set_state(bool state, bool publish, bool send_to_nextion) { void NextionSwitch::write_state(bool state) { this->set_state(state); } -} // namespace nextion -} // namespace esphome +} // namespace esphome::nextion diff --git a/esphome/components/nextion/switch/nextion_switch.h b/esphome/components/nextion/switch/nextion_switch.h index 1548287473..7e0593d217 100644 --- a/esphome/components/nextion/switch/nextion_switch.h +++ b/esphome/components/nextion/switch/nextion_switch.h @@ -4,8 +4,8 @@ #include "../nextion_component.h" #include "../nextion_base.h" -namespace esphome { -namespace nextion { +namespace esphome::nextion { + class NextionSwitch; class NextionSwitch : public NextionComponent, public switch_::Switch, public PollingComponent { @@ -21,7 +21,7 @@ class NextionSwitch : public NextionComponent, public switch_::Switch, public Po void set_state(bool state, bool publish, bool send_to_nextion) override; void send_state_to_nextion() override { this->set_state(this->state, false, true); }; - NextionQueueType get_queue_type() override { return NextionQueueType::SWITCH; } + NextionQueueType get_queue_type() const override { return NextionQueueType::SWITCH; } void set_state_from_string(const std::string &state_value, bool publish, bool send_to_nextion) override {} void set_state_from_int(int state_value, bool publish, bool send_to_nextion) override { this->set_state(state_value != 0, publish, send_to_nextion); @@ -30,5 +30,4 @@ class NextionSwitch : public NextionComponent, public switch_::Switch, public Po protected: void write_state(bool state) override; }; -} // namespace nextion -} // namespace esphome +} // namespace esphome::nextion diff --git a/esphome/components/nextion/text_sensor/nextion_textsensor.cpp b/esphome/components/nextion/text_sensor/nextion_textsensor.cpp index 9b6deeda87..45e4691423 100644 --- a/esphome/components/nextion/text_sensor/nextion_textsensor.cpp +++ b/esphome/components/nextion/text_sensor/nextion_textsensor.cpp @@ -2,8 +2,8 @@ #include "esphome/core/util.h" #include "esphome/core/log.h" -namespace esphome { -namespace nextion { +namespace esphome::nextion { + static const char *const TAG = "nextion_textsensor"; void NextionTextSensor::process_text(const std::string &variable_name, const std::string &text_value) { @@ -45,5 +45,4 @@ void NextionTextSensor::set_state(const std::string &state, bool publish, bool s ESP_LOGN(TAG, "Write: %s='%s'", this->variable_name_.c_str(), state.c_str()); } -} // namespace nextion -} // namespace esphome +} // namespace esphome::nextion diff --git a/esphome/components/nextion/text_sensor/nextion_textsensor.h b/esphome/components/nextion/text_sensor/nextion_textsensor.h index 5716d0a008..42cd5dcef4 100644 --- a/esphome/components/nextion/text_sensor/nextion_textsensor.h +++ b/esphome/components/nextion/text_sensor/nextion_textsensor.h @@ -4,8 +4,8 @@ #include "../nextion_component.h" #include "../nextion_base.h" -namespace esphome { -namespace nextion { +namespace esphome::nextion { + class NextionTextSensor; class NextionTextSensor : public NextionComponent, public text_sensor::TextSensor, public PollingComponent { @@ -22,11 +22,10 @@ class NextionTextSensor : public NextionComponent, public text_sensor::TextSenso void set_state(const std::string &state, bool publish, bool send_to_nextion) override; void send_state_to_nextion() override { this->set_state(this->state, false, true); }; - NextionQueueType get_queue_type() override { return NextionQueueType::TEXT_SENSOR; } + NextionQueueType get_queue_type() const override { return NextionQueueType::TEXT_SENSOR; } void set_state_from_int(int state_value, bool publish, bool send_to_nextion) override {} void set_state_from_string(const std::string &state_value, bool publish, bool send_to_nextion) override { this->set_state(state_value, publish, send_to_nextion); } }; -} // namespace nextion -} // namespace esphome +} // namespace esphome::nextion diff --git a/esphome/components/number/__init__.py b/esphome/components/number/__init__.py index 90f9fe1835..26d2602ba4 100644 --- a/esphome/components/number/__init__.py +++ b/esphome/components/number/__init__.py @@ -79,6 +79,7 @@ from esphome.const import ( DEVICE_CLASS_WIND_SPEED, ) from esphome.core import CORE, CoroPriority, coroutine_with_priority +from esphome.core.config import UNIT_OF_MEASUREMENT_MAX_LENGTH from esphome.core.entity_helpers import ( entity_duplicate_validator, setup_device_class, @@ -186,7 +187,11 @@ NUMBER_OPERATION_OPTIONS = { } validate_device_class = cv.one_of(*DEVICE_CLASSES, lower=True, space="_") -validate_unit_of_measurement = cv.string_strict +validate_unit_of_measurement = cv.All( + cv.string_strict, + # Keep in sync with max_data_length in api.proto + cv.Length(max=UNIT_OF_MEASUREMENT_MAX_LENGTH), +) _NUMBER_SCHEMA = ( cv.ENTITY_BASE_SCHEMA.extend(web_server.WEBSERVER_SORTING_SCHEMA) diff --git a/esphome/components/online_image/__init__.py b/esphome/components/online_image/__init__.py index 292e2bb3bb..5b8294c70e 100644 --- a/esphome/components/online_image/__init__.py +++ b/esphome/components/online_image/__init__.py @@ -7,14 +7,7 @@ from esphome.components.const import CONF_REQUEST_HEADERS from esphome.components.http_request import CONF_HTTP_REQUEST_ID, HttpRequestComponent from esphome.components.image import CONF_TRANSPARENCY, add_metadata import esphome.config_validation as cv -from esphome.const import ( - CONF_BUFFER_SIZE, - CONF_ID, - CONF_ON_ERROR, - CONF_TRIGGER_ID, - CONF_TYPE, - CONF_URL, -) +from esphome.const import CONF_BUFFER_SIZE, CONF_ID, CONF_ON_ERROR, CONF_TYPE, CONF_URL from esphome.core import Lambda AUTO_LOAD = ["image", "runtime_image"] @@ -41,14 +34,6 @@ ReleaseImageAction = online_image_ns.class_( "OnlineImageReleaseAction", automation.Action, cg.Parented.template(OnlineImage) ) -# Triggers -DownloadFinishedTrigger = online_image_ns.class_( - "DownloadFinishedTrigger", automation.Trigger.template() -) -DownloadErrorTrigger = online_image_ns.class_( - "DownloadErrorTrigger", automation.Trigger.template() -) - ONLINE_IMAGE_SCHEMA = ( runtime_image.runtime_image_schema(OnlineImage) @@ -61,18 +46,8 @@ ONLINE_IMAGE_SCHEMA = ( cv.Optional(CONF_REQUEST_HEADERS): cv.All( cv.Schema({cv.string: cv.templatable(cv.string)}) ), - cv.Optional(CONF_ON_DOWNLOAD_FINISHED): automation.validate_automation( - { - cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id( - DownloadFinishedTrigger - ), - } - ), - cv.Optional(CONF_ON_ERROR): automation.validate_automation( - { - cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(DownloadErrorTrigger), - } - ), + cv.Optional(CONF_ON_DOWNLOAD_FINISHED): automation.validate_automation({}), + cv.Optional(CONF_ON_ERROR): automation.validate_automation({}), } ) .extend(cv.polling_component_schema("never")) @@ -165,9 +140,11 @@ async def to_code(config): cg.add(var.add_request_header(key, value)) for conf in config.get(CONF_ON_DOWNLOAD_FINISHED, []): - trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) - await automation.build_automation(trigger, [(bool, "cached")], conf) + await automation.build_callback_automation( + var, "add_on_finished_callback", [(bool, "cached")], conf + ) for conf in config.get(CONF_ON_ERROR, []): - trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) - await automation.build_automation(trigger, [], conf) + await automation.build_callback_automation( + var, "add_on_error_callback", [], conf + ) diff --git a/esphome/components/online_image/online_image.h b/esphome/components/online_image/online_image.h index 3a348cbb07..816d6525ea 100644 --- a/esphome/components/online_image/online_image.h +++ b/esphome/components/online_image/online_image.h @@ -129,18 +129,4 @@ template class OnlineImageReleaseAction : public Action { OnlineImage *parent_; }; -class DownloadFinishedTrigger : public Trigger { - public: - explicit DownloadFinishedTrigger(OnlineImage *parent) { - parent->add_on_finished_callback([this](bool cached) { this->trigger(cached); }); - } -}; - -class DownloadErrorTrigger : public Trigger<> { - public: - explicit DownloadErrorTrigger(OnlineImage *parent) { - parent->add_on_error_callback([this]() { this->trigger(); }); - } -}; - } // namespace esphome::online_image diff --git a/esphome/components/opentherm/input.py b/esphome/components/opentherm/input.py index c5814f74e2..e711e4df8e 100644 --- a/esphome/components/opentherm/input.py +++ b/esphome/components/opentherm/input.py @@ -2,51 +2,48 @@ from typing import Any import esphome.codegen as cg import esphome.config_validation as cv +from esphome.const import CONF_MAX_VALUE, CONF_MIN_VALUE from . import generate, schema -CONF_min_value = "min_value" -CONF_max_value = "max_value" -CONF_auto_min_value = "auto_min_value" -CONF_auto_max_value = "auto_max_value" -CONF_step = "step" +CONF_AUTO_MIN_VALUE = "auto_min_value" +CONF_AUTO_MAX_VALUE = "auto_max_value" OpenthermInput = generate.opentherm_ns.class_("OpenthermInput") def validate_min_value_less_than_max_value(conf): if ( - CONF_min_value in conf - and CONF_max_value in conf - and conf[CONF_min_value] > conf[CONF_max_value] + CONF_MIN_VALUE in conf + and CONF_MAX_VALUE in conf + and conf[CONF_MIN_VALUE] > conf[CONF_MAX_VALUE] ): - raise cv.Invalid(f"{CONF_min_value} must be less than {CONF_max_value}") + raise cv.Invalid(f"{CONF_MIN_VALUE} must be less than {CONF_MAX_VALUE}") return conf def input_schema(entity: schema.InputSchema) -> cv.Schema: result = cv.Schema( { - cv.Optional(CONF_min_value, entity.range[0]): cv.float_range( + cv.Optional(CONF_MIN_VALUE, entity.range[0]): cv.float_range( entity.range[0], entity.range[1] ), - cv.Optional(CONF_max_value, entity.range[1]): cv.float_range( + cv.Optional(CONF_MAX_VALUE, entity.range[1]): cv.float_range( entity.range[0], entity.range[1] ), } ) result = result.add_extra(validate_min_value_less_than_max_value) - result = result.extend({cv.Optional(CONF_step, False): cv.float_}) if entity.auto_min_value is not None: - result = result.extend({cv.Optional(CONF_auto_min_value, False): cv.boolean}) + result = result.extend({cv.Optional(CONF_AUTO_MIN_VALUE, False): cv.boolean}) if entity.auto_max_value is not None: - result = result.extend({cv.Optional(CONF_auto_max_value, False): cv.boolean}) + result = result.extend({cv.Optional(CONF_AUTO_MAX_VALUE, False): cv.boolean}) return result def generate_setters(entity: cg.MockObj, conf: dict[str, Any]) -> None: - generate.add_property_set(entity, CONF_min_value, conf) - generate.add_property_set(entity, CONF_max_value, conf) - generate.add_property_set(entity, CONF_auto_min_value, conf) - generate.add_property_set(entity, CONF_auto_max_value, conf) + generate.add_property_set(entity, CONF_MIN_VALUE, conf) + generate.add_property_set(entity, CONF_MAX_VALUE, conf) + generate.add_property_set(entity, CONF_AUTO_MIN_VALUE, conf) + generate.add_property_set(entity, CONF_AUTO_MAX_VALUE, conf) diff --git a/esphome/components/opentherm/number/__init__.py b/esphome/components/opentherm/number/__init__.py index 6dbc45f49b..17ec12a61a 100644 --- a/esphome/components/opentherm/number/__init__.py +++ b/esphome/components/opentherm/number/__init__.py @@ -3,7 +3,13 @@ from typing import Any import esphome.codegen as cg from esphome.components import number import esphome.config_validation as cv -from esphome.const import CONF_INITIAL_VALUE, CONF_RESTORE_VALUE, CONF_STEP +from esphome.const import ( + CONF_INITIAL_VALUE, + CONF_MAX_VALUE, + CONF_MIN_VALUE, + CONF_RESTORE_VALUE, + CONF_STEP, +) from .. import const, generate, input, schema, validate @@ -18,9 +24,9 @@ OpenthermNumber = generate.opentherm_ns.class_( async def new_openthermnumber(config: dict[str, Any]) -> cg.Pvariable: var = await number.new_number( config, - min_value=config[input.CONF_min_value], - max_value=config[input.CONF_max_value], - step=config[input.CONF_step], + min_value=config[CONF_MIN_VALUE], + max_value=config[CONF_MAX_VALUE], + step=config[CONF_STEP], ) await cg.register_component(var, config) input.generate_setters(var, config) diff --git a/esphome/components/openthread_info/text_sensor.py b/esphome/components/openthread_info/text_sensor.py index ddec8f264c..b672831bf0 100644 --- a/esphome/components/openthread_info/text_sensor.py +++ b/esphome/components/openthread_info/text_sensor.py @@ -54,7 +54,7 @@ CONFIG_SCHEMA = cv.Schema( { cv.Optional(CONF_IP_ADDRESS): text_sensor.text_sensor_schema( IPAddressOpenThreadInfo, entity_category=ENTITY_CATEGORY_DIAGNOSTIC - ), + ).extend(cv.polling_component_schema("1s")), cv.Optional(CONF_ROLE): text_sensor.text_sensor_schema( RoleOpenThreadInfo, entity_category=ENTITY_CATEGORY_DIAGNOSTIC ).extend(cv.polling_component_schema("1s")), diff --git a/esphome/components/ota/automation.h b/esphome/components/ota/automation.h index 92c0050ba0..29a8878136 100644 --- a/esphome/components/ota/automation.h +++ b/esphome/components/ota/automation.h @@ -4,8 +4,7 @@ #include "esphome/core/automation.h" -namespace esphome { -namespace ota { +namespace esphome::ota { class OTAStateChangeTrigger final : public Trigger, public OTAStateListener { public: @@ -67,6 +66,5 @@ class OTAErrorTrigger final : public Trigger, public OTAStateListener { OTAComponent *parent_; }; -} // namespace ota -} // namespace esphome +} // namespace esphome::ota #endif diff --git a/esphome/components/ota/ota_backend.cpp b/esphome/components/ota/ota_backend.cpp index 01a18a58ef..17949de642 100644 --- a/esphome/components/ota/ota_backend.cpp +++ b/esphome/components/ota/ota_backend.cpp @@ -1,7 +1,6 @@ #include "ota_backend.h" -namespace esphome { -namespace ota { +namespace esphome::ota { #ifdef USE_OTA_STATE_LISTENER OTAGlobalCallback *global_ota_callback{nullptr}; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) @@ -34,5 +33,4 @@ void OTAComponent::notify_state_(OTAState state, float progress, uint8_t error) } #endif -} // namespace ota -} // namespace esphome +} // namespace esphome::ota diff --git a/esphome/components/ota/ota_backend.h b/esphome/components/ota/ota_backend.h index ab0ec58e8a..bd9c481901 100644 --- a/esphome/components/ota/ota_backend.h +++ b/esphome/components/ota/ota_backend.h @@ -8,8 +8,7 @@ #include #endif -namespace esphome { -namespace ota { +namespace esphome::ota { enum OTAResponseTypes { OTA_RESPONSE_OK = 0x00, @@ -38,6 +37,7 @@ enum OTAResponseTypes { OTA_RESPONSE_ERROR_NO_UPDATE_PARTITION = 0x8A, OTA_RESPONSE_ERROR_MD5_MISMATCH = 0x8B, OTA_RESPONSE_ERROR_RP2040_NOT_ENOUGH_SPACE = 0x8C, + OTA_RESPONSE_ERROR_SIGNATURE_INVALID = 0x8D, OTA_RESPONSE_ERROR_UNKNOWN = 0xFF, }; @@ -117,5 +117,4 @@ OTAGlobalCallback *get_global_ota_callback(); // - notify_state_deferred_() when in separate task (e.g., web_server OTA) // This ensures proper listener execution in all contexts. #endif -} // namespace ota -} // namespace esphome +} // namespace esphome::ota diff --git a/esphome/components/ota/ota_backend_arduino_libretiny.cpp b/esphome/components/ota/ota_backend_arduino_libretiny.cpp index d364f75007..dcd71e92dd 100644 --- a/esphome/components/ota/ota_backend_arduino_libretiny.cpp +++ b/esphome/components/ota/ota_backend_arduino_libretiny.cpp @@ -7,8 +7,7 @@ #include -namespace esphome { -namespace ota { +namespace esphome::ota { static const char *const TAG = "ota.arduino_libretiny"; @@ -66,7 +65,5 @@ OTAResponseTypes ArduinoLibreTinyOTABackend::end() { void ArduinoLibreTinyOTABackend::abort() { Update.abort(); } -} // namespace ota -} // namespace esphome - +} // namespace esphome::ota #endif // USE_LIBRETINY diff --git a/esphome/components/ota/ota_backend_arduino_libretiny.h b/esphome/components/ota/ota_backend_arduino_libretiny.h index 4514bf84bd..3d426e6759 100644 --- a/esphome/components/ota/ota_backend_arduino_libretiny.h +++ b/esphome/components/ota/ota_backend_arduino_libretiny.h @@ -4,8 +4,7 @@ #include "esphome/core/defines.h" -namespace esphome { -namespace ota { +namespace esphome::ota { class ArduinoLibreTinyOTABackend final { public: @@ -22,7 +21,5 @@ class ArduinoLibreTinyOTABackend final { std::unique_ptr make_ota_backend(); -} // namespace ota -} // namespace esphome - +} // namespace esphome::ota #endif // USE_LIBRETINY diff --git a/esphome/components/ota/ota_backend_arduino_rp2040.cpp b/esphome/components/ota/ota_backend_arduino_rp2040.cpp index e2a57ec665..bc8ef812e6 100644 --- a/esphome/components/ota/ota_backend_arduino_rp2040.cpp +++ b/esphome/components/ota/ota_backend_arduino_rp2040.cpp @@ -9,8 +9,7 @@ #include -namespace esphome { -namespace ota { +namespace esphome::ota { static const char *const TAG = "ota.arduino_rp2040"; @@ -75,8 +74,6 @@ void ArduinoRP2040OTABackend::abort() { rp2040::preferences_prevent_write(false); } -} // namespace ota -} // namespace esphome - +} // namespace esphome::ota #endif // USE_RP2040 #endif // USE_ARDUINO diff --git a/esphome/components/ota/ota_backend_arduino_rp2040.h b/esphome/components/ota/ota_backend_arduino_rp2040.h index 0956cb4b4b..05bd2f5cc4 100644 --- a/esphome/components/ota/ota_backend_arduino_rp2040.h +++ b/esphome/components/ota/ota_backend_arduino_rp2040.h @@ -6,8 +6,7 @@ #include "esphome/core/defines.h" #include "esphome/core/macros.h" -namespace esphome { -namespace ota { +namespace esphome::ota { class ArduinoRP2040OTABackend final { public: @@ -24,8 +23,6 @@ class ArduinoRP2040OTABackend final { std::unique_ptr make_ota_backend(); -} // namespace ota -} // namespace esphome - +} // namespace esphome::ota #endif // USE_RP2040 #endif // USE_ARDUINO diff --git a/esphome/components/ota/ota_backend_esp_idf.cpp b/esphome/components/ota/ota_backend_esp_idf.cpp index 925bb39645..598fce1562 100644 --- a/esphome/components/ota/ota_backend_esp_idf.cpp +++ b/esphome/components/ota/ota_backend_esp_idf.cpp @@ -3,13 +3,15 @@ #include "esphome/components/md5/md5.h" #include "esphome/core/defines.h" +#include "esphome/core/log.h" #include #include #include -namespace esphome { -namespace ota { +namespace esphome::ota { + +static const char *const TAG = "ota.idf"; std::unique_ptr make_ota_backend() { return make_unique(); } @@ -99,7 +101,12 @@ OTAResponseTypes IDFOTABackend::end() { } } if (err == ESP_ERR_OTA_VALIDATE_FAILED) { +#ifdef USE_OTA_SIGNED_VERIFICATION + ESP_LOGE(TAG, "OTA validation failed (err=0x%X) - possible signature verification failure", err); + return OTA_RESPONSE_ERROR_SIGNATURE_INVALID; +#else return OTA_RESPONSE_ERROR_UPDATE_END; +#endif } if (err == ESP_ERR_FLASH_OP_TIMEOUT || err == ESP_ERR_FLASH_OP_FAIL) { return OTA_RESPONSE_ERROR_WRITING_FLASH; @@ -112,6 +119,5 @@ void IDFOTABackend::abort() { this->update_handle_ = 0; } -} // namespace ota -} // namespace esphome +} // namespace esphome::ota #endif // USE_ESP32 diff --git a/esphome/components/ota/ota_backend_esp_idf.h b/esphome/components/ota/ota_backend_esp_idf.h index a0f538afc0..d007bcd128 100644 --- a/esphome/components/ota/ota_backend_esp_idf.h +++ b/esphome/components/ota/ota_backend_esp_idf.h @@ -7,8 +7,7 @@ #include -namespace esphome { -namespace ota { +namespace esphome::ota { class IDFOTABackend final { public: @@ -29,6 +28,5 @@ class IDFOTABackend final { std::unique_ptr make_ota_backend(); -} // namespace ota -} // namespace esphome +} // namespace esphome::ota #endif // USE_ESP32 diff --git a/esphome/components/pca9554/__init__.py b/esphome/components/pca9554/__init__.py index 626b08a378..99b812b33b 100644 --- a/esphome/components/pca9554/__init__.py +++ b/esphome/components/pca9554/__init__.py @@ -5,6 +5,7 @@ import esphome.config_validation as cv from esphome.const import ( CONF_ID, CONF_INPUT, + CONF_INTERRUPT_PIN, CONF_INVERTED, CONF_MODE, CONF_NUMBER, @@ -29,6 +30,7 @@ CONFIG_SCHEMA = ( { cv.Required(CONF_ID): cv.declare_id(PCA9554Component), cv.Optional(CONF_PIN_COUNT, default=8): cv.one_of(4, 8, 16), + cv.Optional(CONF_INTERRUPT_PIN): pins.internal_gpio_input_pin_schema, } ) .extend(cv.COMPONENT_SCHEMA) @@ -43,6 +45,8 @@ async def to_code(config): cg.add(var.set_pin_count(config[CONF_PIN_COUNT])) await cg.register_component(var, config) await i2c.register_i2c_device(var, config) + if interrupt_pin := config.get(CONF_INTERRUPT_PIN): + cg.add(var.set_interrupt_pin(await cg.gpio_pin_expression(interrupt_pin))) def validate_mode(value): diff --git a/esphome/components/pca9554/pca9554.cpp b/esphome/components/pca9554/pca9554.cpp index d94767ef07..ac4f119dfe 100644 --- a/esphome/components/pca9554/pca9554.cpp +++ b/esphome/components/pca9554/pca9554.cpp @@ -34,12 +34,26 @@ void PCA9554Component::setup() { this->read_inputs_(); ESP_LOGD(TAG, "Initialization complete. Warning: %d, Error: %d", this->status_has_warning(), this->status_has_error()); -} + if (this->interrupt_pin_ != nullptr) { + this->interrupt_pin_->setup(); + this->interrupt_pin_->attach_interrupt(&PCA9554Component::gpio_intr, this, gpio::INTERRUPT_FALLING_EDGE); + // Don't invalidate cache on read — only invalidate when interrupt fires + this->set_invalidate_on_read_(false); + } + // Disable loop until an input pin is configured via pin_mode() + // For interrupt-driven mode, loop is re-enabled by the ISR + // For polling mode, loop is re-enabled when pin_mode() registers an input pin + this->disable_loop(); +} +void IRAM_ATTR PCA9554Component::gpio_intr(PCA9554Component *arg) { arg->enable_loop_soon_any_context(); } void PCA9554Component::loop() { - // Invalidate the cache at the start of each loop. - // The actual read will happen on demand when digital_read() is called + // Invalidate the cache so the next digital_read() triggers a fresh I2C read this->reset_pin_cache_(); + if (this->interrupt_pin_ != nullptr) { + // Interrupt-driven: disable loop until next interrupt fires + this->disable_loop(); + } } void PCA9554Component::dump_config() { @@ -47,6 +61,7 @@ void PCA9554Component::dump_config() { "PCA9554:\n" " I/O Pins: %d", this->pin_count_); + LOG_PIN(" Interrupt Pin: ", this->interrupt_pin_); LOG_I2C_DEVICE(this) if (this->is_failed()) { ESP_LOGE(TAG, ESP_LOG_MSG_COMM_FAIL); @@ -76,6 +91,11 @@ void PCA9554Component::pin_mode(uint8_t pin, gpio::Flags flags) { if (flags == gpio::FLAG_INPUT) { // Clear mode mask bit this->config_mask_ &= ~(1 << pin); + // Enable polling loop for input pins (not needed for interrupt-driven mode + // where the ISR handles re-enabling loop) + if (this->interrupt_pin_ == nullptr) { + this->enable_loop(); + } } else if (flags == gpio::FLAG_OUTPUT) { // Set mode mask bit this->config_mask_ |= 1 << pin; @@ -122,11 +142,6 @@ bool PCA9554Component::write_register_(uint8_t reg, uint16_t value) { float PCA9554Component::get_setup_priority() const { return setup_priority::IO; } -#ifdef USE_LOOP_PRIORITY -// Run our loop() method early to invalidate cache before any other components access the pins -float PCA9554Component::get_loop_priority() const { return 9.0f; } // Just after WIFI -#endif - void PCA9554GPIOPin::setup() { pin_mode(flags_); } void PCA9554GPIOPin::pin_mode(gpio::Flags flags) { this->parent_->pin_mode(this->pin_, flags); } bool PCA9554GPIOPin::digital_read() { return this->parent_->digital_read(this->pin_) != this->inverted_; } diff --git a/esphome/components/pca9554/pca9554.h b/esphome/components/pca9554/pca9554.h index 6dd15ccb4b..f33f9d4592 100644 --- a/esphome/components/pca9554/pca9554.h +++ b/esphome/components/pca9554/pca9554.h @@ -16,22 +16,20 @@ class PCA9554Component : public Component, /// Check i2c availability and setup masks void setup() override; - /// Invalidate cache at start of each loop void loop() override; /// 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; -#ifdef USE_LOOP_PRIORITY - float get_loop_priority() const override; -#endif - void dump_config() override; void set_pin_count(size_t pin_count) { this->pin_count_ = pin_count; } + void set_interrupt_pin(InternalGPIOPin *pin) { this->interrupt_pin_ = pin; } protected: + static void IRAM_ATTR gpio_intr(PCA9554Component *arg); + bool read_inputs_(); bool write_register_(uint8_t reg, uint16_t value); @@ -52,6 +50,7 @@ class PCA9554Component : public Component, uint16_t input_mask_{0x00}; /// Storage for last I2C error seen esphome::i2c::ErrorCode last_error_; + InternalGPIOPin *interrupt_pin_{nullptr}; }; /// Helper class to expose a PCA9554 pin as an internal input GPIO pin. diff --git a/esphome/components/pcd8544/display.py b/esphome/components/pcd8544/display.py index 9d993c2105..2f6dcc56ed 100644 --- a/esphome/components/pcd8544/display.py +++ b/esphome/components/pcd8544/display.py @@ -27,7 +27,7 @@ CONFIG_SCHEMA = cv.All( cv.Required(CONF_DC_PIN): pins.gpio_output_pin_schema, cv.Required(CONF_RESET_PIN): pins.gpio_output_pin_schema, cv.Required(CONF_CS_PIN): pins.gpio_output_pin_schema, # CE - cv.Optional(CONF_CONTRAST, default=0x7F): cv.int_, + cv.Optional(CONF_CONTRAST, default=0x7F): cv.int_range(min=0, max=127), } ) .extend(cv.polling_component_schema("1s")) diff --git a/esphome/components/pcf8574/__init__.py b/esphome/components/pcf8574/__init__.py index f387d0a610..902efd2279 100644 --- a/esphome/components/pcf8574/__init__.py +++ b/esphome/components/pcf8574/__init__.py @@ -5,6 +5,7 @@ import esphome.config_validation as cv from esphome.const import ( CONF_ID, CONF_INPUT, + CONF_INTERRUPT_PIN, CONF_INVERTED, CONF_MODE, CONF_NUMBER, @@ -27,6 +28,7 @@ CONFIG_SCHEMA = ( { cv.Required(CONF_ID): cv.declare_id(PCF8574Component), cv.Optional(CONF_PCF8575, default=False): cv.boolean, + cv.Optional(CONF_INTERRUPT_PIN): pins.internal_gpio_input_pin_schema, } ) .extend(cv.COMPONENT_SCHEMA) @@ -39,6 +41,8 @@ async def to_code(config): await cg.register_component(var, config) await i2c.register_i2c_device(var, config) cg.add(var.set_pcf8575(config[CONF_PCF8575])) + if interrupt_pin := config.get(CONF_INTERRUPT_PIN): + cg.add(var.set_interrupt_pin(await cg.gpio_pin_expression(interrupt_pin))) def validate_mode(value): diff --git a/esphome/components/pcf8574/pcf8574.cpp b/esphome/components/pcf8574/pcf8574.cpp index fa9496e7e4..bf4a9442a2 100644 --- a/esphome/components/pcf8574/pcf8574.cpp +++ b/esphome/components/pcf8574/pcf8574.cpp @@ -15,16 +15,33 @@ void PCF8574Component::setup() { this->write_gpio_(); this->read_gpio_(); + + if (this->interrupt_pin_ != nullptr) { + this->interrupt_pin_->setup(); + this->interrupt_pin_->attach_interrupt(&PCF8574Component::gpio_intr, this, gpio::INTERRUPT_FALLING_EDGE); + // Don't invalidate cache on read — only invalidate when interrupt fires + this->set_invalidate_on_read_(false); + } + // Disable loop until an input pin is configured via pin_mode() + // For interrupt-driven mode, loop is re-enabled by the ISR + // For polling mode, loop is re-enabled when pin_mode() registers an input pin + this->disable_loop(); } +void IRAM_ATTR PCF8574Component::gpio_intr(PCF8574Component *arg) { arg->enable_loop_soon_any_context(); } void PCF8574Component::loop() { - // Invalidate the cache at the start of each loop + // Invalidate the cache so the next digital_read() triggers a fresh I2C read this->reset_pin_cache_(); + if (this->interrupt_pin_ != nullptr) { + // Interrupt-driven: disable loop until next interrupt fires + this->disable_loop(); + } } void PCF8574Component::dump_config() { ESP_LOGCONFIG(TAG, "PCF8574:\n" " Is PCF8575: %s", YESNO(this->pcf8575_)); + LOG_PIN(" Interrupt Pin: ", this->interrupt_pin_); LOG_I2C_DEVICE(this) if (this->is_failed()) { ESP_LOGE(TAG, ESP_LOG_MSG_COMM_FAIL); @@ -51,6 +68,11 @@ void PCF8574Component::pin_mode(uint8_t pin, gpio::Flags flags) { this->mode_mask_ &= ~(1 << pin); // Write GPIO to enable input mode this->write_gpio_(); + // Enable polling loop for input pins (not needed for interrupt-driven mode + // where the ISR handles re-enabling loop) + if (this->interrupt_pin_ == nullptr) { + this->enable_loop(); + } } else if (flags == gpio::FLAG_OUTPUT) { // Set mode mask bit this->mode_mask_ |= 1 << pin; @@ -99,11 +121,6 @@ bool PCF8574Component::write_gpio_() { } float PCF8574Component::get_setup_priority() const { return setup_priority::IO; } -#ifdef USE_LOOP_PRIORITY -// Run our loop() method early to invalidate cache before any other components access the pins -float PCF8574Component::get_loop_priority() const { return 9.0f; } // Just after WIFI -#endif - void PCF8574GPIOPin::setup() { pin_mode(flags_); } void PCF8574GPIOPin::pin_mode(gpio::Flags flags) { this->parent_->pin_mode(this->pin_, flags); } bool PCF8574GPIOPin::digital_read() { return this->parent_->digital_read(this->pin_) != this->inverted_; } diff --git a/esphome/components/pcf8574/pcf8574.h b/esphome/components/pcf8574/pcf8574.h index 23bccc26c9..cae2e930b7 100644 --- a/esphome/components/pcf8574/pcf8574.h +++ b/esphome/components/pcf8574/pcf8574.h @@ -17,22 +17,21 @@ class PCF8574Component : public Component, PCF8574Component() = default; void set_pcf8575(bool pcf8575) { pcf8575_ = pcf8575; } + void set_interrupt_pin(InternalGPIOPin *pin) { this->interrupt_pin_ = pin; } /// Check i2c availability and setup masks void setup() override; - /// Invalidate cache at start of each loop void loop() override; /// 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; -#ifdef USE_LOOP_PRIORITY - float get_loop_priority() const override; -#endif void dump_config() override; protected: + static void IRAM_ATTR gpio_intr(PCF8574Component *arg); + bool digital_read_hw(uint8_t pin) override; bool digital_read_cache(uint8_t pin) override; void digital_write_hw(uint8_t pin, bool value) override; @@ -47,6 +46,7 @@ class PCF8574Component : public Component, /// The state read in read_gpio_ - 1 means HIGH, 0 means LOW uint16_t input_mask_{0x00}; bool pcf8575_; ///< TRUE->16-channel PCF8575, FALSE->8-channel PCF8574 + InternalGPIOPin *interrupt_pin_{nullptr}; }; /// Helper class to expose a PCF8574 pin as an internal input GPIO pin. diff --git a/esphome/components/pi4ioe5v6408/__init__.py b/esphome/components/pi4ioe5v6408/__init__.py index c64f923823..d5b19dab1c 100644 --- a/esphome/components/pi4ioe5v6408/__init__.py +++ b/esphome/components/pi4ioe5v6408/__init__.py @@ -5,6 +5,7 @@ import esphome.config_validation as cv from esphome.const import ( CONF_ID, CONF_INPUT, + CONF_INTERRUPT_PIN, CONF_INVERTED, CONF_MODE, CONF_NUMBER, @@ -33,6 +34,7 @@ CONFIG_SCHEMA = ( { cv.Required(CONF_ID): cv.declare_id(PI4IOE5V6408Component), cv.Optional(CONF_RESET, default=True): cv.boolean, + cv.Optional(CONF_INTERRUPT_PIN): pins.internal_gpio_input_pin_schema, } ) .extend(cv.COMPONENT_SCHEMA) @@ -46,6 +48,8 @@ async def to_code(config): await i2c.register_i2c_device(var, config) cg.add(var.set_reset(config[CONF_RESET])) + if interrupt_pin := config.get(CONF_INTERRUPT_PIN): + cg.add(var.set_interrupt_pin(await cg.gpio_pin_expression(interrupt_pin))) def validate_mode(value): diff --git a/esphome/components/pi4ioe5v6408/pi4ioe5v6408.cpp b/esphome/components/pi4ioe5v6408/pi4ioe5v6408.cpp index 9247e114f0..6e8631022a 100644 --- a/esphome/components/pi4ioe5v6408/pi4ioe5v6408.cpp +++ b/esphome/components/pi4ioe5v6408/pi4ioe5v6408.cpp @@ -33,9 +33,24 @@ void PI4IOE5V6408Component::setup() { return; } } + + // No need to clear latched interrupts before attaching the ISR — if INT is + // already low the ISR fires immediately, loop runs, cache invalidates, and + // the read clears the latch. One harmless extra read at most. + if (this->interrupt_pin_ != nullptr) { + this->interrupt_pin_->setup(); + this->interrupt_pin_->attach_interrupt(&PI4IOE5V6408Component::gpio_intr, this, gpio::INTERRUPT_FALLING_EDGE); + this->set_invalidate_on_read_(false); + } + // Disable loop until an input pin is configured via pin_mode() + // For interrupt-driven mode, loop is re-enabled by the ISR + // For polling mode, loop is re-enabled when pin_mode() registers an input pin + this->disable_loop(); } +void IRAM_ATTR PI4IOE5V6408Component::gpio_intr(PI4IOE5V6408Component *arg) { arg->enable_loop_soon_any_context(); } void PI4IOE5V6408Component::dump_config() { ESP_LOGCONFIG(TAG, "PI4IOE5V6408:"); + LOG_PIN(" Interrupt Pin: ", this->interrupt_pin_); LOG_I2C_DEVICE(this) if (this->is_failed()) { ESP_LOGE(TAG, ESP_LOG_MSG_COMM_FAIL); @@ -55,12 +70,22 @@ void PI4IOE5V6408Component::pin_mode(uint8_t pin, gpio::Flags flags) { this->pull_up_down_mask_ &= ~(1 << pin); this->pull_enable_mask_ |= 1 << pin; } + // Enable polling loop for input pins (not needed for interrupt-driven mode + // where the ISR handles re-enabling loop) + if (this->interrupt_pin_ == nullptr) { + this->enable_loop(); + } } // Write GPIO to enable input mode this->write_gpio_modes_(); } -void PI4IOE5V6408Component::loop() { this->reset_pin_cache_(); } +void PI4IOE5V6408Component::loop() { + this->reset_pin_cache_(); + if (this->interrupt_pin_ != nullptr) { + this->disable_loop(); + } +} bool PI4IOE5V6408Component::read_gpio_outputs_() { if (this->is_failed()) @@ -142,6 +167,13 @@ bool PI4IOE5V6408Component::write_gpio_modes_() { this->status_set_warning(LOG_STR("Failed to write GPIO pull enable")); return false; } + // Enable interrupts for input pins when interrupt pin is configured + // (input pins have mode_mask_ bit cleared) + if (this->interrupt_pin_ != nullptr && + !this->write_byte(PI4IOE5V6408_REGISTER_INTERRUPT_ENABLE_MASK, static_cast(~this->mode_mask_))) { + this->status_set_warning(LOG_STR("Failed to write interrupt enable mask")); + return false; + } #if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE ESP_LOGV(TAG, "Wrote GPIO config:\n" diff --git a/esphome/components/pi4ioe5v6408/pi4ioe5v6408.h b/esphome/components/pi4ioe5v6408/pi4ioe5v6408.h index 4dc31201ce..ff2474fe99 100644 --- a/esphome/components/pi4ioe5v6408/pi4ioe5v6408.h +++ b/esphome/components/pi4ioe5v6408/pi4ioe5v6408.h @@ -22,8 +22,11 @@ class PI4IOE5V6408Component : public Component, /// Indicate if the component should reset the state during setup void set_reset(bool reset) { this->reset_ = reset; } + void set_interrupt_pin(InternalGPIOPin *pin) { this->interrupt_pin_ = pin; } protected: + static void IRAM_ATTR gpio_intr(PI4IOE5V6408Component *arg); + bool digital_read_hw(uint8_t pin) override; bool digital_read_cache(uint8_t pin) override; void digital_write_hw(uint8_t pin, bool value) override; @@ -40,6 +43,7 @@ class PI4IOE5V6408Component : public Component, uint8_t pull_up_down_mask_{0x00}; bool reset_{true}; + InternalGPIOPin *interrupt_pin_{nullptr}; bool read_gpio_modes_(); bool write_gpio_modes_(); diff --git a/esphome/components/pid/pid_autotuner.cpp b/esphome/components/pid/pid_autotuner.cpp index e1ddd1d7c6..3b971e6559 100644 --- a/esphome/components/pid/pid_autotuner.cpp +++ b/esphome/components/pid/pid_autotuner.cpp @@ -101,10 +101,10 @@ PIDAutotuner::PIDAutotuneResult PIDAutotuner::update(float setpoint, float proce if (!zc_symmetrical || !amplitude_convergent) { // The frequency/amplitude is not fully accurate yet, try to wait // until the fault clears, or terminate after a while anyway - if (zc_symmetrical) { + if (!zc_symmetrical) { ESP_LOGVV(TAG, "%s: ZC is not symmetrical", this->id_.c_str()); } - if (amplitude_convergent) { + if (!amplitude_convergent) { ESP_LOGVV(TAG, "%s: Amplitude is not convergent", this->id_.c_str()); } uint32_t phase = this->relay_function_.phase_count; diff --git a/esphome/components/pid/pid_autotuner.h b/esphome/components/pid/pid_autotuner.h index 98dc02bcc4..1db9ca7138 100644 --- a/esphome/components/pid/pid_autotuner.h +++ b/esphome/components/pid/pid_autotuner.h @@ -3,7 +3,6 @@ #include "esphome/core/component.h" #include "esphome/core/optional.h" #include "pid_controller.h" -#include "pid_simulator.h" #include diff --git a/esphome/components/pid/pid_simulator.h b/esphome/components/pid/pid_simulator.h deleted file mode 100644 index 30222f2f7a..0000000000 --- a/esphome/components/pid/pid_simulator.h +++ /dev/null @@ -1,77 +0,0 @@ -#pragma once - -#include "esphome/core/component.h" -#include "esphome/core/helpers.h" -#include "esphome/components/sensor/sensor.h" -#include "esphome/components/output/float_output.h" - -#include - -namespace esphome { -namespace pid { - -class PIDSimulator : public PollingComponent, public output::FloatOutput { - public: - PIDSimulator() : PollingComponent(1000) {} - - float surface = 1; /// surface area in m² - float mass = 3; /// mass of simulated object in kg - float temperature = 21; /// current temperature of object in °C - float efficiency = 0.98; /// heating efficiency, 1 is 100% efficient - float thermal_conductivity = 15; /// thermal conductivity of surface are in W/(m*K), here: steel - float specific_heat_capacity = 4.182; /// specific heat capacity of mass in kJ/(kg*K), here: water - float heat_power = 500; /// Heating power in W - float ambient_temperature = 20; /// Ambient temperature in °C - float update_interval = 1; /// The simulated updated interval in seconds - std::vector delayed_temps; /// storage of past temperatures for delaying temperature reading - size_t delay_cycles = 15; /// how many update cycles to delay the output - float output_value = 0.0; /// Current output value of heating element - sensor::Sensor *sensor = new sensor::Sensor(); - - float delta_t(float power) { - // P = Q / t - // Q = c * m * 𝚫t - // 𝚫t = (P*t) / (c*m) - float c = this->specific_heat_capacity; - float t = this->update_interval; - float p = power / 1000; // in kW - float m = this->mass; - return (p * t) / (c * m); - } - - float update_temp() { - float value = clamp(output_value, 0.0f, 1.0f); - - // Heat - float power = value * heat_power * efficiency; - temperature += this->delta_t(power); - - // Cool - // Q = k_w * A * (T_mass - T_ambient) - // P = Q / t - float dt = temperature - ambient_temperature; - float cool_power = (thermal_conductivity * surface * dt) / update_interval; - temperature -= this->delta_t(cool_power); - - // Delay temperature readings - delayed_temps.push_back(temperature); - if (delayed_temps.size() > delay_cycles) - delayed_temps.erase(delayed_temps.begin()); - float prev_temp = this->delayed_temps[0]; - float alpha = 0.1f; - float ret = (1 - alpha) * prev_temp + alpha * prev_temp; - return ret; - } - - void setup() override { sensor->publish_state(this->temperature); } - void update() override { - float new_temp = this->update_temp(); - sensor->publish_state(new_temp); - } - - protected: - void write_state(float state) override { this->output_value = state; } -}; - -} // namespace pid -} // namespace esphome diff --git a/esphome/components/pipsolar/output/__init__.py b/esphome/components/pipsolar/output/__init__.py index 81e99e15a2..4ae8d9d487 100644 --- a/esphome/components/pipsolar/output/__init__.py +++ b/esphome/components/pipsolar/output/__init__.py @@ -94,7 +94,7 @@ async def to_code(config): SetOutputAction, cv.Schema( { - cv.Required(CONF_ID): cv.use_id(CONF_ID), + cv.Required(CONF_ID): cv.use_id(PipsolarOutput), cv.Required(CONF_VALUE): cv.templatable(cv.positive_float), } ), diff --git a/esphome/components/pipsolar/sensor/__init__.py b/esphome/components/pipsolar/sensor/__init__.py index 8d3ba10d62..88c6566d63 100644 --- a/esphome/components/pipsolar/sensor/__init__.py +++ b/esphome/components/pipsolar/sensor/__init__.py @@ -102,46 +102,56 @@ TYPES = { unit_of_measurement=UNIT_HERTZ, icon=ICON_CURRENT_AC, accuracy_decimals=1, + device_class=DEVICE_CLASS_FREQUENCY, + state_class=STATE_CLASS_MEASUREMENT, ), CONF_AC_OUTPUT_RATING_CURRENT: sensor.sensor_schema( unit_of_measurement=UNIT_AMPERE, accuracy_decimals=1, device_class=DEVICE_CLASS_CURRENT, + state_class=STATE_CLASS_MEASUREMENT, ), CONF_AC_OUTPUT_RATING_APPARENT_POWER: sensor.sensor_schema( unit_of_measurement=UNIT_VOLT_AMPS, accuracy_decimals=0, device_class=DEVICE_CLASS_APPARENT_POWER, + state_class=STATE_CLASS_MEASUREMENT, ), CONF_AC_OUTPUT_RATING_ACTIVE_POWER: sensor.sensor_schema( unit_of_measurement=UNIT_WATT, accuracy_decimals=0, device_class=DEVICE_CLASS_POWER, + state_class=STATE_CLASS_MEASUREMENT, ), CONF_BATTERY_RATING_VOLTAGE: sensor.sensor_schema( unit_of_measurement=UNIT_VOLT, accuracy_decimals=1, device_class=DEVICE_CLASS_VOLTAGE, + state_class=STATE_CLASS_MEASUREMENT, ), CONF_BATTERY_RECHARGE_VOLTAGE: sensor.sensor_schema( unit_of_measurement=UNIT_VOLT, accuracy_decimals=1, device_class=DEVICE_CLASS_VOLTAGE, + state_class=STATE_CLASS_MEASUREMENT, ), CONF_BATTERY_UNDER_VOLTAGE: sensor.sensor_schema( unit_of_measurement=UNIT_VOLT, accuracy_decimals=1, device_class=DEVICE_CLASS_VOLTAGE, + state_class=STATE_CLASS_MEASUREMENT, ), CONF_BATTERY_BULK_VOLTAGE: sensor.sensor_schema( unit_of_measurement=UNIT_VOLT, accuracy_decimals=1, device_class=DEVICE_CLASS_VOLTAGE, + state_class=STATE_CLASS_MEASUREMENT, ), CONF_BATTERY_FLOAT_VOLTAGE: sensor.sensor_schema( unit_of_measurement=UNIT_VOLT, accuracy_decimals=1, device_class=DEVICE_CLASS_VOLTAGE, + state_class=STATE_CLASS_MEASUREMENT, ), CONF_BATTERY_TYPE: sensor.sensor_schema( accuracy_decimals=0, @@ -150,11 +160,13 @@ TYPES = { unit_of_measurement=UNIT_AMPERE, accuracy_decimals=0, device_class=DEVICE_CLASS_CURRENT, + state_class=STATE_CLASS_MEASUREMENT, ), CONF_CURRENT_MAX_CHARGING_CURRENT: sensor.sensor_schema( unit_of_measurement=UNIT_AMPERE, accuracy_decimals=0, device_class=DEVICE_CLASS_CURRENT, + state_class=STATE_CLASS_MEASUREMENT, ), CONF_INPUT_VOLTAGE_RANGE: sensor.sensor_schema( accuracy_decimals=0, @@ -294,6 +306,7 @@ TYPES = { unit_of_measurement=UNIT_VOLT, accuracy_decimals=1, device_class=DEVICE_CLASS_VOLTAGE, + state_class=STATE_CLASS_MEASUREMENT, ), CONF_EEPROM_VERSION: sensor.sensor_schema( accuracy_decimals=0, diff --git a/esphome/components/pmsx003/sensor.py b/esphome/components/pmsx003/sensor.py index cdcedc85ac..0a11120bf0 100644 --- a/esphome/components/pmsx003/sensor.py +++ b/esphome/components/pmsx003/sensor.py @@ -185,7 +185,7 @@ def validate_update_interval(value): return value -CONFIG_SCHEMA = ( +CONFIG_SCHEMA = cv.All( cv.Schema( { cv.GenerateID(): cv.declare_id(PMSX003Component), @@ -290,7 +290,8 @@ CONFIG_SCHEMA = ( } ) .extend(cv.COMPONENT_SCHEMA) - .extend(uart.UART_DEVICE_SCHEMA) + .extend(uart.UART_DEVICE_SCHEMA), + validate_pmsx003_sensors, ) diff --git a/esphome/components/pn532/__init__.py b/esphome/components/pn532/__init__.py index 6f679ed10a..4ccda49a72 100644 --- a/esphome/components/pn532/__init__.py +++ b/esphome/components/pn532/__init__.py @@ -19,10 +19,6 @@ CONF_PN532_ID = "pn532_id" pn532_ns = cg.esphome_ns.namespace("pn532") PN532 = pn532_ns.class_("PN532", cg.PollingComponent) -PN532OnFinishedWriteTrigger = pn532_ns.class_( - "PN532OnFinishedWriteTrigger", automation.Trigger.template() -) - PN532IsWritingCondition = pn532_ns.class_( "PN532IsWritingCondition", automation.Condition ) @@ -35,13 +31,7 @@ PN532_SCHEMA = cv.Schema( cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(nfc.NfcOnTagTrigger), } ), - cv.Optional(CONF_ON_FINISHED_WRITE): automation.validate_automation( - { - cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id( - PN532OnFinishedWriteTrigger - ), - } - ), + cv.Optional(CONF_ON_FINISHED_WRITE): automation.validate_automation({}), cv.Optional(CONF_ON_TAG_REMOVED): automation.validate_automation( { cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(nfc.NfcOnTagTrigger), @@ -77,8 +67,9 @@ async def setup_pn532(var, config): ) for conf in config.get(CONF_ON_FINISHED_WRITE, []): - trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) - await automation.build_automation(trigger, [], conf) + await automation.build_callback_automation( + var, "add_on_finished_write_callback", [], conf + ) @automation.register_condition( diff --git a/esphome/components/pn532/pn532.h b/esphome/components/pn532/pn532.h index 1f6a6b3bc3..b76cbb1946 100644 --- a/esphome/components/pn532/pn532.h +++ b/esphome/components/pn532/pn532.h @@ -133,13 +133,6 @@ class PN532BinarySensor : public binary_sensor::BinarySensor { bool found_{false}; }; -class PN532OnFinishedWriteTrigger : public Trigger<> { - public: - explicit PN532OnFinishedWriteTrigger(PN532 *parent) { - parent->add_on_finished_write_callback([this]() { this->trigger(); }); - } -}; - template class PN532IsWritingCondition : public Condition, public Parented { public: bool check(const Ts &...x) override { return this->parent_->is_writing(); } diff --git a/esphome/components/pn7150/__init__.py b/esphome/components/pn7150/__init__.py index 6af1412881..c8723dc31c 100644 --- a/esphome/components/pn7150/__init__.py +++ b/esphome/components/pn7150/__init__.py @@ -50,14 +50,6 @@ SetWriteMessageAction = pn7150_ns.class_("SetWriteMessageAction", automation.Act SetWriteModeAction = pn7150_ns.class_("SetWriteModeAction", automation.Action) -PN7150OnEmulatedTagScanTrigger = pn7150_ns.class_( - "PN7150OnEmulatedTagScanTrigger", automation.Trigger.template() -) - -PN7150OnFinishedWriteTrigger = pn7150_ns.class_( - "PN7150OnFinishedWriteTrigger", automation.Trigger.template() -) - PN7150IsWritingCondition = pn7150_ns.class_( "PN7150IsWritingCondition", automation.Condition ) @@ -83,20 +75,8 @@ SET_MESSAGE_ACTION_SCHEMA = cv.Schema( PN7150_SCHEMA = cv.Schema( { cv.GenerateID(): cv.declare_id(PN7150), - cv.Optional(CONF_ON_EMULATED_TAG_SCAN): automation.validate_automation( - { - cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id( - PN7150OnEmulatedTagScanTrigger - ), - } - ), - cv.Optional(CONF_ON_FINISHED_WRITE): automation.validate_automation( - { - cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id( - PN7150OnFinishedWriteTrigger - ), - } - ), + cv.Optional(CONF_ON_EMULATED_TAG_SCAN): automation.validate_automation({}), + cv.Optional(CONF_ON_FINISHED_WRITE): automation.validate_automation({}), cv.Optional(CONF_ON_TAG): automation.validate_automation( { cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(nfc.NfcOnTagTrigger), @@ -215,12 +195,14 @@ async def setup_pn7150(var, config): ) for conf in config.get(CONF_ON_EMULATED_TAG_SCAN, []): - trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) - await automation.build_automation(trigger, [], conf) + await automation.build_callback_automation( + var, "add_on_emulated_tag_scan_callback", [], conf + ) for conf in config.get(CONF_ON_FINISHED_WRITE, []): - trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) - await automation.build_automation(trigger, [], conf) + await automation.build_callback_automation( + var, "add_on_finished_write_callback", [], conf + ) @automation.register_condition( diff --git a/esphome/components/pn7150/automation.h b/esphome/components/pn7150/automation.h index 21329a998a..a8c65ae633 100644 --- a/esphome/components/pn7150/automation.h +++ b/esphome/components/pn7150/automation.h @@ -7,20 +7,6 @@ namespace esphome { namespace pn7150 { -class PN7150OnEmulatedTagScanTrigger : public Trigger<> { - public: - explicit PN7150OnEmulatedTagScanTrigger(PN7150 *parent) { - parent->add_on_emulated_tag_scan_callback([this]() { this->trigger(); }); - } -}; - -class PN7150OnFinishedWriteTrigger : public Trigger<> { - public: - explicit PN7150OnFinishedWriteTrigger(PN7150 *parent) { - parent->add_on_finished_write_callback([this]() { this->trigger(); }); - } -}; - template class PN7150IsWritingCondition : public Condition, public Parented { public: bool check(const Ts &...x) override { return this->parent_->is_writing(); } diff --git a/esphome/components/pn7160/__init__.py b/esphome/components/pn7160/__init__.py index 54e4b74796..e382594b93 100644 --- a/esphome/components/pn7160/__init__.py +++ b/esphome/components/pn7160/__init__.py @@ -52,14 +52,6 @@ SetWriteMessageAction = pn7160_ns.class_("SetWriteMessageAction", automation.Act SetWriteModeAction = pn7160_ns.class_("SetWriteModeAction", automation.Action) -PN7160OnEmulatedTagScanTrigger = pn7160_ns.class_( - "PN7160OnEmulatedTagScanTrigger", automation.Trigger.template() -) - -PN7160OnFinishedWriteTrigger = pn7160_ns.class_( - "PN7160OnFinishedWriteTrigger", automation.Trigger.template() -) - PN7160IsWritingCondition = pn7160_ns.class_( "PN7160IsWritingCondition", automation.Condition ) @@ -85,20 +77,8 @@ SET_MESSAGE_ACTION_SCHEMA = cv.Schema( PN7160_SCHEMA = cv.Schema( { cv.GenerateID(): cv.declare_id(PN7160), - cv.Optional(CONF_ON_EMULATED_TAG_SCAN): automation.validate_automation( - { - cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id( - PN7160OnEmulatedTagScanTrigger - ), - } - ), - cv.Optional(CONF_ON_FINISHED_WRITE): automation.validate_automation( - { - cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id( - PN7160OnFinishedWriteTrigger - ), - } - ), + cv.Optional(CONF_ON_EMULATED_TAG_SCAN): automation.validate_automation({}), + cv.Optional(CONF_ON_FINISHED_WRITE): automation.validate_automation({}), cv.Optional(CONF_ON_TAG): automation.validate_automation( { cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(nfc.NfcOnTagTrigger), @@ -227,12 +207,14 @@ async def setup_pn7160(var, config): ) for conf in config.get(CONF_ON_EMULATED_TAG_SCAN, []): - trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) - await automation.build_automation(trigger, [], conf) + await automation.build_callback_automation( + var, "add_on_emulated_tag_scan_callback", [], conf + ) for conf in config.get(CONF_ON_FINISHED_WRITE, []): - trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) - await automation.build_automation(trigger, [], conf) + await automation.build_callback_automation( + var, "add_on_finished_write_callback", [], conf + ) @automation.register_condition( diff --git a/esphome/components/pn7160/automation.h b/esphome/components/pn7160/automation.h index 08148c2311..7759da8f53 100644 --- a/esphome/components/pn7160/automation.h +++ b/esphome/components/pn7160/automation.h @@ -7,20 +7,6 @@ namespace esphome { namespace pn7160 { -class PN7160OnEmulatedTagScanTrigger : public Trigger<> { - public: - explicit PN7160OnEmulatedTagScanTrigger(PN7160 *parent) { - parent->add_on_emulated_tag_scan_callback([this]() { this->trigger(); }); - } -}; - -class PN7160OnFinishedWriteTrigger : public Trigger<> { - public: - explicit PN7160OnFinishedWriteTrigger(PN7160 *parent) { - parent->add_on_finished_write_callback([this]() { this->trigger(); }); - } -}; - template class PN7160IsWritingCondition : public Condition, public Parented { public: bool check(const Ts &...x) override { return this->parent_->is_writing(); } diff --git a/esphome/components/prometheus/__init__.py b/esphome/components/prometheus/__init__.py index 26a9e70f7c..cc1541ce80 100644 --- a/esphome/components/prometheus/__init__.py +++ b/esphome/components/prometheus/__init__.py @@ -10,12 +10,14 @@ AUTO_LOAD = ["web_server_base"] prometheus_ns = cg.esphome_ns.namespace("prometheus") PrometheusHandler = prometheus_ns.class_("PrometheusHandler", cg.Component) -CUSTOMIZED_ENTITY = cv.Schema( - { - cv.Optional(CONF_ID): cv.string_strict, - cv.Optional(CONF_NAME): cv.string_strict, - }, - cv.has_at_least_one_key, +CUSTOMIZED_ENTITY = cv.All( + cv.Schema( + { + cv.Optional(CONF_ID): cv.string_strict, + cv.Optional(CONF_NAME): cv.string_strict, + }, + ), + cv.has_at_least_one_key(CONF_ID, CONF_NAME), ) CONFIG_SCHEMA = cv.Schema( diff --git a/esphome/components/pylontech/sensor/__init__.py b/esphome/components/pylontech/sensor/__init__.py index 716cc1001a..450f663274 100644 --- a/esphome/components/pylontech/sensor/__init__.py +++ b/esphome/components/pylontech/sensor/__init__.py @@ -10,6 +10,7 @@ from esphome.const import ( DEVICE_CLASS_CURRENT, DEVICE_CLASS_TEMPERATURE, DEVICE_CLASS_VOLTAGE, + STATE_CLASS_MEASUREMENT, UNIT_AMPERE, UNIT_CELSIUS, UNIT_PERCENT, @@ -18,7 +19,7 @@ from esphome.const import ( from .. import CONF_BATTERY, CONF_PYLONTECH_ID, PYLONTECH_COMPONENT_SCHEMA, pylontech_ns -PylontechSensor = pylontech_ns.class_("PylontechSensor", cg.Component) +PylontechSensor = pylontech_ns.class_("PylontechSensor") CONF_COULOMB = "coulomb" CONF_TEMPERATURE_LOW = "temperature_low" @@ -32,46 +33,55 @@ TYPES: dict[str, cv.Schema] = { unit_of_measurement=UNIT_VOLT, accuracy_decimals=3, device_class=DEVICE_CLASS_VOLTAGE, + state_class=STATE_CLASS_MEASUREMENT, ), CONF_CURRENT: sensor.sensor_schema( unit_of_measurement=UNIT_AMPERE, accuracy_decimals=3, device_class=DEVICE_CLASS_CURRENT, + state_class=STATE_CLASS_MEASUREMENT, ), CONF_TEMPERATURE: sensor.sensor_schema( unit_of_measurement=UNIT_CELSIUS, accuracy_decimals=1, device_class=DEVICE_CLASS_TEMPERATURE, + state_class=STATE_CLASS_MEASUREMENT, ), CONF_TEMPERATURE_LOW: sensor.sensor_schema( unit_of_measurement=UNIT_CELSIUS, accuracy_decimals=1, device_class=DEVICE_CLASS_TEMPERATURE, + state_class=STATE_CLASS_MEASUREMENT, ), CONF_TEMPERATURE_HIGH: sensor.sensor_schema( unit_of_measurement=UNIT_CELSIUS, accuracy_decimals=1, device_class=DEVICE_CLASS_TEMPERATURE, + state_class=STATE_CLASS_MEASUREMENT, ), CONF_VOLTAGE_LOW: sensor.sensor_schema( unit_of_measurement=UNIT_VOLT, accuracy_decimals=3, device_class=DEVICE_CLASS_VOLTAGE, + state_class=STATE_CLASS_MEASUREMENT, ), CONF_VOLTAGE_HIGH: sensor.sensor_schema( unit_of_measurement=UNIT_VOLT, accuracy_decimals=3, device_class=DEVICE_CLASS_VOLTAGE, + state_class=STATE_CLASS_MEASUREMENT, ), CONF_COULOMB: sensor.sensor_schema( unit_of_measurement=UNIT_PERCENT, accuracy_decimals=0, device_class=DEVICE_CLASS_BATTERY, + state_class=STATE_CLASS_MEASUREMENT, ), CONF_MOS_TEMPERATURE: sensor.sensor_schema( unit_of_measurement=UNIT_CELSIUS, accuracy_decimals=1, device_class=DEVICE_CLASS_TEMPERATURE, + state_class=STATE_CLASS_MEASUREMENT, ), } diff --git a/esphome/components/pylontech/sensor/pylontech_sensor.h b/esphome/components/pylontech/sensor/pylontech_sensor.h index 8986adc26c..25e71606a4 100644 --- a/esphome/components/pylontech/sensor/pylontech_sensor.h +++ b/esphome/components/pylontech/sensor/pylontech_sensor.h @@ -6,7 +6,7 @@ namespace esphome { namespace pylontech { -class PylontechSensor : public PylontechListener, public Component { +class PylontechSensor : public PylontechListener { public: PylontechSensor(int8_t bat_num); void dump_config() override; diff --git a/esphome/components/pylontech/text_sensor/__init__.py b/esphome/components/pylontech/text_sensor/__init__.py index 15741ea9d1..f68ca10374 100644 --- a/esphome/components/pylontech/text_sensor/__init__.py +++ b/esphome/components/pylontech/text_sensor/__init__.py @@ -5,7 +5,7 @@ from esphome.const import CONF_ID from .. import CONF_BATTERY, CONF_PYLONTECH_ID, PYLONTECH_COMPONENT_SCHEMA, pylontech_ns -PylontechTextSensor = pylontech_ns.class_("PylontechTextSensor", cg.Component) +PylontechTextSensor = pylontech_ns.class_("PylontechTextSensor") CONF_BASE_STATE = "base_state" CONF_VOLTAGE_STATE = "voltage_state" diff --git a/esphome/components/pylontech/text_sensor/pylontech_text_sensor.h b/esphome/components/pylontech/text_sensor/pylontech_text_sensor.h index a685512ed5..27a3993b3e 100644 --- a/esphome/components/pylontech/text_sensor/pylontech_text_sensor.h +++ b/esphome/components/pylontech/text_sensor/pylontech_text_sensor.h @@ -6,7 +6,7 @@ namespace esphome { namespace pylontech { -class PylontechTextSensor : public PylontechListener, public Component { +class PylontechTextSensor : public PylontechListener { public: PylontechTextSensor(int8_t bat_num); void dump_config() override; diff --git a/esphome/components/pzemac/sensor.py b/esphome/components/pzemac/sensor.py index fa1c3961d0..c134bc19c1 100644 --- a/esphome/components/pzemac/sensor.py +++ b/esphome/components/pzemac/sensor.py @@ -13,6 +13,7 @@ from esphome.const import ( CONF_VOLTAGE, DEVICE_CLASS_CURRENT, DEVICE_CLASS_ENERGY, + DEVICE_CLASS_FREQUENCY, DEVICE_CLASS_POWER, DEVICE_CLASS_POWER_FACTOR, DEVICE_CLASS_VOLTAGE, @@ -66,6 +67,7 @@ CONFIG_SCHEMA = ( unit_of_measurement=UNIT_HERTZ, icon=ICON_CURRENT_AC, accuracy_decimals=1, + device_class=DEVICE_CLASS_FREQUENCY, state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional(CONF_POWER_FACTOR): sensor.sensor_schema( diff --git a/esphome/components/qmc5883l/sensor.py b/esphome/components/qmc5883l/sensor.py index b79e370a05..fe34381ad8 100644 --- a/esphome/components/qmc5883l/sensor.py +++ b/esphome/components/qmc5883l/sensor.py @@ -100,6 +100,7 @@ heading_schema = sensor.sensor_schema( unit_of_measurement=UNIT_DEGREES, icon=ICON_SCREEN_ROTATION, accuracy_decimals=1, + state_class=STATE_CLASS_MEASUREMENT, ) temperature_schema = sensor.sensor_schema( unit_of_measurement=UNIT_CELSIUS, diff --git a/esphome/components/qmp6988/qmp6988.cpp b/esphome/components/qmp6988/qmp6988.cpp index 17d91c3633..976efe7910 100644 --- a/esphome/components/qmp6988/qmp6988.cpp +++ b/esphome/components/qmp6988/qmp6988.cpp @@ -1,4 +1,6 @@ #include "qmp6988.h" + +#include #include namespace esphome { @@ -129,7 +131,7 @@ bool QMP6988Component::get_calibration_data_() { ESP_LOGV(TAG, "Calibration data:\n" - " COE_a0[%d] COE_a1[%d] COE_a2[%d] COE_b00[%d]\n" + " COE_a0[%" PRId32 "] COE_a1[%d] COE_a2[%d] COE_b00[%" PRId32 "]\n" " COE_bt1[%d] COE_bt2[%d] COE_bp1[%d] COE_b11[%d]\n" " COE_bp2[%d] COE_b12[%d] COE_b21[%d] COE_bp3[%d]", qmp6988_data_.qmp6988_cali.COE_a0, qmp6988_data_.qmp6988_cali.COE_a1, qmp6988_data_.qmp6988_cali.COE_a2, @@ -153,7 +155,7 @@ bool QMP6988Component::get_calibration_data_() { qmp6988_data_.ik.bp3 = 2915L * (int64_t) qmp6988_data_.qmp6988_cali.COE_bp3 + 157155561L; // 28Q65 ESP_LOGV(TAG, "Int calibration data:\n" - " a0[%d] a1[%d] a2[%d] b00[%d]\n" + " a0[%" PRId32 "] a1[%" PRId32 "] a2[%" PRId32 "] b00[%" PRId32 "]\n" " bt1[%lld] bt2[%lld] bp1[%lld] b11[%lld]\n" " bp2[%lld] b12[%lld] b21[%lld] bp3[%lld]", qmp6988_data_.ik.a0, qmp6988_data_.ik.a1, qmp6988_data_.ik.a2, qmp6988_data_.ik.b00, qmp6988_data_.ik.bt1, diff --git a/esphome/components/rd03d/rd03d.cpp b/esphome/components/rd03d/rd03d.cpp index d47347fcfa..c9c6a546ab 100644 --- a/esphome/components/rd03d/rd03d.cpp +++ b/esphome/components/rd03d/rd03d.cpp @@ -1,6 +1,8 @@ #include "rd03d.h" #include "esphome/core/helpers.h" #include "esphome/core/log.h" + +#include #include namespace esphome::rd03d { @@ -56,7 +58,7 @@ void RD03DComponent::dump_config() { *this->tracking_mode_ == TrackingMode::SINGLE_TARGET ? "single" : "multi"); } if (this->throttle_ > 0) { - ESP_LOGCONFIG(TAG, " Throttle: %ums", this->throttle_); + ESP_LOGCONFIG(TAG, " Throttle: %" PRIu32 "ms", this->throttle_); } #ifdef USE_SENSOR LOG_SENSOR(" ", "Target Count", this->target_count_sensor_); diff --git a/esphome/components/remote_base/__init__.py b/esphome/components/remote_base/__init__.py index a0594d7f67..99eda76f81 100644 --- a/esphome/components/remote_base/__init__.py +++ b/esphome/components/remote_base/__init__.py @@ -470,7 +470,7 @@ CANALSATLD_SCHEMA = cv.Schema( ) -@register_binary_sensor("canalsatld", CanalSatLDBinarySensor, CANALSAT_SCHEMA) +@register_binary_sensor("canalsatld", CanalSatLDBinarySensor, CANALSATLD_SCHEMA) def canalsatld_binary_sensor(var, config): cg.add( var.set_data( @@ -1130,7 +1130,7 @@ def sony_dumper(var, config): async def sony_action(var, config, args): template_ = await cg.templatable(config[CONF_DATA], args, cg.uint32) cg.add(var.set_data(template_)) - template_ = await cg.templatable(config[CONF_NBITS], args, cg.uint32) + template_ = await cg.templatable(config[CONF_NBITS], args, cg.uint8) cg.add(var.set_nbits(template_)) @@ -1174,7 +1174,7 @@ def symphony_dumper(var, config): async def symphony_action(var, config, args): template_ = await cg.templatable(config[CONF_DATA], args, cg.uint32) cg.add(var.set_data(template_)) - template_ = await cg.templatable(config[CONF_NBITS], args, cg.uint32) + template_ = await cg.templatable(config[CONF_NBITS], args, cg.uint8) cg.add(var.set_nbits(template_)) template_ = await cg.templatable(config[CONF_COMMAND_REPEATS], args, cg.uint8) cg.add(var.set_repeats(template_)) @@ -1188,7 +1188,7 @@ def validate_raw_alternating(value): this_negative = val < 0 if i != 0 and this_negative == last_negative: raise cv.Invalid( - f"Values must alternate between being positive and negative, please see index {i} and {i + 1}", + f"Values must alternate between being positive and negative, please see index {i - 1} and {i}", [i], ) last_negative = this_negative @@ -2105,12 +2105,12 @@ async def abbwelcome_action(var, config, args): ) cg.add( var.set_source_address( - await cg.templatable(config[CONF_SOURCE_ADDRESS], args, cg.uint16) + await cg.templatable(config[CONF_SOURCE_ADDRESS], args, cg.uint32) ) ) cg.add( var.set_destination_address( - await cg.templatable(config[CONF_DESTINATION_ADDRESS], args, cg.uint16) + await cg.templatable(config[CONF_DESTINATION_ADDRESS], args, cg.uint32) ) ) cg.add( diff --git a/esphome/components/remote_base/gobox_protocol.cpp b/esphome/components/remote_base/gobox_protocol.cpp index 4f6de5e59e..0e1617659d 100644 --- a/esphome/components/remote_base/gobox_protocol.cpp +++ b/esphome/components/remote_base/gobox_protocol.cpp @@ -1,5 +1,6 @@ #include "gobox_protocol.h" #include "esphome/core/log.h" +#include namespace esphome { namespace remote_base { @@ -25,7 +26,7 @@ void GoboxProtocol::encode(RemoteTransmitData *dst, const GoboxData &data) { dst->set_carrier_frequency(38000); dst->reserve((HEADER_SIZE + CODE_SIZE + 1) * 2); uint64_t code = (HEADER << CODE_SIZE) | (data.code & ((1UL << CODE_SIZE) - 1)); - ESP_LOGI(TAG, "Send Gobox: code=0x%Lx", code); + ESP_LOGI(TAG, "Send Gobox: code=0x%016" PRIx64, code); for (int16_t i = (HEADER_SIZE + CODE_SIZE - 1); i >= 0; i--) { if (code & ((uint64_t) 1 << i)) { dst->item(BIT_MARK_US, BIT_ONE_SPACE_US); diff --git a/esphome/components/remote_base/symphony_protocol.cpp b/esphome/components/remote_base/symphony_protocol.cpp index f30a980d91..6844e449ed 100644 --- a/esphome/components/remote_base/symphony_protocol.cpp +++ b/esphome/components/remote_base/symphony_protocol.cpp @@ -1,6 +1,8 @@ #include "symphony_protocol.h" #include "esphome/core/log.h" +#include + namespace esphome { namespace remote_base { @@ -26,8 +28,8 @@ static constexpr uint32_t INTER_FRAME_GAP_US = 34760; void SymphonyProtocol::encode(RemoteTransmitData *dst, const SymphonyData &data) { dst->set_carrier_frequency(CARRIER_FREQUENCY); - ESP_LOGD(TAG, "Sending Symphony: data=0x%0*X nbits=%u repeats=%u", (data.nbits + 3) / 4, (uint32_t) data.data, - data.nbits, data.repeats); + ESP_LOGD(TAG, "Sending Symphony: data=0x%0*" PRIX32 " nbits=%" PRIu8 " repeats=%" PRIu8, (data.nbits + 3) / 4, + (uint32_t) data.data, data.nbits, data.repeats); // Each bit produces a mark+space (2 entries). We fold the inter-frame/footer gap // into the last bit's space of each frame to avoid over-length gaps. dst->reserve(data.nbits * 2u * data.repeats); @@ -112,8 +114,8 @@ optional SymphonyProtocol::decode(RemoteReceiveData src) { } void SymphonyProtocol::dump(const SymphonyData &data) { - const int32_t hex_width = (data.nbits + 3) / 4; // pad to nibble width - ESP_LOGI(TAG, "Received Symphony: data=0x%0*X, nbits=%d", hex_width, (uint32_t) data.data, data.nbits); + const int hex_width = (data.nbits + 3) / 4; // pad to nibble width + ESP_LOGI(TAG, "Received Symphony: data=0x%0*" PRIX32 ", nbits=%" PRIu8, hex_width, (uint32_t) data.data, data.nbits); } } // namespace remote_base diff --git a/esphome/components/resampler/speaker/__init__.py b/esphome/components/resampler/speaker/__init__.py index 4e4705a889..3134cf7646 100644 --- a/esphome/components/resampler/speaker/__init__.py +++ b/esphome/components/resampler/speaker/__init__.py @@ -1,5 +1,5 @@ import esphome.codegen as cg -from esphome.components import audio, esp32, socket, speaker +from esphome.components import audio, esp32, speaker import esphome.config_validation as cv from esphome.const import ( CONF_BITS_PER_SAMPLE, @@ -77,9 +77,6 @@ FINAL_VALIDATE_SCHEMA = _validate_audio_compatibility async def to_code(config): - # Enable wake_loop_threadsafe for immediate command processing from other tasks - socket.require_wake_loop_threadsafe() - var = cg.new_Pvariable(config[CONF_ID]) await cg.register_component(var, config) await speaker.register_speaker(var, config) diff --git a/esphome/components/resampler/speaker/resampler_speaker.cpp b/esphome/components/resampler/speaker/resampler_speaker.cpp index 1303bc459e..3b50353ddc 100644 --- a/esphome/components/resampler/speaker/resampler_speaker.cpp +++ b/esphome/components/resampler/speaker/resampler_speaker.cpp @@ -245,11 +245,9 @@ void ResamplerSpeaker::send_command_(uint32_t command_bit, bool wake_loop) { uint32_t event_bits = xEventGroupGetBits(this->event_group_); if (!(event_bits & command_bit)) { xEventGroupSetBits(this->event_group_, command_bit); -#if defined(USE_SOCKET_SELECT_SUPPORT) && defined(USE_WAKE_LOOP_THREADSAFE) if (wake_loop) { App.wake_loop_threadsafe(); } -#endif } } @@ -317,57 +315,59 @@ void ResamplerSpeaker::resample_task(void *params) { xEventGroupSetBits(this_resampler->event_group_, ResamplingEventGroupBits::STATE_STARTING); - std::unique_ptr resampler = - make_unique(this_resampler->audio_stream_info_.ms_to_bytes(TRANSFER_BUFFER_DURATION_MS), - this_resampler->target_stream_info_.ms_to_bytes(TRANSFER_BUFFER_DURATION_MS)); + { // Ensure C++ objects fall out of scope for proper cleanup before stopping the task + std::unique_ptr resampler = make_unique( + this_resampler->audio_stream_info_.ms_to_bytes(TRANSFER_BUFFER_DURATION_MS), + this_resampler->target_stream_info_.ms_to_bytes(TRANSFER_BUFFER_DURATION_MS)); - esp_err_t err = resampler->start(this_resampler->audio_stream_info_, this_resampler->target_stream_info_, - this_resampler->taps_, this_resampler->filters_); + esp_err_t err = resampler->start(this_resampler->audio_stream_info_, this_resampler->target_stream_info_, + this_resampler->taps_, this_resampler->filters_); - if (err == ESP_OK) { - std::shared_ptr temp_ring_buffer = - RingBuffer::create(this_resampler->audio_stream_info_.ms_to_bytes(this_resampler->buffer_duration_ms_)); + if (err == ESP_OK) { + std::shared_ptr temp_ring_buffer = + RingBuffer::create(this_resampler->audio_stream_info_.ms_to_bytes(this_resampler->buffer_duration_ms_)); - if (!temp_ring_buffer) { - err = ESP_ERR_NO_MEM; - } else { - this_resampler->ring_buffer_ = temp_ring_buffer; - resampler->add_source(this_resampler->ring_buffer_); + if (!temp_ring_buffer) { + err = ESP_ERR_NO_MEM; + } else { + this_resampler->ring_buffer_ = temp_ring_buffer; + resampler->add_source(this_resampler->ring_buffer_); - this_resampler->output_speaker_->set_audio_stream_info(this_resampler->target_stream_info_); - resampler->add_sink(this_resampler->output_speaker_); - } - } - - if (err == ESP_OK) { - xEventGroupSetBits(this_resampler->event_group_, ResamplingEventGroupBits::STATE_RUNNING); - } else if (err == ESP_ERR_NO_MEM) { - xEventGroupSetBits(this_resampler->event_group_, ResamplingEventGroupBits::ERR_ESP_NO_MEM); - } else if (err == ESP_ERR_NOT_SUPPORTED) { - xEventGroupSetBits(this_resampler->event_group_, ResamplingEventGroupBits::ERR_ESP_NOT_SUPPORTED); - } - - while (err == ESP_OK) { - uint32_t event_bits = xEventGroupGetBits(this_resampler->event_group_); - - if (event_bits & ResamplingEventGroupBits::TASK_COMMAND_STOP) { - break; + this_resampler->output_speaker_->set_audio_stream_info(this_resampler->target_stream_info_); + resampler->add_sink(this_resampler->output_speaker_); + } } - // Stop gracefully if the decoder is done - int32_t ms_differential = 0; - audio::AudioResamplerState resampler_state = resampler->resample(false, &ms_differential); - - if (resampler_state == audio::AudioResamplerState::FINISHED) { - break; - } else if (resampler_state == audio::AudioResamplerState::FAILED) { - xEventGroupSetBits(this_resampler->event_group_, ResamplingEventGroupBits::ERR_ESP_FAIL); - break; + if (err == ESP_OK) { + xEventGroupSetBits(this_resampler->event_group_, ResamplingEventGroupBits::STATE_RUNNING); + } else if (err == ESP_ERR_NO_MEM) { + xEventGroupSetBits(this_resampler->event_group_, ResamplingEventGroupBits::ERR_ESP_NO_MEM); + } else if (err == ESP_ERR_NOT_SUPPORTED) { + xEventGroupSetBits(this_resampler->event_group_, ResamplingEventGroupBits::ERR_ESP_NOT_SUPPORTED); } + + while (err == ESP_OK) { + uint32_t event_bits = xEventGroupGetBits(this_resampler->event_group_); + + if (event_bits & ResamplingEventGroupBits::TASK_COMMAND_STOP) { + break; + } + + // Stop gracefully if the decoder is done + int32_t ms_differential = 0; + audio::AudioResamplerState resampler_state = resampler->resample(false, &ms_differential); + + if (resampler_state == audio::AudioResamplerState::FINISHED) { + break; + } else if (resampler_state == audio::AudioResamplerState::FAILED) { + xEventGroupSetBits(this_resampler->event_group_, ResamplingEventGroupBits::ERR_ESP_FAIL); + break; + } + } + + xEventGroupSetBits(this_resampler->event_group_, ResamplingEventGroupBits::STATE_STOPPING); } - xEventGroupSetBits(this_resampler->event_group_, ResamplingEventGroupBits::STATE_STOPPING); - resampler.reset(); xEventGroupSetBits(this_resampler->event_group_, ResamplingEventGroupBits::STATE_STOPPED); vTaskSuspend(nullptr); // Suspend this task indefinitely until the loop method deletes it diff --git a/esphome/components/rf_bridge/__init__.py b/esphome/components/rf_bridge/__init__.py index 934f24b789..4ee1e7891f 100644 --- a/esphome/components/rf_bridge/__init__.py +++ b/esphome/components/rf_bridge/__init__.py @@ -12,7 +12,6 @@ from esphome.const import ( CONF_PROTOCOL, CONF_RAW, CONF_SYNC, - CONF_TRIGGER_ID, ) DEPENDENCIES = ["uart"] @@ -26,14 +25,6 @@ RFBridgeComponent = rf_bridge_ns.class_( RFBridgeData = rf_bridge_ns.struct("RFBridgeData") RFBridgeAdvancedData = rf_bridge_ns.struct("RFBridgeAdvancedData") -RFBridgeReceivedCodeTrigger = rf_bridge_ns.class_( - "RFBridgeReceivedCodeTrigger", automation.Trigger.template(RFBridgeData) -) -RFBridgeReceivedAdvancedCodeTrigger = rf_bridge_ns.class_( - "RFBridgeReceivedAdvancedCodeTrigger", - automation.Trigger.template(RFBridgeAdvancedData), -) - RFBridgeSendCodeAction = rf_bridge_ns.class_( "RFBridgeSendCodeAction", automation.Action ) @@ -65,19 +56,9 @@ CONFIG_SCHEMA = cv.All( cv.Schema( { cv.GenerateID(): cv.declare_id(RFBridgeComponent), - cv.Optional(CONF_ON_CODE_RECEIVED): automation.validate_automation( - { - cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id( - RFBridgeReceivedCodeTrigger - ), - } - ), + cv.Optional(CONF_ON_CODE_RECEIVED): automation.validate_automation({}), cv.Optional(CONF_ON_ADVANCED_CODE_RECEIVED): automation.validate_automation( - { - cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id( - RFBridgeReceivedAdvancedCodeTrigger - ), - } + {} ), } ) @@ -92,13 +73,15 @@ async def to_code(config): await uart.register_uart_device(var, config) for conf in config.get(CONF_ON_CODE_RECEIVED, []): - trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) - await automation.build_automation(trigger, [(RFBridgeData, "data")], conf) - + await automation.build_callback_automation( + var, "add_on_code_received_callback", [(RFBridgeData, "data")], conf + ) for conf in config.get(CONF_ON_ADVANCED_CODE_RECEIVED, []): - trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) - await automation.build_automation( - trigger, [(RFBridgeAdvancedData, "data")], conf + await automation.build_callback_automation( + var, + "add_on_advanced_code_received_callback", + [(RFBridgeAdvancedData, "data")], + conf, ) @@ -202,9 +185,9 @@ RFBRIDGE_SEND_ADVANCED_CODE_SCHEMA = cv.Schema( async def rf_bridge_send_advanced_code_to_code(config, action_id, template_args, args): paren = await cg.get_variable(config[CONF_ID]) var = cg.new_Pvariable(action_id, template_args, paren) - template_ = await cg.templatable(config[CONF_LENGTH], args, cg.uint16) + template_ = await cg.templatable(config[CONF_LENGTH], args, cg.uint8) cg.add(var.set_length(template_)) - template_ = await cg.templatable(config[CONF_PROTOCOL], args, cg.uint16) + template_ = await cg.templatable(config[CONF_PROTOCOL], args, cg.uint8) cg.add(var.set_protocol(template_)) template_ = await cg.templatable(config[CONF_CODE], args, cg.std_string) cg.add(var.set_code(template_)) diff --git a/esphome/components/rf_bridge/rf_bridge.h b/esphome/components/rf_bridge/rf_bridge.h index e5780c9ebe..571ac6c385 100644 --- a/esphome/components/rf_bridge/rf_bridge.h +++ b/esphome/components/rf_bridge/rf_bridge.h @@ -77,20 +77,6 @@ class RFBridgeComponent : public uart::UARTDevice, public Component { CallbackManager advanced_data_callback_; }; -class RFBridgeReceivedCodeTrigger : public Trigger { - public: - explicit RFBridgeReceivedCodeTrigger(RFBridgeComponent *parent) { - parent->add_on_code_received_callback([this](RFBridgeData data) { this->trigger(data); }); - } -}; - -class RFBridgeReceivedAdvancedCodeTrigger : public Trigger { - public: - explicit RFBridgeReceivedAdvancedCodeTrigger(RFBridgeComponent *parent) { - parent->add_on_advanced_code_received_callback([this](const RFBridgeAdvancedData &data) { this->trigger(data); }); - } -}; - template class RFBridgeSendCodeAction : public Action { public: RFBridgeSendCodeAction(RFBridgeComponent *parent) : parent_(parent) {} diff --git a/esphome/components/rotary_encoder/rotary_encoder.h b/esphome/components/rotary_encoder/rotary_encoder.h index 4b776fe55e..6f4a4fd83c 100644 --- a/esphome/components/rotary_encoder/rotary_encoder.h +++ b/esphome/components/rotary_encoder/rotary_encoder.h @@ -118,19 +118,5 @@ template class RotaryEncoderSetValueAction : public Action { - public: - explicit RotaryEncoderClockwiseTrigger(RotaryEncoderSensor *parent) { - parent->add_on_clockwise_callback([this]() { this->trigger(); }); - } -}; - -class RotaryEncoderAnticlockwiseTrigger : public Trigger<> { - public: - explicit RotaryEncoderAnticlockwiseTrigger(RotaryEncoderSensor *parent) { - parent->add_on_anticlockwise_callback([this]() { this->trigger(); }); - } -}; - } // namespace rotary_encoder } // namespace esphome diff --git a/esphome/components/rotary_encoder/sensor.py b/esphome/components/rotary_encoder/sensor.py index be315db55d..d88657e715 100644 --- a/esphome/components/rotary_encoder/sensor.py +++ b/esphome/components/rotary_encoder/sensor.py @@ -10,9 +10,9 @@ from esphome.const import ( CONF_PIN_B, CONF_RESOLUTION, CONF_RESTORE_MODE, - CONF_TRIGGER_ID, CONF_VALUE, ICON_ROTATE_RIGHT, + STATE_CLASS_MEASUREMENT, UNIT_STEPS, ) @@ -43,13 +43,6 @@ RotaryEncoderSetValueAction = rotary_encoder_ns.class_( "RotaryEncoderSetValueAction", automation.Action ) -RotaryEncoderClockwiseTrigger = rotary_encoder_ns.class_( - "RotaryEncoderClockwiseTrigger", automation.Trigger -) -RotaryEncoderAnticlockwiseTrigger = rotary_encoder_ns.class_( - "RotaryEncoderAnticlockwiseTrigger", automation.Trigger -) - def validate_min_max_value(config): if CONF_MIN_VALUE in config and CONF_MAX_VALUE in config: @@ -57,7 +50,7 @@ def validate_min_max_value(config): max_val = config[CONF_MAX_VALUE] if min_val >= max_val: raise cv.Invalid( - f"Max value {max_val} must be smaller than min value {min_val}" + f"Max value {max_val} must be greater than min value {min_val}" ) return config @@ -68,6 +61,7 @@ CONFIG_SCHEMA = cv.All( unit_of_measurement=UNIT_STEPS, icon=ICON_ROTATE_RIGHT, accuracy_decimals=0, + state_class=STATE_CLASS_MEASUREMENT, ) .extend( { @@ -81,20 +75,8 @@ CONFIG_SCHEMA = cv.All( cv.Optional(CONF_RESTORE_MODE, default="RESTORE_DEFAULT_ZERO"): cv.enum( RESTORE_MODES, upper=True, space="_" ), - cv.Optional(CONF_ON_CLOCKWISE): automation.validate_automation( - { - cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id( - RotaryEncoderClockwiseTrigger - ), - } - ), - cv.Optional(CONF_ON_ANTICLOCKWISE): automation.validate_automation( - { - cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id( - RotaryEncoderAnticlockwiseTrigger - ), - } - ), + cv.Optional(CONF_ON_CLOCKWISE): automation.validate_automation({}), + cv.Optional(CONF_ON_ANTICLOCKWISE): automation.validate_automation({}), } ) .extend(cv.COMPONENT_SCHEMA), @@ -123,11 +105,13 @@ async def to_code(config): cg.add(var.set_max_value(config[CONF_MAX_VALUE])) for conf in config.get(CONF_ON_CLOCKWISE, []): - trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) - await automation.build_automation(trigger, [], conf) + await automation.build_callback_automation( + var, "add_on_clockwise_callback", [], conf + ) for conf in config.get(CONF_ON_ANTICLOCKWISE, []): - trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) - await automation.build_automation(trigger, [], conf) + await automation.build_callback_automation( + var, "add_on_anticlockwise_callback", [], conf + ) @automation.register_action( diff --git a/esphome/components/rpi_dpi_rgb/display.py b/esphome/components/rpi_dpi_rgb/display.py index e92eee7c0c..ee462686e4 100644 --- a/esphome/components/rpi_dpi_rgb/display.py +++ b/esphome/components/rpi_dpi_rgb/display.py @@ -102,7 +102,7 @@ CONFIG_SCHEMA = cv.All( } ), ), - cv.Optional(CONF_COLOR_ORDER): cv.one_of( + cv.Optional(CONF_COLOR_ORDER, default="BGR"): cv.one_of( *COLOR_ORDERS.keys(), upper=True ), cv.Optional(CONF_INVERT_COLORS, default=False): cv.boolean, diff --git a/esphome/components/rtttl/__init__.py b/esphome/components/rtttl/__init__.py index 3566734200..638e950ba6 100644 --- a/esphome/components/rtttl/__init__.py +++ b/esphome/components/rtttl/__init__.py @@ -5,14 +5,7 @@ import esphome.codegen as cg from esphome.components.output import FloatOutput from esphome.components.speaker import Speaker import esphome.config_validation as cv -from esphome.const import ( - CONF_GAIN, - CONF_ID, - CONF_OUTPUT, - CONF_PLATFORM, - CONF_SPEAKER, - CONF_TRIGGER_ID, -) +from esphome.const import CONF_GAIN, CONF_ID, CONF_OUTPUT, CONF_PLATFORM, CONF_SPEAKER import esphome.final_validate as fv _LOGGER = logging.getLogger(__name__) @@ -26,9 +19,6 @@ rtttl_ns = cg.esphome_ns.namespace("rtttl") Rtttl = rtttl_ns.class_("Rtttl", cg.Component) PlayAction = rtttl_ns.class_("PlayAction", automation.Action) StopAction = rtttl_ns.class_("StopAction", automation.Action) -FinishedPlaybackTrigger = rtttl_ns.class_( - "FinishedPlaybackTrigger", automation.Trigger.template() -) IsPlayingCondition = rtttl_ns.class_("IsPlayingCondition", automation.Condition) MULTI_CONF = True @@ -40,13 +30,7 @@ CONFIG_SCHEMA = cv.All( cv.Optional(CONF_OUTPUT): cv.use_id(FloatOutput), cv.Optional(CONF_SPEAKER): cv.use_id(Speaker), cv.Optional(CONF_GAIN, default="0.6"): cv.percentage, - cv.Optional(CONF_ON_FINISHED_PLAYBACK): automation.validate_automation( - { - cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id( - FinishedPlaybackTrigger - ), - } - ), + cv.Optional(CONF_ON_FINISHED_PLAYBACK): automation.validate_automation({}), } ).extend(cv.COMPONENT_SCHEMA), cv.has_exactly_one_key(CONF_OUTPUT, CONF_SPEAKER), @@ -103,8 +87,9 @@ async def to_code(config): cg.add(var.set_gain(config[CONF_GAIN])) 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( diff --git a/esphome/components/rtttl/rtttl.h b/esphome/components/rtttl/rtttl.h index bff43d2edd..98ed9ba1bf 100644 --- a/esphome/components/rtttl/rtttl.h +++ b/esphome/components/rtttl/rtttl.h @@ -131,11 +131,4 @@ template class IsPlayingCondition : public Condition, pub bool check(const Ts &...x) override { return this->parent_->is_playing(); } }; -class FinishedPlaybackTrigger : public Trigger<> { - public: - explicit FinishedPlaybackTrigger(Rtttl *parent) { - parent->add_on_finished_playback_callback([this]() { this->trigger(); }); - } -}; - } // namespace esphome::rtttl diff --git a/esphome/components/runtime_image/bmp_decoder.cpp b/esphome/components/runtime_image/bmp_decoder.cpp index 174f924b28..6a1bd61d86 100644 --- a/esphome/components/runtime_image/bmp_decoder.cpp +++ b/esphome/components/runtime_image/bmp_decoder.cpp @@ -6,6 +6,8 @@ #include "esphome/core/helpers.h" #include "esphome/core/log.h" +#include + namespace esphome::runtime_image { static const char *const TAG = "image_decoder.bmp"; @@ -107,7 +109,7 @@ int HOT BmpDecoder::decode(uint8_t *buffer, size_t size) { } if (this->compression_method_ != 0) { - ESP_LOGE(TAG, "Unsupported compression method: %d", this->compression_method_); + ESP_LOGE(TAG, "Unsupported compression method: %" PRIu32, this->compression_method_); return DECODE_ERROR_UNSUPPORTED_FORMAT; } diff --git a/esphome/components/runtime_image/runtime_image.cpp b/esphome/components/runtime_image/runtime_image.cpp index 2ebe67c3a5..fa42b53496 100644 --- a/esphome/components/runtime_image/runtime_image.cpp +++ b/esphome/components/runtime_image/runtime_image.cpp @@ -248,6 +248,9 @@ void RuntimeImage::release_buffer_() { this->height_ = 0; this->buffer_width_ = 0; this->buffer_height_ = 0; +#ifdef USE_LVGL + memset(&this->dsc_, 0, sizeof(this->dsc_)); +#endif } } diff --git a/esphome/components/runtime_stats/runtime_stats.cpp b/esphome/components/runtime_stats/runtime_stats.cpp index cb28acc96c..06714b5a44 100644 --- a/esphome/components/runtime_stats/runtime_stats.cpp +++ b/esphome/components/runtime_stats/runtime_stats.cpp @@ -2,6 +2,7 @@ #ifdef USE_RUNTIME_STATS +#include "esphome/core/application.h" #include "esphome/core/component.h" #include @@ -13,20 +14,16 @@ RuntimeStatsCollector::RuntimeStatsCollector() : log_interval_(60000), next_log_ global_runtime_stats = this; } -void RuntimeStatsCollector::record_component_time(Component *component, uint32_t duration_us) { - if (component == nullptr) - return; - - // Record stats using component pointer as key - this->component_stats_[component].record_time(duration_us); -} - void RuntimeStatsCollector::log_stats_() { - // First pass: count active components + auto &components = App.components_; + + // Single pass: collect active components into stack buffer + SmallBufferWithHeapFallback<256, Component *> buffer(components.size()); + Component **sorted = buffer.get(); size_t count = 0; - for (const auto &it : this->component_stats_) { - if (it.second.get_period_count() > 0) { - count++; + for (auto *component : components) { + if (component->runtime_stats_.period_count > 0) { + sorted[count++] = component; } } @@ -39,61 +36,58 @@ void RuntimeStatsCollector::log_stats_() { return; } - // Stack buffer sized to actual active count (up to 256 components), heap fallback for larger - SmallBufferWithHeapFallback<256, Component *> buffer(count); - Component **sorted = buffer.get(); - - // Second pass: fill buffer with active components - size_t idx = 0; - for (const auto &it : this->component_stats_) { - if (it.second.get_period_count() > 0) { - sorted[idx++] = it.first; - } - } - // Sort by period runtime (descending) - std::sort(sorted, sorted + count, [this](Component *a, Component *b) { - return this->component_stats_[a].get_period_time_us() > this->component_stats_[b].get_period_time_us(); - }); + std::sort(sorted, sorted + count, compare_period_time); // Log top components by period runtime for (size_t i = 0; i < count; i++) { - const auto &stats = this->component_stats_[sorted[i]]; + const auto &stats = sorted[i]->runtime_stats_; ESP_LOGI(TAG, " %s: count=%" PRIu32 ", avg=%.3fms, max=%.2fms, total=%.1fms", - LOG_STR_ARG(sorted[i]->get_component_log_str()), stats.get_period_count(), - stats.get_period_avg_time_us() / 1000.0f, stats.get_period_max_time_us() / 1000.0f, - stats.get_period_time_us() / 1000.0f); + LOG_STR_ARG(sorted[i]->get_component_log_str()), stats.period_count, + stats.period_count > 0 ? stats.period_time_us / (float) stats.period_count / 1000.0f : 0.0f, + stats.period_max_time_us / 1000.0f, stats.period_time_us / 1000.0f); } // Log total stats since boot (only for active components - idle ones haven't changed) ESP_LOGI(TAG, " Total stats (since boot): %zu active components", count); // Re-sort by total runtime for all-time stats - std::sort(sorted, sorted + count, [this](Component *a, Component *b) { - return this->component_stats_[a].get_total_time_us() > this->component_stats_[b].get_total_time_us(); - }); + std::sort(sorted, sorted + count, compare_total_time); for (size_t i = 0; i < count; i++) { - const auto &stats = this->component_stats_[sorted[i]]; + const auto &stats = sorted[i]->runtime_stats_; ESP_LOGI(TAG, " %s: count=%" PRIu32 ", avg=%.3fms, max=%.2fms, total=%.1fms", - LOG_STR_ARG(sorted[i]->get_component_log_str()), stats.get_total_count(), - stats.get_total_avg_time_us() / 1000.0f, stats.get_total_max_time_us() / 1000.0f, - stats.get_total_time_us() / 1000.0); + LOG_STR_ARG(sorted[i]->get_component_log_str()), stats.total_count, + stats.total_count > 0 ? stats.total_time_us / (float) stats.total_count / 1000.0f : 0.0f, + stats.total_max_time_us / 1000.0f, stats.total_time_us / 1000.0); } + + // Reset period stats + for (auto *component : components) { + component->runtime_stats_.reset_period(); + } +} + +bool RuntimeStatsCollector::compare_period_time(Component *a, Component *b) { + return a->runtime_stats_.period_time_us > b->runtime_stats_.period_time_us; +} + +bool RuntimeStatsCollector::compare_total_time(Component *a, Component *b) { + return a->runtime_stats_.total_time_us > b->runtime_stats_.total_time_us; } void RuntimeStatsCollector::process_pending_stats(uint32_t current_time) { if ((int32_t) (current_time - this->next_log_time_) >= 0) { this->log_stats_(); - this->reset_stats_(); this->next_log_time_ = current_time + this->log_interval_; } } } // namespace runtime_stats -runtime_stats::RuntimeStatsCollector *global_runtime_stats = - nullptr; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) +runtime_stats::RuntimeStatsCollector + *global_runtime_stats = // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) + nullptr; } // namespace esphome diff --git a/esphome/components/runtime_stats/runtime_stats.h b/esphome/components/runtime_stats/runtime_stats.h index 303d895985..3c2c9f78ad 100644 --- a/esphome/components/runtime_stats/runtime_stats.h +++ b/esphome/components/runtime_stats/runtime_stats.h @@ -4,11 +4,8 @@ #ifdef USE_RUNTIME_STATS -#include #include -#include #include "esphome/core/hal.h" -#include "esphome/core/helpers.h" #include "esphome/core/log.h" namespace esphome { @@ -19,64 +16,6 @@ namespace runtime_stats { static const char *const TAG = "runtime_stats"; -class ComponentRuntimeStats { - public: - ComponentRuntimeStats() - : period_count_(0), - period_time_us_(0), - period_max_time_us_(0), - total_count_(0), - total_time_us_(0), - total_max_time_us_(0) {} - - void record_time(uint32_t duration_us) { - // Update period counters - this->period_count_++; - this->period_time_us_ += duration_us; - if (duration_us > this->period_max_time_us_) - this->period_max_time_us_ = duration_us; - - // Update total counters (uint64_t to avoid overflow — uint32_t would overflow after ~10 hours) - this->total_count_++; - this->total_time_us_ += duration_us; - if (duration_us > this->total_max_time_us_) - this->total_max_time_us_ = duration_us; - } - - void reset_period_stats() { - this->period_count_ = 0; - this->period_time_us_ = 0; - this->period_max_time_us_ = 0; - } - - // Period stats (reset each logging interval) - uint32_t get_period_count() const { return this->period_count_; } - uint32_t get_period_time_us() const { return this->period_time_us_; } - uint32_t get_period_max_time_us() const { return this->period_max_time_us_; } - float get_period_avg_time_us() const { - return this->period_count_ > 0 ? this->period_time_us_ / static_cast(this->period_count_) : 0.0f; - } - - // Total stats (persistent until reboot, uint64_t to avoid overflow) - uint32_t get_total_count() const { return this->total_count_; } - uint64_t get_total_time_us() const { return this->total_time_us_; } - uint32_t get_total_max_time_us() const { return this->total_max_time_us_; } - float get_total_avg_time_us() const { - return this->total_count_ > 0 ? this->total_time_us_ / static_cast(this->total_count_) : 0.0f; - } - - protected: - // Period stats (reset each logging interval) - uint32_t period_count_; - uint32_t period_time_us_; - uint32_t period_max_time_us_; - - // Total stats (persistent until reboot) - uint32_t total_count_; - uint64_t total_time_us_; - uint32_t total_max_time_us_; -}; - class RuntimeStatsCollector { public: RuntimeStatsCollector(); @@ -87,23 +26,15 @@ class RuntimeStatsCollector { } uint32_t get_log_interval() const { return this->log_interval_; } - void record_component_time(Component *component, uint32_t duration_us); - // Process any pending stats printing (should be called after component loop) void process_pending_stats(uint32_t current_time); protected: void log_stats_(); + // Static comparators — member functions have friend access, lambdas do not + static bool compare_period_time(Component *a, Component *b); + static bool compare_total_time(Component *a, Component *b); - void reset_stats_() { - for (auto &it : this->component_stats_) { - it.second.reset_period_stats(); - } - } - - // Map from component to its stats - // We use Component* as the key since each component is unique - std::map component_stats_; uint32_t log_interval_; uint32_t next_log_time_{0}; }; diff --git a/esphome/components/safe_mode/__init__.py b/esphome/components/safe_mode/__init__.py index e868985054..da36d21eb7 100644 --- a/esphome/components/safe_mode/__init__.py +++ b/esphome/components/safe_mode/__init__.py @@ -7,7 +7,6 @@ from esphome.const import ( CONF_NUM_ATTEMPTS, CONF_REBOOT_TIMEOUT, CONF_SAFE_MODE, - CONF_TRIGGER_ID, KEY_PAST_SAFE_MODE, ) from esphome.core import CORE, CoroPriority, coroutine_with_priority @@ -20,7 +19,6 @@ CONF_ON_SAFE_MODE = "on_safe_mode" safe_mode_ns = cg.esphome_ns.namespace("safe_mode") SafeModeComponent = safe_mode_ns.class_("SafeModeComponent", cg.Component) -SafeModeTrigger = safe_mode_ns.class_("SafeModeTrigger", automation.Trigger.template()) MarkSuccessfulAction = safe_mode_ns.class_("MarkSuccessfulAction", automation.Action) @@ -43,11 +41,7 @@ CONFIG_SCHEMA = cv.All( cv.Optional( CONF_REBOOT_TIMEOUT, default="5min" ): cv.positive_time_period_milliseconds, - cv.Optional(CONF_ON_SAFE_MODE): automation.validate_automation( - { - cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(SafeModeTrigger), - } - ), + cv.Optional(CONF_ON_SAFE_MODE): automation.validate_automation({}), } ).extend(cv.COMPONENT_SCHEMA), _remove_id_if_disabled, @@ -80,8 +74,9 @@ async def to_code(config): if on_safe_mode_config := config.get(CONF_ON_SAFE_MODE): cg.add_define("USE_SAFE_MODE_CALLBACK") for conf in on_safe_mode_config: - trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) - await automation.build_automation(trigger, [], conf) + await automation.build_callback_automation( + var, "add_on_safe_mode_callback", [], conf + ) condition = var.should_enter_safe_mode( config[CONF_NUM_ATTEMPTS], diff --git a/esphome/components/safe_mode/automation.h b/esphome/components/safe_mode/automation.h index dee02c64a0..79b53c0881 100644 --- a/esphome/components/safe_mode/automation.h +++ b/esphome/components/safe_mode/automation.h @@ -1,19 +1,9 @@ #pragma once -#include "esphome/core/defines.h" #include "esphome/core/automation.h" #include "safe_mode.h" namespace esphome::safe_mode { -#ifdef USE_SAFE_MODE_CALLBACK -class SafeModeTrigger final : public Trigger<> { - public: - explicit SafeModeTrigger(SafeModeComponent *parent) { - parent->add_on_safe_mode_callback([this]() { trigger(); }); - } -}; -#endif // USE_SAFE_MODE_CALLBACK - template class MarkSuccessfulAction : public Action, public Parented { public: void play(const Ts &...x) override { this->parent_->mark_successful(); } diff --git a/esphome/components/sdm_meter/sensor.py b/esphome/components/sdm_meter/sensor.py index affbc0409e..8006d0b4ba 100644 --- a/esphome/components/sdm_meter/sensor.py +++ b/esphome/components/sdm_meter/sensor.py @@ -19,10 +19,13 @@ from esphome.const import ( CONF_REACTIVE_POWER, CONF_TOTAL_POWER, CONF_VOLTAGE, + DEVICE_CLASS_APPARENT_POWER, DEVICE_CLASS_CURRENT, DEVICE_CLASS_ENERGY, + DEVICE_CLASS_FREQUENCY, DEVICE_CLASS_POWER, DEVICE_CLASS_POWER_FACTOR, + DEVICE_CLASS_REACTIVE_POWER, DEVICE_CLASS_VOLTAGE, ICON_CURRENT_AC, ICON_FLASH, @@ -67,11 +70,13 @@ PHASE_SENSORS = { CONF_APPARENT_POWER: sensor.sensor_schema( unit_of_measurement=UNIT_VOLT_AMPS, accuracy_decimals=2, + device_class=DEVICE_CLASS_APPARENT_POWER, state_class=STATE_CLASS_MEASUREMENT, ), CONF_REACTIVE_POWER: sensor.sensor_schema( unit_of_measurement=UNIT_VOLT_AMPS_REACTIVE, accuracy_decimals=2, + device_class=DEVICE_CLASS_REACTIVE_POWER, state_class=STATE_CLASS_MEASUREMENT, ), CONF_POWER_FACTOR: sensor.sensor_schema( @@ -80,7 +85,10 @@ PHASE_SENSORS = { state_class=STATE_CLASS_MEASUREMENT, ), CONF_PHASE_ANGLE: sensor.sensor_schema( - unit_of_measurement=UNIT_DEGREES, icon=ICON_FLASH, accuracy_decimals=3 + unit_of_measurement=UNIT_DEGREES, + icon=ICON_FLASH, + accuracy_decimals=3, + state_class=STATE_CLASS_MEASUREMENT, ), } @@ -99,6 +107,7 @@ CONFIG_SCHEMA = ( unit_of_measurement=UNIT_HERTZ, icon=ICON_CURRENT_AC, accuracy_decimals=3, + device_class=DEVICE_CLASS_FREQUENCY, state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional(CONF_TOTAL_POWER): sensor.sensor_schema( diff --git a/esphome/components/seeed_mr24hpc1/sensor.py b/esphome/components/seeed_mr24hpc1/sensor.py index 7b20941aa4..ca15fd5be6 100644 --- a/esphome/components/seeed_mr24hpc1/sensor.py +++ b/esphome/components/seeed_mr24hpc1/sensor.py @@ -5,6 +5,7 @@ from esphome.const import ( DEVICE_CLASS_DISTANCE, DEVICE_CLASS_ENERGY, DEVICE_CLASS_SPEED, + STATE_CLASS_MEASUREMENT, UNIT_METER, ) @@ -26,6 +27,7 @@ CONFIG_SCHEMA = cv.Schema( unit_of_measurement=UNIT_METER, accuracy_decimals=2, # Specify the number of decimal places icon="mdi:signal-distance-variant", + state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional(CONF_MOVEMENT_SIGNS): sensor.sensor_schema( icon="mdi:human-greeting-variant", @@ -34,6 +36,7 @@ CONFIG_SCHEMA = cv.Schema( unit_of_measurement=UNIT_METER, accuracy_decimals=2, icon="mdi:signal-distance-variant", + state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional(CONF_CUSTOM_SPATIAL_STATIC_VALUE): sensor.sensor_schema( device_class=DEVICE_CLASS_ENERGY, @@ -48,6 +51,7 @@ CONFIG_SCHEMA = cv.Schema( device_class=DEVICE_CLASS_SPEED, accuracy_decimals=2, icon="mdi:run-fast", + state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional(CONF_CUSTOM_MODE_NUM): sensor.sensor_schema( icon="mdi:counter", diff --git a/esphome/components/selec_meter/sensor.py b/esphome/components/selec_meter/sensor.py index 069b61af5a..1a53eb5c37 100644 --- a/esphome/components/selec_meter/sensor.py +++ b/esphome/components/selec_meter/sensor.py @@ -14,10 +14,13 @@ from esphome.const import ( CONF_POWER_FACTOR, CONF_REACTIVE_POWER, CONF_VOLTAGE, + DEVICE_CLASS_APPARENT_POWER, DEVICE_CLASS_CURRENT, DEVICE_CLASS_ENERGY, + DEVICE_CLASS_FREQUENCY, DEVICE_CLASS_POWER, DEVICE_CLASS_POWER_FACTOR, + DEVICE_CLASS_REACTIVE_POWER, DEVICE_CLASS_VOLTAGE, ICON_CURRENT_AC, STATE_CLASS_MEASUREMENT, @@ -97,11 +100,13 @@ SENSORS = { CONF_REACTIVE_POWER: sensor.sensor_schema( unit_of_measurement=UNIT_VOLT_AMPS_REACTIVE, accuracy_decimals=3, + device_class=DEVICE_CLASS_REACTIVE_POWER, state_class=STATE_CLASS_MEASUREMENT, ), CONF_APPARENT_POWER: sensor.sensor_schema( unit_of_measurement=UNIT_VOLT_AMPS, accuracy_decimals=3, + device_class=DEVICE_CLASS_APPARENT_POWER, state_class=STATE_CLASS_MEASUREMENT, ), CONF_VOLTAGE: sensor.sensor_schema( @@ -125,6 +130,7 @@ SENSORS = { unit_of_measurement=UNIT_HERTZ, icon=ICON_CURRENT_AC, accuracy_decimals=2, + device_class=DEVICE_CLASS_FREQUENCY, state_class=STATE_CLASS_MEASUREMENT, ), CONF_MAXIMUM_DEMAND_ACTIVE_POWER: sensor.sensor_schema( @@ -136,11 +142,13 @@ SENSORS = { CONF_MAXIMUM_DEMAND_REACTIVE_POWER: sensor.sensor_schema( unit_of_measurement=UNIT_VOLT_AMPS_REACTIVE, accuracy_decimals=3, + device_class=DEVICE_CLASS_REACTIVE_POWER, state_class=STATE_CLASS_MEASUREMENT, ), CONF_MAXIMUM_DEMAND_APPARENT_POWER: sensor.sensor_schema( unit_of_measurement=UNIT_VOLT_AMPS, accuracy_decimals=3, + device_class=DEVICE_CLASS_APPARENT_POWER, state_class=STATE_CLASS_MEASUREMENT, ), } diff --git a/esphome/components/select/select_call.cpp b/esphome/components/select/select_call.cpp index 0e14371d00..9d2fb725f3 100644 --- a/esphome/components/select/select_call.cpp +++ b/esphome/components/select/select_call.cpp @@ -116,7 +116,7 @@ void SelectCall::perform() { auto idx = target_index.value(); // All operations use indices, call control() by index to avoid string conversion - ESP_LOGD(TAG, "'%s' - Set selected option to: %s", name, parent->option_at(idx)); + ESP_LOGV(TAG, "'%s' - Set selected option to: %s", name, parent->option_at(idx)); parent->control(idx); } diff --git a/esphome/components/sen5x/sensor.py b/esphome/components/sen5x/sensor.py index 9fe51121f1..ce35cf5bf1 100644 --- a/esphome/components/sen5x/sensor.py +++ b/esphome/components/sen5x/sensor.py @@ -25,7 +25,6 @@ from esphome.const import ( CONF_TEMPERATURE_COMPENSATION, CONF_TIME_CONSTANT, CONF_VOC, - CONF_VOC_BASELINE, DEVICE_CLASS_AQI, DEVICE_CLASS_HUMIDITY, DEVICE_CLASS_PM1, @@ -165,7 +164,6 @@ CONFIG_SCHEMA = ( gain_factor=230, ), cv.Optional(CONF_STORE_BASELINE, default=True): cv.boolean, - cv.Optional(CONF_VOC_BASELINE): cv.hex_uint16_t, cv.Optional(CONF_TEMPERATURE): sensor.sensor_schema( unit_of_measurement=UNIT_CELSIUS, icon=ICON_THERMOMETER, diff --git a/esphome/components/sensor/__init__.py b/esphome/components/sensor/__init__.py index 19d03a0afc..275c4542fb 100644 --- a/esphome/components/sensor/__init__.py +++ b/esphome/components/sensor/__init__.py @@ -106,6 +106,7 @@ from esphome.const import ( ENTITY_CATEGORY_CONFIG, ) from esphome.core import CORE, CoroPriority, coroutine_with_priority +from esphome.core.config import UNIT_OF_MEASUREMENT_MAX_LENGTH from esphome.core.entity_helpers import ( entity_duplicate_validator, setup_device_class, @@ -113,6 +114,7 @@ from esphome.core.entity_helpers import ( setup_unit_of_measurement, ) from esphome.cpp_generator import MockObj, MockObjClass +from esphome.schema_extractors import SCHEMA_EXTRACT, schema_extractor from esphome.util import Registry CODEOWNERS = ["@esphome/core"] @@ -229,7 +231,10 @@ _SENSOR_ENTITY_CATEGORIES = { } +@schema_extractor("enum") def sensor_entity_category(value): + if value == SCHEMA_EXTRACT: + return _SENSOR_ENTITY_CATEGORIES return cv.enum(_SENSOR_ENTITY_CATEGORIES, lower=True)(value) @@ -286,7 +291,11 @@ ClampFilter = sensor_ns.class_("ClampFilter", Filter) RoundFilter = sensor_ns.class_("RoundFilter", Filter) RoundMultipleFilter = sensor_ns.class_("RoundMultipleFilter", Filter) -validate_unit_of_measurement = cv.string_strict +validate_unit_of_measurement = cv.All( + cv.string_strict, + # Keep in sync with max_data_length in api.proto + cv.Length(max=UNIT_OF_MEASUREMENT_MAX_LENGTH), +) validate_accuracy_decimals = cv.int_ validate_icon = cv.icon validate_device_class = cv.one_of(*DEVICE_CLASSES, lower=True, space="_") @@ -381,7 +390,7 @@ async def filter_out_filter_to_code(config, filter_id): if not isinstance(config, list): config = [config] template_ = [await cg.templatable(x, [], float) for x in config] - return cg.new_Pvariable(filter_id, template_) + return cg.new_Pvariable(filter_id, cg.TemplateArguments(len(template_)), template_) QUANTILE_SCHEMA = cv.All( @@ -620,7 +629,7 @@ async def delta_filter_to_code(config, filter_id): @FILTER_REGISTRY.register("or", OrFilter, validate_filters) async def or_filter_to_code(config, filter_id): filters = await build_filters(config) - return cg.new_Pvariable(filter_id, filters) + return cg.new_Pvariable(filter_id, cg.TemplateArguments(len(filters)), filters) @FILTER_REGISTRY.register( @@ -650,7 +659,9 @@ async def throttle_with_priority_filter_to_code(config, filter_id): if not isinstance(config[CONF_VALUE], list): config[CONF_VALUE] = [config[CONF_VALUE]] template_ = [await cg.templatable(x, [], float) for x in config[CONF_VALUE]] - return cg.new_Pvariable(filter_id, config[CONF_TIMEOUT], template_) + return cg.new_Pvariable( + filter_id, cg.TemplateArguments(len(template_)), config[CONF_TIMEOUT], template_ + ) HEARTBEAT_SCHEMA = cv.Schema( @@ -768,7 +779,9 @@ async def calibrate_linear_filter_to_code(config, filter_id): linear_functions = [[k, b, float("NaN")]] elif config[CONF_METHOD] == "exact": linear_functions = map_linear(x, y) - return cg.new_Pvariable(filter_id, linear_functions) + return cg.new_Pvariable( + filter_id, cg.TemplateArguments(len(linear_functions)), linear_functions + ) CONF_DEGREE = "degree" @@ -806,7 +819,7 @@ async def calibrate_polynomial_filter_to_code(config, filter_id): # Column vector b = [[v] for v in y] res = [v[0] for v in _lstsq(a, b)] - return cg.new_Pvariable(filter_id, res) + return cg.new_Pvariable(filter_id, cg.TemplateArguments(len(res)), res) def validate_clamp(config): diff --git a/esphome/components/sensor/filter.cpp b/esphome/components/sensor/filter.cpp index d995ee4111..6a90a5af66 100644 --- a/esphome/components/sensor/filter.cpp +++ b/esphome/components/sensor/filter.cpp @@ -222,16 +222,14 @@ MultiplyFilter::MultiplyFilter(TemplatableValue multiplier) : multiplier_ optional MultiplyFilter::new_value(float value) { return value * this->multiplier_.value(); } -// ValueListFilter (base class) -ValueListFilter::ValueListFilter(std::initializer_list> values) : values_(values) {} - -bool ValueListFilter::value_matches_any_(float sensor_value) { - int8_t accuracy = this->parent_->get_accuracy_decimals(); +// ValueListFilter helper (non-template, shared by all ValueListFilter instantiations) +bool value_list_matches_any(Sensor *parent, float sensor_value, const TemplatableValue *values, size_t count) { + int8_t accuracy = parent->get_accuracy_decimals(); float accuracy_mult = pow10_int(accuracy); float rounded_sensor = roundf(accuracy_mult * sensor_value); - for (auto &filter_value : this->values_) { - float fv = filter_value.value(); + for (size_t i = 0; i < count; i++) { + float fv = values[i].value(); // Handle NaN comparison if (std::isnan(fv)) { @@ -248,16 +246,6 @@ bool ValueListFilter::value_matches_any_(float sensor_value) { return false; } -// FilterOutValueFilter -FilterOutValueFilter::FilterOutValueFilter(std::initializer_list> values_to_filter_out) - : ValueListFilter(values_to_filter_out) {} - -optional FilterOutValueFilter::new_value(float value) { - if (this->value_matches_any_(value)) - return {}; // Filter out - return value; // Pass through -} - // ThrottleFilter ThrottleFilter::ThrottleFilter(uint32_t min_time_between_inputs) : min_time_between_inputs_(min_time_between_inputs) {} optional ThrottleFilter::new_value(float value) { @@ -269,17 +257,13 @@ optional ThrottleFilter::new_value(float value) { return {}; } -// ThrottleWithPriorityFilter -ThrottleWithPriorityFilter::ThrottleWithPriorityFilter( - uint32_t min_time_between_inputs, std::initializer_list> prioritized_values) - : ValueListFilter(prioritized_values), min_time_between_inputs_(min_time_between_inputs) {} - -optional ThrottleWithPriorityFilter::new_value(float value) { +// ThrottleWithPriorityFilter helper (non-template, keeps App access in .cpp) +optional throttle_with_priority_new_value(Sensor *parent, float value, const TemplatableValue *values, + size_t count, uint32_t &last_input, uint32_t min_time_between_inputs) { const uint32_t now = App.get_loop_component_start_time(); - // Allow value through if: no previous input, time expired, or is prioritized - if (this->last_input_ == 0 || now - this->last_input_ >= min_time_between_inputs_ || - this->value_matches_any_(value)) { - this->last_input_ = now; + if (last_input == 0 || now - last_input >= min_time_between_inputs || + value_list_matches_any(parent, value, values, count)) { + last_input = now; return value; } return {}; @@ -311,32 +295,20 @@ optional DeltaFilter::new_value(float value) { return {}; } -// OrFilter -OrFilter::OrFilter(std::initializer_list filters) : filters_(filters), phi_(this) {} -OrFilter::PhiNode::PhiNode(OrFilter *or_parent) : or_parent_(or_parent) {} - -optional OrFilter::PhiNode::new_value(float value) { - if (!this->or_parent_->has_value_) { - this->or_parent_->output(value); - this->or_parent_->has_value_ = true; +// OrFilter helpers +void or_filter_initialize(Filter **filters, size_t count, Sensor *parent, Filter *phi) { + for (size_t i = 0; i < count; i++) { + filters[i]->initialize(parent, phi); } - - return {}; + phi->initialize(parent, nullptr); } -optional OrFilter::new_value(float value) { - this->has_value_ = false; - for (auto *filter : this->filters_) - filter->input(value); +optional or_filter_new_value(Filter **filters, size_t count, float value, bool &has_value) { + has_value = false; + for (size_t i = 0; i < count; i++) + filters[i]->input(value); return {}; } -void OrFilter::initialize(Sensor *parent, Filter *next) { - Filter::initialize(parent, next); - for (auto *filter : this->filters_) { - filter->initialize(parent, &this->phi_); - } - this->phi_.initialize(parent, nullptr); -} // TimeoutFilterBase - shared loop logic void TimeoutFilterBase::loop() { @@ -413,25 +385,19 @@ void HeartbeatFilter::setup() { float HeartbeatFilter::get_setup_priority() const { return setup_priority::HARDWARE; } -CalibrateLinearFilter::CalibrateLinearFilter(std::initializer_list> linear_functions) - : linear_functions_(linear_functions) {} - -optional CalibrateLinearFilter::new_value(float value) { - for (const auto &f : this->linear_functions_) { - if (!std::isfinite(f[2]) || value < f[2]) - return (value * f[0]) + f[1]; +optional calibrate_linear_compute(const std::array *functions, size_t count, float value) { + for (size_t i = 0; i < count; i++) { + if (!std::isfinite(functions[i][2]) || value < functions[i][2]) + return (value * functions[i][0]) + functions[i][1]; } return NAN; } -CalibratePolynomialFilter::CalibratePolynomialFilter(std::initializer_list coefficients) - : coefficients_(coefficients) {} - -optional CalibratePolynomialFilter::new_value(float value) { +optional calibrate_polynomial_compute(const float *coefficients, size_t count, float value) { float res = 0.0f; float x = 1.0f; - for (const auto &coefficient : this->coefficients_) { - res += x * coefficient; + for (size_t i = 0; i < count; i++) { + res += x * coefficients[i]; x *= value; } return res; diff --git a/esphome/components/sensor/filter.h b/esphome/components/sensor/filter.h index 6a76bd373e..cb4abd154a 100644 --- a/esphome/components/sensor/filter.h +++ b/esphome/components/sensor/filter.h @@ -3,6 +3,7 @@ #include "esphome/core/defines.h" #ifdef USE_SENSOR_FILTER +#include #include #include #include "esphome/core/automation.h" @@ -328,28 +329,42 @@ class MultiplyFilter : public Filter { TemplatableValue multiplier_; }; -/** Base class for filters that compare sensor values against a list of configured values. +/// Non-template helper for value matching (implementation in filter.cpp) +bool value_list_matches_any(Sensor *parent, float sensor_value, const TemplatableValue *values, size_t count); + +/** Base class for filters that compare sensor values against a fixed list of configured values. * - * This base class provides common functionality for filters that need to check if a sensor - * value matches any value in a configured list, with proper handling of NaN values and - * accuracy-based rounding for comparisons. + * Templated on N (the number of values) so the list is stored inline in a std::array, + * avoiding heap allocation and the overhead of FixedVector. + * + * @tparam N Number of values in the filter list, set by code generation to match + * the exact number of values configured in YAML. */ -class ValueListFilter : public Filter { +template class ValueListFilter : public Filter { protected: - explicit ValueListFilter(std::initializer_list> values); + explicit ValueListFilter(std::initializer_list> values) { + init_array_from(this->values_, values); + } /// Check if sensor value matches any configured value (with accuracy rounding) - bool value_matches_any_(float sensor_value); + bool value_matches_any_(float sensor_value) { + return value_list_matches_any(this->parent_, sensor_value, this->values_.data(), N); + } - FixedVector> values_; + std::array, N> values_{}; }; /// A simple filter that only forwards the filter chain if it doesn't receive `value_to_filter_out`. -class FilterOutValueFilter : public ValueListFilter { +template class FilterOutValueFilter : public ValueListFilter { public: - explicit FilterOutValueFilter(std::initializer_list> values_to_filter_out); + explicit FilterOutValueFilter(std::initializer_list> values_to_filter_out) + : ValueListFilter(values_to_filter_out) {} - optional new_value(float value) override; + optional new_value(float value) override { + if (this->value_matches_any_(value)) + return {}; // Filter out + return value; // Pass through + } }; class ThrottleFilter : public Filter { @@ -363,13 +378,21 @@ class ThrottleFilter : public Filter { uint32_t min_time_between_inputs_; }; +/// Non-template helper for ThrottleWithPriorityFilter (implementation in filter.cpp) +optional throttle_with_priority_new_value(Sensor *parent, float value, const TemplatableValue *values, + size_t count, uint32_t &last_input, uint32_t min_time_between_inputs); + /// Same as 'throttle' but will immediately publish values contained in `value_to_prioritize`. -class ThrottleWithPriorityFilter : public ValueListFilter { +template class ThrottleWithPriorityFilter : public ValueListFilter { public: explicit ThrottleWithPriorityFilter(uint32_t min_time_between_inputs, - std::initializer_list> prioritized_values); + std::initializer_list> prioritized_values) + : ValueListFilter(prioritized_values), min_time_between_inputs_(min_time_between_inputs) {} - optional new_value(float value) override; + optional new_value(float value) override { + return throttle_with_priority_new_value(this->parent_, value, this->values_.data(), N, this->last_input_, + this->min_time_between_inputs_); + } protected: uint32_t last_input_{0}; @@ -466,45 +489,77 @@ class DeltaFilter : public Filter { float last_value_{NAN}; }; -class OrFilter : public Filter { +/// Non-template helpers for OrFilter (implementation in filter.cpp) +void or_filter_initialize(Filter **filters, size_t count, Sensor *parent, Filter *phi); +optional or_filter_new_value(Filter **filters, size_t count, float value, bool &has_value); + +/// N is set by code generation to match the exact number of filters configured in YAML. +template class OrFilter : public Filter { public: - explicit OrFilter(std::initializer_list filters); + explicit OrFilter(std::initializer_list filters) { init_array_from(this->filters_, filters); } - void initialize(Sensor *parent, Filter *next) override; + void initialize(Sensor *parent, Filter *next) override { + Filter::initialize(parent, next); + or_filter_initialize(this->filters_.data(), N, parent, &this->phi_); + } - optional new_value(float value) override; + optional new_value(float value) override { + return or_filter_new_value(this->filters_.data(), N, value, this->has_value_); + } protected: class PhiNode : public Filter { public: - PhiNode(OrFilter *or_parent); - optional new_value(float value) override; + PhiNode(OrFilter *or_parent) : or_parent_(or_parent) {} + optional new_value(float value) override { + if (!this->or_parent_->has_value_) { + this->or_parent_->output(value); + this->or_parent_->has_value_ = true; + } + return {}; + } protected: OrFilter *or_parent_; }; - FixedVector filters_; - PhiNode phi_; + std::array filters_{}; + PhiNode phi_{this}; bool has_value_{false}; }; -class CalibrateLinearFilter : public Filter { +/// Non-template helper for linear calibration (implementation in filter.cpp) +optional calibrate_linear_compute(const std::array *functions, size_t count, float value); + +/// N is set by code generation to match the exact number of calibration segments. +template class CalibrateLinearFilter : public Filter { public: - explicit CalibrateLinearFilter(std::initializer_list> linear_functions); - optional new_value(float value) override; + explicit CalibrateLinearFilter(std::initializer_list> linear_functions) { + init_array_from(this->linear_functions_, linear_functions); + } + optional new_value(float value) override { + return calibrate_linear_compute(this->linear_functions_.data(), N, value); + } protected: - FixedVector> linear_functions_; + std::array, N> linear_functions_{}; }; -class CalibratePolynomialFilter : public Filter { +/// Non-template helper for polynomial calibration (implementation in filter.cpp) +optional calibrate_polynomial_compute(const float *coefficients, size_t count, float value); + +/// N is set by code generation to match the exact number of polynomial coefficients. +template class CalibratePolynomialFilter : public Filter { public: - explicit CalibratePolynomialFilter(std::initializer_list coefficients); - optional new_value(float value) override; + explicit CalibratePolynomialFilter(std::initializer_list coefficients) { + init_array_from(this->coefficients_, coefficients); + } + optional new_value(float value) override { + return calibrate_polynomial_compute(this->coefficients_.data(), N, value); + } protected: - FixedVector coefficients_; + std::array coefficients_{}; }; class ClampFilter : public Filter { diff --git a/esphome/components/serial_proxy/serial_proxy.cpp b/esphome/components/serial_proxy/serial_proxy.cpp index f3c256c62a..04c94e9292 100644 --- a/esphome/components/serial_proxy/serial_proxy.cpp +++ b/esphome/components/serial_proxy/serial_proxy.cpp @@ -3,6 +3,8 @@ #ifdef USE_SERIAL_PROXY #include "esphome/core/log.h" + +#include #include "esphome/core/util.h" #ifdef USE_API @@ -74,7 +76,7 @@ void __attribute__((noinline)) SerialProxy::read_and_send_(size_t available) { void SerialProxy::dump_config() { ESP_LOGCONFIG(TAG, - "Serial Proxy [%u]:\n" + "Serial Proxy [%" PRIu32 "]:\n" " Name: %s\n" " Port Type: %s\n" " RTS Pin: %s\n" @@ -89,7 +91,9 @@ void SerialProxy::dump_config() { void SerialProxy::configure(uint32_t baudrate, bool flow_control, uint8_t parity, uint8_t stop_bits, uint8_t data_size) { - ESP_LOGD(TAG, "Configuring serial proxy [%u]: baud=%u, flow_ctrl=%s, parity=%u, stop=%u, data=%u", + ESP_LOGD(TAG, + "Configuring serial proxy [%" PRIu32 "]: baud=%" PRIu32 ", flow_ctrl=%s, parity=%" PRIu8 ", stop=%" PRIu8 + ", data=%" PRIu8, this->instance_index_, baudrate, YESNO(flow_control), parity, stop_bits, data_size); auto *uart_comp = this->parent_; @@ -148,7 +152,7 @@ void SerialProxy::write_from_client(const uint8_t *data, size_t len) { void SerialProxy::set_modem_pins(uint32_t line_states) { const bool rts = (line_states & SERIAL_PROXY_LINE_STATE_FLAG_RTS) != 0; const bool dtr = (line_states & SERIAL_PROXY_LINE_STATE_FLAG_DTR) != 0; - ESP_LOGV(TAG, "Setting modem pins [%u]: RTS=%s, DTR=%s", this->instance_index_, ONOFF(rts), ONOFF(dtr)); + ESP_LOGV(TAG, "Setting modem pins [%" PRIu32 "]: RTS=%s, DTR=%s", this->instance_index_, ONOFF(rts), ONOFF(dtr)); if (this->rts_pin_ != nullptr) { this->rts_state_ = rts; @@ -161,12 +165,12 @@ void SerialProxy::set_modem_pins(uint32_t line_states) { } uint32_t SerialProxy::get_modem_pins() const { - return (this->rts_state_ ? SERIAL_PROXY_LINE_STATE_FLAG_RTS : 0u) | - (this->dtr_state_ ? SERIAL_PROXY_LINE_STATE_FLAG_DTR : 0u); + return (this->rts_state_ ? static_cast(SERIAL_PROXY_LINE_STATE_FLAG_RTS) : 0u) | + (this->dtr_state_ ? static_cast(SERIAL_PROXY_LINE_STATE_FLAG_DTR) : 0u); } uart::UARTFlushResult SerialProxy::flush_port() { - ESP_LOGV(TAG, "Flushing serial proxy [%u]", this->instance_index_); + ESP_LOGV(TAG, "Flushing serial proxy [%" PRIu32 "]", this->instance_index_); return this->flush(); } @@ -180,19 +184,19 @@ void SerialProxy::serial_proxy_request(api::APIConnection *api_connection, api:: } this->api_connection_ = api_connection; this->enable_loop(); - ESP_LOGV(TAG, "API connection subscribed to serial proxy [%u]", this->instance_index_); + ESP_LOGV(TAG, "API connection subscribed to serial proxy [%" PRIu32 "]", this->instance_index_); break; case api::enums::SERIAL_PROXY_REQUEST_TYPE_UNSUBSCRIBE: if (this->api_connection_ != api_connection) { - ESP_LOGV(TAG, "API connection is not subscribed to serial proxy [%u]", this->instance_index_); + ESP_LOGV(TAG, "API connection is not subscribed to serial proxy [%" PRIu32 "]", this->instance_index_); return; } this->api_connection_ = nullptr; this->disable_loop(); - ESP_LOGV(TAG, "API connection unsubscribed from serial proxy [%u]", this->instance_index_); + ESP_LOGV(TAG, "API connection unsubscribed from serial proxy [%" PRIu32 "]", this->instance_index_); break; default: - ESP_LOGW(TAG, "Unknown serial proxy request type: %u", static_cast(type)); + ESP_LOGW(TAG, "Unknown serial proxy request type: %" PRIu32, static_cast(type)); break; } } diff --git a/esphome/components/sgp4x/sensor.py b/esphome/components/sgp4x/sensor.py index ab78ab59d9..1e58a0f26a 100644 --- a/esphome/components/sgp4x/sensor.py +++ b/esphome/components/sgp4x/sensor.py @@ -15,7 +15,6 @@ from esphome.const import ( CONF_STORE_BASELINE, CONF_TEMPERATURE_SOURCE, CONF_VOC, - CONF_VOC_BASELINE, DEVICE_CLASS_AQI, ICON_RADIATOR, STATE_CLASS_MEASUREMENT, @@ -44,20 +43,27 @@ def validate_sensors(config): return config -GAS_SENSOR = cv.Schema( - { - cv.Optional(CONF_ALGORITHM_TUNING): cv.Schema( - { - cv.Optional(CONF_INDEX_OFFSET, default=100): cv.int_, - cv.Optional(CONF_LEARNING_TIME_OFFSET_HOURS, default=12): cv.int_, - cv.Optional(CONF_LEARNING_TIME_GAIN_HOURS, default=12): cv.int_, - cv.Optional(CONF_GATING_MAX_DURATION_MINUTES, default=720): cv.int_, - cv.Optional(CONF_STD_INITIAL, default=50): cv.int_, - cv.Optional(CONF_GAIN_FACTOR, default=230): cv.int_, - } - ) - } -) +def _gas_sensor_schema(index_offset_default: int): + return cv.Schema( + { + cv.Optional(CONF_ALGORITHM_TUNING): cv.Schema( + { + cv.Optional( + CONF_INDEX_OFFSET, default=index_offset_default + ): cv.int_, + cv.Optional(CONF_LEARNING_TIME_OFFSET_HOURS, default=12): cv.int_, + cv.Optional(CONF_LEARNING_TIME_GAIN_HOURS, default=12): cv.int_, + cv.Optional(CONF_GATING_MAX_DURATION_MINUTES, default=720): cv.int_, + cv.Optional(CONF_STD_INITIAL, default=50): cv.int_, + cv.Optional(CONF_GAIN_FACTOR, default=230): cv.int_, + } + ) + } + ) + + +VOC_SENSOR = _gas_sensor_schema(100) +NOX_SENSOR = _gas_sensor_schema(1) CONFIG_SCHEMA = cv.All( cv.Schema( @@ -68,15 +74,14 @@ CONFIG_SCHEMA = cv.All( accuracy_decimals=0, device_class=DEVICE_CLASS_AQI, state_class=STATE_CLASS_MEASUREMENT, - ).extend(GAS_SENSOR), + ).extend(VOC_SENSOR), cv.Optional(CONF_NOX): sensor.sensor_schema( icon=ICON_RADIATOR, accuracy_decimals=0, device_class=DEVICE_CLASS_AQI, state_class=STATE_CLASS_MEASUREMENT, - ).extend(GAS_SENSOR), + ).extend(NOX_SENSOR), cv.Optional(CONF_STORE_BASELINE, default=True): cv.boolean, - cv.Optional(CONF_VOC_BASELINE): cv.hex_uint16_t, cv.Optional(CONF_COMPENSATION): cv.Schema( { cv.Required(CONF_HUMIDITY_SOURCE): cv.use_id(sensor.Sensor), @@ -105,9 +110,6 @@ async def to_code(config): cg.add(var.set_store_baseline(config[CONF_STORE_BASELINE])) - if CONF_VOC_BASELINE in config: - cg.add(var.set_voc_baseline(CONF_VOC_BASELINE)) - if CONF_VOC in config: sens = await sensor.new_sensor(config[CONF_VOC]) cg.add(var.set_voc_sensor(sens)) diff --git a/esphome/components/shelly_dimmer/light.py b/esphome/components/shelly_dimmer/light.py index c96bc380d7..1688f9d6a6 100644 --- a/esphome/components/shelly_dimmer/light.py +++ b/esphome/components/shelly_dimmer/light.py @@ -22,6 +22,7 @@ from esphome.const import ( DEVICE_CLASS_CURRENT, DEVICE_CLASS_POWER, DEVICE_CLASS_VOLTAGE, + STATE_CLASS_MEASUREMENT, UNIT_AMPERE, UNIT_VOLT, UNIT_WATT, @@ -66,7 +67,7 @@ KNOWN_FIRMWARE = { def parse_firmware_version(value): - match = re.match(r"(\d+).(\d+)", value) + match = re.fullmatch(r"(\d+)\.(\d+)", value) if match is None: raise ValueError(f"Not a valid version number {value}") major = int(match[1]) @@ -128,7 +129,7 @@ def validate_firmware(value): def validate_sha256(value): value = cv.string(value) - if not value.isalnum() or not len(value) == 64: + if not re.fullmatch(r"[0-9a-fA-F]{64}", value): raise ValueError(f"Not a valid SHA256 hex string: {value}") return value @@ -163,16 +164,19 @@ CONFIG_SCHEMA = ( unit_of_measurement=UNIT_WATT, accuracy_decimals=1, device_class=DEVICE_CLASS_POWER, + state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional(CONF_VOLTAGE): sensor.sensor_schema( unit_of_measurement=UNIT_VOLT, accuracy_decimals=1, device_class=DEVICE_CLASS_VOLTAGE, + state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional(CONF_CURRENT): sensor.sensor_schema( unit_of_measurement=UNIT_AMPERE, device_class=DEVICE_CLASS_CURRENT, accuracy_decimals=2, + state_class=STATE_CLASS_MEASUREMENT, ), # Change the default gamma_correct setting. cv.Optional(CONF_GAMMA_CORRECT, default=1.0): cv.positive_float, diff --git a/esphome/components/shtcx/shtcx.cpp b/esphome/components/shtcx/shtcx.cpp index ec12a5babd..9ec0a2cdb7 100644 --- a/esphome/components/shtcx/shtcx.cpp +++ b/esphome/components/shtcx/shtcx.cpp @@ -2,16 +2,15 @@ #include "esphome/core/log.h" #include "esphome/core/hal.h" -namespace esphome { -namespace shtcx { +namespace esphome::shtcx { static const char *const TAG = "shtcx"; -static const uint16_t SHTCX_COMMAND_SLEEP = 0xB098; -static const uint16_t SHTCX_COMMAND_WAKEUP = 0x3517; -static const uint16_t SHTCX_COMMAND_READ_ID_REGISTER = 0xEFC8; -static const uint16_t SHTCX_COMMAND_SOFT_RESET = 0x805D; -static const uint16_t SHTCX_COMMAND_POLLING_H = 0x7866; +static constexpr uint16_t SHTCX_COMMAND_SLEEP = 0xB098; +static constexpr uint16_t SHTCX_COMMAND_WAKEUP = 0x3517; +static constexpr uint16_t SHTCX_COMMAND_READ_ID_REGISTER = 0xEFC8; +static constexpr uint16_t SHTCX_COMMAND_SOFT_RESET = 0x805D; +static constexpr uint16_t SHTCX_COMMAND_POLLING_H = 0x7866; static const LogString *shtcx_type_to_string(SHTCXType type) { switch (type) { @@ -91,8 +90,6 @@ void SHTCXComponent::update() { } else { temperature = 175.0f * float(raw_data[0]) / 65536.0f - 45.0f; humidity = 100.0f * float(raw_data[1]) / 65536.0f; - - ESP_LOGD(TAG, "Temperature=%.2f°C Humidity=%.2f%%", temperature, humidity); } if (this->temperature_sensor_ != nullptr) this->temperature_sensor_->publish_state(temperature); @@ -117,5 +114,4 @@ void SHTCXComponent::wake_up() { delayMicroseconds(200); } -} // namespace shtcx -} // namespace esphome +} // namespace esphome::shtcx diff --git a/esphome/components/shtcx/shtcx.h b/esphome/components/shtcx/shtcx.h index f9778dce8d..a86b204e2b 100644 --- a/esphome/components/shtcx/shtcx.h +++ b/esphome/components/shtcx/shtcx.h @@ -4,10 +4,13 @@ #include "esphome/components/sensor/sensor.h" #include "esphome/components/sensirion_common/i2c_sensirion.h" -namespace esphome { -namespace shtcx { +namespace esphome::shtcx { -enum SHTCXType { SHTCX_TYPE_SHTC3 = 0, SHTCX_TYPE_SHTC1, SHTCX_TYPE_UNKNOWN }; +enum SHTCXType : uint8_t { + SHTCX_TYPE_SHTC3 = 0, + SHTCX_TYPE_SHTC1, + SHTCX_TYPE_UNKNOWN, +}; /// This class implements support for the SHT3x-DIS family of temperature+humidity i2c sensors. class SHTCXComponent : public PollingComponent, public sensirion_common::SensirionI2CDevice { @@ -23,11 +26,10 @@ class SHTCXComponent : public PollingComponent, public sensirion_common::Sensiri void wake_up(); protected: - SHTCXType type_; - uint16_t sensor_id_; sensor::Sensor *temperature_sensor_{nullptr}; sensor::Sensor *humidity_sensor_{nullptr}; + uint16_t sensor_id_; + SHTCXType type_; }; -} // namespace shtcx -} // namespace esphome +} // namespace esphome::shtcx diff --git a/esphome/components/sim800l/__init__.py b/esphome/components/sim800l/__init__.py index ebb74302a9..91771047e1 100644 --- a/esphome/components/sim800l/__init__.py +++ b/esphome/components/sim800l/__init__.py @@ -2,7 +2,7 @@ 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_ID, CONF_MESSAGE, CONF_TRIGGER_ID +from esphome.const import CONF_ID, CONF_MESSAGE DEPENDENCIES = ["uart"] CODEOWNERS = ["@glmnet"] @@ -11,28 +11,6 @@ MULTI_CONF = True sim800l_ns = cg.esphome_ns.namespace("sim800l") Sim800LComponent = sim800l_ns.class_("Sim800LComponent", cg.Component) -Sim800LReceivedMessageTrigger = sim800l_ns.class_( - "Sim800LReceivedMessageTrigger", - automation.Trigger.template(cg.std_string, cg.std_string), -) -Sim800LIncomingCallTrigger = sim800l_ns.class_( - "Sim800LIncomingCallTrigger", - automation.Trigger.template(cg.std_string), -) -Sim800LCallConnectedTrigger = sim800l_ns.class_( - "Sim800LCallConnectedTrigger", - automation.Trigger.template(), -) -Sim800LCallDisconnectedTrigger = sim800l_ns.class_( - "Sim800LCallDisconnectedTrigger", - automation.Trigger.template(), -) - -Sim800LReceivedUssdTrigger = sim800l_ns.class_( - "Sim800LReceivedUssdTrigger", - automation.Trigger.template(cg.std_string), -) - # Actions Sim800LSendSmsAction = sim800l_ns.class_("Sim800LSendSmsAction", automation.Action) Sim800LSendUssdAction = sim800l_ns.class_("Sim800LSendUssdAction", automation.Action) @@ -55,41 +33,11 @@ CONFIG_SCHEMA = cv.All( cv.Schema( { cv.GenerateID(): cv.declare_id(Sim800LComponent), - cv.Optional(CONF_ON_SMS_RECEIVED): automation.validate_automation( - { - cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id( - Sim800LReceivedMessageTrigger - ), - } - ), - cv.Optional(CONF_ON_INCOMING_CALL): automation.validate_automation( - { - cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id( - Sim800LIncomingCallTrigger - ), - } - ), - cv.Optional(CONF_ON_CALL_CONNECTED): automation.validate_automation( - { - cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id( - Sim800LCallConnectedTrigger - ), - } - ), - cv.Optional(CONF_ON_CALL_DISCONNECTED): automation.validate_automation( - { - cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id( - Sim800LCallDisconnectedTrigger - ), - } - ), - cv.Optional(CONF_ON_USSD_RECEIVED): automation.validate_automation( - { - cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id( - Sim800LReceivedUssdTrigger - ), - } - ), + cv.Optional(CONF_ON_SMS_RECEIVED): automation.validate_automation({}), + cv.Optional(CONF_ON_INCOMING_CALL): automation.validate_automation({}), + cv.Optional(CONF_ON_CALL_CONNECTED): automation.validate_automation({}), + cv.Optional(CONF_ON_CALL_DISCONNECTED): automation.validate_automation({}), + cv.Optional(CONF_ON_USSD_RECEIVED): automation.validate_automation({}), } ) .extend(cv.polling_component_schema("5s")) @@ -106,23 +54,28 @@ async def to_code(config): await uart.register_uart_device(var, config) for conf in config.get(CONF_ON_SMS_RECEIVED, []): - trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) - await automation.build_automation( - trigger, [(cg.std_string, "message"), (cg.std_string, "sender")], conf + await automation.build_callback_automation( + var, + "add_on_sms_received_callback", + [(cg.std_string, "message"), (cg.std_string, "sender")], + conf, ) for conf in config.get(CONF_ON_INCOMING_CALL, []): - trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) - await automation.build_automation(trigger, [(cg.std_string, "caller_id")], conf) + await automation.build_callback_automation( + var, "add_on_incoming_call_callback", [(cg.std_string, "caller_id")], conf + ) for conf in config.get(CONF_ON_CALL_CONNECTED, []): - trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) - await automation.build_automation(trigger, [], conf) + await automation.build_callback_automation( + var, "add_on_call_connected_callback", [], conf + ) for conf in config.get(CONF_ON_CALL_DISCONNECTED, []): - trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) - await automation.build_automation(trigger, [], conf) - + await automation.build_callback_automation( + var, "add_on_call_disconnected_callback", [], conf + ) for conf in config.get(CONF_ON_USSD_RECEIVED, []): - trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) - await automation.build_automation(trigger, [(cg.std_string, "ussd")], conf) + await automation.build_callback_automation( + var, "add_on_ussd_received_callback", [(cg.std_string, "ussd")], conf + ) SIM800L_SEND_SMS_SCHEMA = cv.Schema( diff --git a/esphome/components/sim800l/sim800l.h b/esphome/components/sim800l/sim800l.h index d79279ea72..d0da123039 100644 --- a/esphome/components/sim800l/sim800l.h +++ b/esphome/components/sim800l/sim800l.h @@ -121,41 +121,6 @@ class Sim800LComponent : public uart::UARTDevice, public PollingComponent { CallbackManager ussd_received_callback_; }; -class Sim800LReceivedMessageTrigger : public Trigger { - public: - explicit Sim800LReceivedMessageTrigger(Sim800LComponent *parent) { - parent->add_on_sms_received_callback( - [this](const std::string &message, const std::string &sender) { this->trigger(message, sender); }); - } -}; - -class Sim800LIncomingCallTrigger : public Trigger { - public: - explicit Sim800LIncomingCallTrigger(Sim800LComponent *parent) { - parent->add_on_incoming_call_callback([this](const std::string &caller_id) { this->trigger(caller_id); }); - } -}; - -class Sim800LCallConnectedTrigger : public Trigger<> { - public: - explicit Sim800LCallConnectedTrigger(Sim800LComponent *parent) { - parent->add_on_call_connected_callback([this]() { this->trigger(); }); - } -}; - -class Sim800LCallDisconnectedTrigger : public Trigger<> { - public: - explicit Sim800LCallDisconnectedTrigger(Sim800LComponent *parent) { - parent->add_on_call_disconnected_callback([this]() { this->trigger(); }); - } -}; -class Sim800LReceivedUssdTrigger : public Trigger { - public: - explicit Sim800LReceivedUssdTrigger(Sim800LComponent *parent) { - parent->add_on_ussd_received_callback([this](const std::string &ussd) { this->trigger(ussd); }); - } -}; - template class Sim800LSendSmsAction : public Action { public: Sim800LSendSmsAction(Sim800LComponent *parent) : parent_(parent) {} diff --git a/esphome/components/sm2135/sm2135.cpp b/esphome/components/sm2135/sm2135.cpp index 1293c3f321..c3d10e70c2 100644 --- a/esphome/components/sm2135/sm2135.cpp +++ b/esphome/components/sm2135/sm2135.cpp @@ -25,7 +25,7 @@ void SM2135::setup() { this->data_pin_->pin_mode(gpio::FLAG_OUTPUT); this->clock_pin_->setup(); this->clock_pin_->digital_write(false); - this->data_pin_->pin_mode(gpio::FLAG_OUTPUT); + this->clock_pin_->pin_mode(gpio::FLAG_OUTPUT); this->data_pin_->pin_mode(gpio::FLAG_PULLUP); this->clock_pin_->pin_mode(gpio::FLAG_PULLUP); diff --git a/esphome/components/sml/__init__.py b/esphome/components/sml/__init__.py index eaeddce390..1b7f9da4fb 100644 --- a/esphome/components/sml/__init__.py +++ b/esphome/components/sml/__init__.py @@ -4,7 +4,7 @@ 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_ID, CONF_ON_DATA, CONF_TRIGGER_ID +from esphome.const import CONF_ID, CONF_ON_DATA CODEOWNERS = ["@alengwenus"] @@ -18,36 +18,27 @@ CONF_SML_ID = "sml_id" CONF_OBIS_CODE = "obis_code" CONF_SERVER_ID = "server_id" -sml_ns = cg.esphome_ns.namespace("sml") -DataTrigger = sml_ns.class_( - "DataTrigger", - automation.Trigger.template( - cg.std_vector.template(cg.uint8).operator("ref"), cg.bool_ - ), +CONFIG_SCHEMA = ( + cv.Schema( + { + cv.GenerateID(): cv.declare_id(Sml), + cv.Optional(CONF_ON_DATA): automation.validate_automation({}), + } + ) + .extend(uart.UART_DEVICE_SCHEMA) + .extend(cv.COMPONENT_SCHEMA) ) -CONFIG_SCHEMA = cv.Schema( - { - cv.GenerateID(): cv.declare_id(Sml), - cv.Optional(CONF_ON_DATA): automation.validate_automation( - { - cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(DataTrigger), - } - ), - } -).extend(uart.UART_DEVICE_SCHEMA) - - 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) for conf in config.get(CONF_ON_DATA, []): - trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) - await automation.build_automation( - trigger, + await automation.build_callback_automation( + var, + "add_on_data_callback", [ ( cg.std_vector.template(cg.uint8).operator("ref").operator("const"), diff --git a/esphome/components/sml/automation.h b/esphome/components/sml/automation.h deleted file mode 100644 index d51063065d..0000000000 --- a/esphome/components/sml/automation.h +++ /dev/null @@ -1,19 +0,0 @@ -#pragma once - -#include "esphome/core/automation.h" -#include "sml.h" - -#include - -namespace esphome { -namespace sml { - -class DataTrigger : public Trigger &, bool> { - public: - explicit DataTrigger(Sml *sml) { - sml->add_on_data_callback([this](const std::vector &data, bool valid) { this->trigger(data, valid); }); - } -}; - -} // namespace sml -} // namespace esphome diff --git a/esphome/components/sml/sensor/__init__.py b/esphome/components/sml/sensor/__init__.py index 164a31f8a7..e6d7180f17 100644 --- a/esphome/components/sml/sensor/__init__.py +++ b/esphome/components/sml/sensor/__init__.py @@ -10,13 +10,17 @@ AUTO_LOAD = ["sml"] SmlSensor = sml_ns.class_("SmlSensor", sensor.Sensor, cg.Component) -CONFIG_SCHEMA = sensor.sensor_schema().extend( - { - cv.GenerateID(): cv.declare_id(SmlSensor), - cv.GenerateID(CONF_SML_ID): cv.use_id(Sml), - cv.Required(CONF_OBIS_CODE): obis_code, - cv.Optional(CONF_SERVER_ID, default=""): cv.string, - } +CONFIG_SCHEMA = ( + sensor.sensor_schema() + .extend( + { + cv.GenerateID(): cv.declare_id(SmlSensor), + cv.GenerateID(CONF_SML_ID): cv.use_id(Sml), + cv.Required(CONF_OBIS_CODE): obis_code, + cv.Optional(CONF_SERVER_ID, default=""): cv.string, + } + ) + .extend(cv.COMPONENT_SCHEMA) ) diff --git a/esphome/components/sml/text_sensor/__init__.py b/esphome/components/sml/text_sensor/__init__.py index 9c9da26c3a..5a5ab658c4 100644 --- a/esphome/components/sml/text_sensor/__init__.py +++ b/esphome/components/sml/text_sensor/__init__.py @@ -19,13 +19,17 @@ SML_TYPES = { SmlTextSensor = sml_ns.class_("SmlTextSensor", text_sensor.TextSensor, cg.Component) -CONFIG_SCHEMA = text_sensor.text_sensor_schema(SmlTextSensor).extend( - { - cv.GenerateID(CONF_SML_ID): cv.use_id(Sml), - cv.Required(CONF_OBIS_CODE): obis_code, - cv.Optional(CONF_SERVER_ID, default=""): cv.string, - cv.Optional(CONF_FORMAT, default=""): cv.enum(SML_TYPES, lower=True), - } +CONFIG_SCHEMA = ( + text_sensor.text_sensor_schema(SmlTextSensor) + .extend( + { + cv.GenerateID(CONF_SML_ID): cv.use_id(Sml), + cv.Required(CONF_OBIS_CODE): obis_code, + cv.Optional(CONF_SERVER_ID, default=""): cv.string, + cv.Optional(CONF_FORMAT, default=""): cv.enum(SML_TYPES, lower=True), + } + ) + .extend(cv.COMPONENT_SCHEMA) ) diff --git a/esphome/components/socket/__init__.py b/esphome/components/socket/__init__.py index 08cf3ea33c..abbbb0f056 100644 --- a/esphome/components/socket/__init__.py +++ b/esphome/components/socket/__init__.py @@ -31,9 +31,6 @@ MIN_UDP_SOCKETS = 6 # Minimum listening sockets — at least api + ota baseline. MIN_TCP_LISTEN_SOCKETS = 2 -# Wake loop threadsafe support tracking -KEY_WAKE_LOOP_THREADSAFE_REQUIRED = "wake_loop_threadsafe_required" - class SocketType(StrEnum): TCP = "tcp" @@ -123,37 +120,22 @@ def get_socket_counts() -> SocketCounts: def require_wake_loop_threadsafe() -> None: - """Mark that wake_loop_threadsafe support is required by a component. + """Deprecated: wake loop support is now always available on all platforms. - Call this from components that need to wake the main event loop from background threads. - This enables the shared UDP loopback socket mechanism (~208 bytes RAM). - The socket is shared across all components that use this feature. - - This call is a no-op if networking is not enabled in the configuration. - - IMPORTANT: This is for background thread context only, NOT ISR context. - Socket operations are not safe to call from ISR handlers. - - On ESP32, FreeRTOS task notifications are used instead (no socket needed). - - Example: - from esphome.components import socket - - async def to_code(config): - socket.require_wake_loop_threadsafe() + This function adds backward-compatible defines so external components + that check #ifdef USE_WAKE_LOOP_THREADSAFE / USE_SOCKET_SELECT_SUPPORT + continue to compile. Remove before 2026.12.0. """ - - # Only set up once (idempotent - multiple components can call this) - if CORE.has_networking and not CORE.data.get( - KEY_WAKE_LOOP_THREADSAFE_REQUIRED, False - ): - CORE.data[KEY_WAKE_LOOP_THREADSAFE_REQUIRED] = True - cg.add_define("USE_WAKE_LOOP_THREADSAFE") - if not CORE.is_esp32 and not CORE.is_libretiny: - # Only platforms without fast select need a UDP socket for wake - # notifications. ESP32 and LibreTiny use FreeRTOS task notifications - # instead (no socket needed). - consume_sockets(1, "socket.wake_loop_threadsafe", SocketType.UDP)({}) + # Remove before 2026.12.0 + _LOGGER.warning( + "require_wake_loop_threadsafe() is deprecated and no longer needed. " + "Wake loop support is now always available. Remove this call and any " + "#ifdef USE_SOCKET_SELECT_SUPPORT / USE_WAKE_LOOP_THREADSAFE guards. " + "This will be removed in 2026.12.0." + ) + # Add deprecated defines for backward compat with external component C++ code + cg.add_define("USE_WAKE_LOOP_THREADSAFE") + cg.add_define("USE_SOCKET_SELECT_SUPPORT") CONFIG_SCHEMA = cv.Schema( @@ -184,10 +166,8 @@ async def to_code(config): cg.add_define("USE_SOCKET_IMPL_LWIP_TCP") elif impl == IMPLEMENTATION_LWIP_SOCKETS: cg.add_define("USE_SOCKET_IMPL_LWIP_SOCKETS") - cg.add_define("USE_SOCKET_SELECT_SUPPORT") elif impl == IMPLEMENTATION_BSD_SOCKETS: cg.add_define("USE_SOCKET_IMPL_BSD_SOCKETS") - cg.add_define("USE_SOCKET_SELECT_SUPPORT") # ESP32 and LibreTiny both have LwIP >= 2.1.3 with lwip_socket_dbg_get_socket() # and FreeRTOS task notifications — enable fast select to bypass lwip_select(). # Only when not using lwip_tcp, which does not provide select() support. diff --git a/esphome/components/socket/lwip_raw_tcp_impl.cpp b/esphome/components/socket/lwip_raw_tcp_impl.cpp index 3bcbd88085..86131d3ddb 100644 --- a/esphome/components/socket/lwip_raw_tcp_impl.cpp +++ b/esphome/components/socket/lwip_raw_tcp_impl.cpp @@ -8,6 +8,7 @@ #include #include "esphome/core/helpers.h" +#include "esphome/core/wake.h" #include "esphome/core/log.h" #ifdef USE_ESP8266 @@ -19,102 +20,6 @@ namespace esphome::socket { -#ifdef USE_ESP8266 -// Flag to signal socket activity - checked by socket_delay() to exit early -// NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables) -static volatile bool s_socket_woke = false; - -void socket_delay(uint32_t ms) { - // Use esp_delay with a callback that checks if socket data arrived. - // This allows the delay to exit early when socket_wake() is called by - // lwip recv_fn/accept_fn callbacks, reducing socket latency. - // - // When ms is 0, we must use delay(0) because esp_delay(0, callback) - // exits immediately without yielding, which can cause watchdog timeouts - // when the main loop runs in high-frequency mode (e.g., during light effects). - if (ms == 0) { - delay(0); - return; - } - s_socket_woke = false; - esp_delay(ms, []() { return !s_socket_woke; }); -} - -void IRAM_ATTR socket_wake() { - s_socket_woke = true; - esp_schedule(); -} -#elif defined(USE_RP2040) -// RP2040 (non-FreeRTOS) socket wake using hardware WFE/SEV instructions. -// -// Same pattern as ESP8266's esp_delay()/esp_schedule(): set a one-shot timer, -// then sleep with __wfe(). Wake on either: -// - Timer alarm fires → callback calls __sev() → __wfe() returns → timeout -// - Socket data arrives → LWIP callback calls socket_wake() → __sev() → __wfe() returns → early wake -// -// CYW43 WiFi chip communicates via SPI interrupts on core 0. When data arrives, -// the GPIO interrupt fires → async_context pendsv processes CYW43/LWIP → recv/accept -// callbacks call socket_wake() → __sev() wakes the main loop from __wfe() sleep. -// NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables) -static volatile bool s_socket_woke = false; -// NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables) -static volatile bool s_delay_expired = false; - -static int64_t alarm_callback(alarm_id_t id, void *user_data) { - (void) id; - (void) user_data; - s_delay_expired = true; - // Wake the main loop from __wfe() sleep — timeout expired. - __sev(); - // Return 0 = don't reschedule (one-shot) - return 0; -} - -void socket_delay(uint32_t ms) { - if (ms == 0) { - yield(); - return; - } - // If a wake was already signalled, consume it and return immediately - // instead of going to sleep. This avoids losing a wake that arrived - // between loop iterations. - if (s_socket_woke) { - s_socket_woke = false; - return; - } - // Don't clear s_socket_woke here — if an IRQ fires between the check above - // and the while loop below, the while condition sees it immediately. Clearing - // here would lose that wake and sleep until the timer fires. - s_delay_expired = false; - // Set a one-shot timer to wake us after the timeout. - // add_alarm_in_ms returns >0 on success, 0 if time already passed, <0 on error. - alarm_id_t alarm = add_alarm_in_ms(ms, alarm_callback, nullptr, true); - if (alarm <= 0) { - delay(ms); - return; - } - // Sleep until woken by either the timer alarm or socket_wake(). - // __wfe() may return spuriously (stale event register, other interrupts), - // so we loop checking both flags. - while (!s_socket_woke && !s_delay_expired) { - __wfe(); - } - // Cancel timer if we woke early (socket data arrived before timeout) - if (!s_delay_expired) - cancel_alarm(alarm); - s_socket_woke = false; // consume the wake for next call -} - -// No IRAM_ATTR equivalent needed: on RP2040, CYW43 async_context runs LWIP -// callbacks via pendsv (not hard IRQ), so they execute from flash safely. -void socket_wake() { - s_socket_woke = true; - // Wake the main loop from __wfe() sleep. __sev() is a global event that - // wakes any core sleeping in __wfe(). This is ISR-safe. - __sev(); -} -#endif - // ---- LWIP thread safety ---- // // On RP2040 (Pico W), arduino-pico sets PICO_CYW43_ARCH_THREADSAFE_BACKGROUND=1. @@ -543,10 +448,8 @@ err_t LWIPRawImpl::recv_fn(struct pbuf *pb, err_t err) { } else { pbuf_cat(this->rx_buf_, pb); } -#if (defined(USE_ESP8266) || defined(USE_RP2040)) // Wake the main loop immediately so it can process the received data. - socket_wake(); -#endif + esphome::wake_loop_any_context(); return ERR_OK; } @@ -555,15 +458,15 @@ void LWIPRawImpl::wait_for_data_() { // (needs async_context lock). // // Loop until data arrives, connection closes, or the full timeout elapses. - // socket_delay() may return early due to other sockets waking the global - // socket_wake() flag, so we re-enter for the remaining time. + // wakeable_delay() may return early due to any wake source, + // so we re-enter for the remaining time. uint32_t timeout_ms = this->recv_timeout_cs_ * 10; uint32_t start = millis(); while (this->waiting_for_data_()) { uint32_t elapsed = millis() - start; if (elapsed >= timeout_ms) break; - socket_delay(timeout_ms - elapsed); + esphome::internal::wakeable_delay(timeout_ms - elapsed); } } @@ -951,10 +854,8 @@ err_t LWIPRawListenImpl::accept_fn_(struct tcp_pcb *newpcb, err_t err) { tcp_err(newpcb, LWIPRawListenImpl::s_queued_err_fn); tcp_recv(newpcb, LWIPRawListenImpl::s_queued_recv_fn); LWIP_LOG("Accepted connection, queue size: %d", this->accepted_socket_count_); -#if (defined(USE_ESP8266) || defined(USE_RP2040)) // Wake the main loop immediately so it can accept the new connection. - socket_wake(); -#endif + esphome::wake_loop_any_context(); return ERR_OK; } diff --git a/esphome/components/socket/lwip_raw_tcp_impl.h b/esphome/components/socket/lwip_raw_tcp_impl.h index 3c27d71062..e2dcb80d32 100644 --- a/esphome/components/socket/lwip_raw_tcp_impl.h +++ b/esphome/components/socket/lwip_raw_tcp_impl.h @@ -109,7 +109,7 @@ class LWIPRawImpl : public LWIPRawCommon { return -1; } // Raw TCP doesn't use a blocking flag directly. Blocking behavior - // is provided by SO_RCVTIMEO which makes read() wait via socket_delay(). + // is provided by SO_RCVTIMEO which makes read() wait via wakeable_delay(). return 0; } int loop() { return 0; } diff --git a/esphome/components/socket/socket.cpp b/esphome/components/socket/socket.cpp index bfb6ae8e13..bc43b2746e 100644 --- a/esphome/components/socket/socket.cpp +++ b/esphome/components/socket/socket.cpp @@ -8,7 +8,7 @@ namespace esphome::socket { -#if defined(USE_SOCKET_SELECT_SUPPORT) && !defined(USE_LWIP_FAST_SELECT) +#ifdef USE_HOST // Shared ready() implementation for fd-based socket implementations (BSD and LWIP sockets). // Checks if the Application's select() loop has marked this fd as ready. bool socket_ready_fd(int fd, bool loop_monitored) { return !loop_monitored || App.is_socket_ready_(fd); } diff --git a/esphome/components/socket/socket.h b/esphome/components/socket/socket.h index 226a669e31..9ea71321e0 100644 --- a/esphome/components/socket/socket.h +++ b/esphome/components/socket/socket.h @@ -45,7 +45,7 @@ using ListenSocket = LWIPRawListenImpl; inline bool socket_ready(struct lwip_sock *cached_sock, bool loop_monitored) { return !loop_monitored || (cached_sock != nullptr && esphome_lwip_socket_has_data(cached_sock)); } -#elif defined(USE_SOCKET_SELECT_SUPPORT) +#elif defined(USE_HOST) /// Shared ready() helper for fd-based socket implementations. /// Checks if the Application's select() loop has marked this fd as ready. bool socket_ready_fd(int fd, bool loop_monitored); @@ -120,19 +120,5 @@ socklen_t set_sockaddr_any(struct sockaddr *addr, socklen_t addrlen, uint16_t po /// Format sockaddr into caller-provided buffer, returns length written (excluding null) size_t format_sockaddr_to(const struct sockaddr *addr_ptr, socklen_t len, std::span buf); -#if (defined(USE_ESP8266) || defined(USE_RP2040)) && defined(USE_SOCKET_IMPL_LWIP_TCP) -/// Delay that can be woken early by socket activity. -/// On ESP8266, uses esp_delay() with a callback that checks socket activity. -/// On RP2040, uses __wfe() (Wait For Event) to truly sleep until an interrupt -/// (for example, CYW43 GPIO or a timer alarm) fires and wakes the CPU. -void socket_delay(uint32_t ms); // NOLINT(readability-redundant-declaration) - -/// Signal socket/IO activity and wake the main loop early. -/// On ESP8266: sets flag + esp_schedule(). -/// On RP2040: sets flag + __sev() (Send Event) to wake from __wfe(). -/// ISR-safe on both platforms. -void socket_wake(); // NOLINT(readability-redundant-declaration) -#endif - } // namespace esphome::socket #endif diff --git a/esphome/components/spa06_base/spa06_base.cpp b/esphome/components/spa06_base/spa06_base.cpp index 36268aa9a2..b0490628cb 100644 --- a/esphome/components/spa06_base/spa06_base.cpp +++ b/esphome/components/spa06_base/spa06_base.cpp @@ -1,5 +1,7 @@ #include "spa06_base.h" +#include + #include "esphome/core/helpers.h" namespace esphome::spa06_base { @@ -195,7 +197,7 @@ bool SPA06Component::read_coefficients_() { ESP_LOGV(TAG, "Coefficients:\n" " c0: %i, c1: %i,\n" - " c00: %i, c10: %i, c20: %i, c30: %i, c40: %i,\n" + " c00: %" PRIi32 ", c10: %" PRIi32 ", c20: %i, c30: %i, c40: %i,\n" " c01: %i, c11: %i, c21: %i, c31: %i", this->c0_, this->c1_, this->c00_, this->c10_, this->c20_, this->c30_, this->c40_, this->c01_, this->c11_, this->c21_, this->c31_); diff --git a/esphome/components/spi/spi.cpp b/esphome/components/spi/spi.cpp index 36344a6d38..20359135ba 100644 --- a/esphome/components/spi/spi.cpp +++ b/esphome/components/spi/spi.cpp @@ -68,7 +68,7 @@ void SPIComponent::dump_config() { LOG_PIN(" SDI Pin: ", this->sdi_pin_); LOG_PIN(" SDO Pin: ", this->sdo_pin_); for (size_t i = 0; i != this->data_pins_.size(); i++) { - ESP_LOGCONFIG(TAG, " Data pin %u: GPIO%d", i, this->data_pins_[i]); + ESP_LOGCONFIG(TAG, " Data pin %zu: GPIO%d", i, this->data_pins_[i]); } if (this->spi_bus_->is_hw()) { ESP_LOGCONFIG(TAG, " Using HW SPI: %s", this->interface_name_); @@ -118,4 +118,12 @@ uint16_t SPIDelegateBitBash::transfer_(uint16_t data, size_t num_bits) { return out_data; } +#if !defined(USE_ESP32) && !defined(USE_ARDUINO) +// Stub for unsupported platforms (host, Zephyr, etc.) - hardware SPI is unavailable +SPIBus *SPIComponent::get_bus(SPIInterface interface, GPIOPin *clk, GPIOPin *sdo, GPIOPin *sdi, + const std::vector &data_pins) { + return nullptr; +} +#endif + } // namespace esphome::spi diff --git a/esphome/components/spi/spi.h b/esphome/components/spi/spi.h index 84c8bca267..dc538f4c41 100644 --- a/esphome/components/spi/spi.h +++ b/esphome/components/spi/spi.h @@ -23,9 +23,9 @@ using SPIInterface = SPIClassRP2040 *; using SPIInterface = SPIClass *; #endif -#elif defined(CLANG_TIDY) +#elif defined(USE_HOST) || defined(CLANG_TIDY) -using SPIInterface = void *; // Stub for platforms without SPI (e.g., Zephyr) +using SPIInterface = void *; // Stub for platforms without SPI (e.g., host, Zephyr) #endif // USE_ESP32 / USE_ARDUINO diff --git a/esphome/components/sprinkler/__init__.py b/esphome/components/sprinkler/__init__.py index 6e2ff4ee2e..fb2beb5b16 100644 --- a/esphome/components/sprinkler/__init__.py +++ b/esphome/components/sprinkler/__init__.py @@ -272,7 +272,7 @@ SPRINKLER_VALVE_SCHEMA = cv.Schema( ), cv.Optional( CONF_UNIT_OF_MEASUREMENT, default=UNIT_SECOND - ): cv.one_of(UNIT_MINUTE, UNIT_SECOND, lower="True"), + ): cv.one_of(UNIT_MINUTE, UNIT_SECOND, lower=True), } ) .extend(cv.COMPONENT_SCHEMA), @@ -427,7 +427,7 @@ CONFIG_SCHEMA = cv.All( async def sprinkler_set_divider_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_DIVIDER], args, cg.float_) + template_ = await cg.templatable(config[CONF_DIVIDER], args, cg.uint32) cg.add(var.set_divider(template_)) return var @@ -471,7 +471,7 @@ async def sprinkler_set_queued_valve_to_code(config, action_id, template_arg, ar async def sprinkler_set_repeat_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_REPEAT], args, cg.float_) + template_ = await cg.templatable(config[CONF_REPEAT], args, cg.uint32) cg.add(var.set_repeat(template_)) return var diff --git a/esphome/components/sps30/sps30.cpp b/esphome/components/sps30/sps30.cpp index dbb44743d2..e4fc4ffd31 100644 --- a/esphome/components/sps30/sps30.cpp +++ b/esphome/components/sps30/sps30.cpp @@ -2,6 +2,8 @@ #include "esphome/core/log.h" #include "sps30.h" +#include + namespace esphome { namespace sps30 { @@ -105,7 +107,7 @@ void SPS30Component::dump_config() { " Firmware version v%0d.%0d", this->serial_number_, this->raw_firmware_version_ >> 8, this->raw_firmware_version_ & 0xFF); if (this->idle_interval_.has_value()) { - ESP_LOGCONFIG(TAG, " Idle interval: %us", this->idle_interval_.value() / 1000); + ESP_LOGCONFIG(TAG, " Idle interval: %" PRIu32 "s", this->idle_interval_.value() / 1000); } LOG_SENSOR(" ", "PM1.0 Weight Concentration", this->pm_1_0_sensor_); LOG_SENSOR(" ", "PM2.5 Weight Concentration", this->pm_2_5_sensor_); @@ -142,8 +144,8 @@ void SPS30Component::update() { // If its not time to take an action, do nothing. const uint32_t update_start_ms = millis(); if (this->next_state_ != NONE && (int32_t) (this->next_state_ms_ - update_start_ms) > 0) { - ESP_LOGD(TAG, "Sensor waiting for %ums before transitioning to state %d.", (this->next_state_ms_ - update_start_ms), - this->next_state_); + ESP_LOGD(TAG, "Sensor waiting for %" PRIu32 "ms before transitioning to state %d.", + (this->next_state_ms_ - update_start_ms), this->next_state_); return; } diff --git a/esphome/components/st7701s/display.py b/esphome/components/st7701s/display.py index a8b12dfa28..7f6492812f 100644 --- a/esphome/components/st7701s/display.py +++ b/esphome/components/st7701s/display.py @@ -138,7 +138,7 @@ CONFIG_SCHEMA = cv.All( cv.Optional(CONF_INIT_SEQUENCE, default=1): cv.ensure_list( map_sequence ), - cv.Optional(CONF_COLOR_ORDER): cv.one_of( + cv.Optional(CONF_COLOR_ORDER, default="BGR"): cv.one_of( *COLOR_ORDERS.keys(), upper=True ), cv.Optional(CONF_PCLK_FREQUENCY, default="16MHz"): cv.All( diff --git a/esphome/components/st7735/__init__.py b/esphome/components/st7735/__init__.py index ba854bb0ae..62dfa6f413 100644 --- a/esphome/components/st7735/__init__.py +++ b/esphome/components/st7735/__init__.py @@ -1,3 +1,8 @@ import esphome.codegen as cg st7735_ns = cg.esphome_ns.namespace("st7735") + +DEPRECATED_COMPONENT = """ +The 'st7735' component is deprecated and no new models will be added to it. +New model PRs should target the newer and more performant 'mipi_spi' component. +""" diff --git a/esphome/components/st7735/display.py b/esphome/components/st7735/display.py index 9dc69f27ff..766370c21f 100644 --- a/esphome/components/st7735/display.py +++ b/esphome/components/st7735/display.py @@ -1,3 +1,5 @@ +import logging + from esphome import pins import esphome.codegen as cg from esphome.components import display, spi @@ -15,6 +17,7 @@ from esphome.const import ( from . import st7735_ns CODEOWNERS = ["@SenexCrenshaw"] +LOGGER = logging.getLogger(__name__) DEPENDENCIES = ["spi"] @@ -87,6 +90,9 @@ async def setup_st7735(var, config): async def to_code(config): + LOGGER.warning( + "The 'st7735' component is deprecated, it is recommended to use 'mipi_spi' instead." + ) var = cg.new_Pvariable( config[CONF_ID], config[CONF_MODEL], diff --git a/esphome/components/st7789v/display.py b/esphome/components/st7789v/display.py index c9f4199616..85414237cf 100644 --- a/esphome/components/st7789v/display.py +++ b/esphome/components/st7789v/display.py @@ -127,7 +127,7 @@ def validate_st7789v(config): if model_data[REQUIRE_PS] and CONF_POWER_SUPPLY not in config: raise cv.Invalid( - f'{CONF_POWER_SUPPLY} must be specified when {CONF_MODEL} is {config[CONF_MODEL]}"' + f"{CONF_POWER_SUPPLY} must be specified when {CONF_MODEL} is {config[CONF_MODEL]}" ) if ( diff --git a/esphome/components/status_led/light/status_led_light.h b/esphome/components/status_led/light/status_led_light.h index a5c98d90d4..3a745e0017 100644 --- a/esphome/components/status_led/light/status_led_light.h +++ b/esphome/components/status_led/light/status_led_light.h @@ -30,9 +30,6 @@ class StatusLEDLightOutput : public light::LightOutput, public Component { void dump_config() override; float get_setup_priority() const override { return setup_priority::HARDWARE; } -#ifdef USE_LOOP_PRIORITY - float get_loop_priority() const override { return 50.0f; } -#endif protected: GPIOPin *pin_{nullptr}; diff --git a/esphome/components/status_led/status_led.cpp b/esphome/components/status_led/status_led.cpp index 93a8d4b38e..a792110eeb 100644 --- a/esphome/components/status_led/status_led.cpp +++ b/esphome/components/status_led/status_led.cpp @@ -28,9 +28,6 @@ void StatusLED::loop() { } } float StatusLED::get_setup_priority() const { return setup_priority::HARDWARE; } -#ifdef USE_LOOP_PRIORITY -float StatusLED::get_loop_priority() const { return 50.0f; } -#endif } // namespace status_led } // namespace esphome diff --git a/esphome/components/status_led/status_led.h b/esphome/components/status_led/status_led.h index f262eb260c..a4b5db93d7 100644 --- a/esphome/components/status_led/status_led.h +++ b/esphome/components/status_led/status_led.h @@ -14,9 +14,6 @@ class StatusLED : public Component { void dump_config() override; void loop() override; float get_setup_priority() const override; -#ifdef USE_LOOP_PRIORITY - float get_loop_priority() const override; -#endif protected: GPIOPin *pin_; diff --git a/esphome/components/stepper/__init__.py b/esphome/components/stepper/__init__.py index 27d4fc276d..8acacc3b49 100644 --- a/esphome/components/stepper/__init__.py +++ b/esphome/components/stepper/__init__.py @@ -46,7 +46,7 @@ def validate_acceleration(value): def validate_speed(value): value = cv.string(value) - for suffix in ("steps/s", "steps/s"): + for suffix in ("steps/s",): value = value.removesuffix(suffix) if value == "inf": diff --git a/esphome/components/sun/sensor/__init__.py b/esphome/components/sun/sensor/__init__.py index a356d9cca8..a1ced8ff5b 100644 --- a/esphome/components/sun/sensor/__init__.py +++ b/esphome/components/sun/sensor/__init__.py @@ -1,7 +1,12 @@ import esphome.codegen as cg from esphome.components import sensor import esphome.config_validation as cv -from esphome.const import CONF_TYPE, ICON_WEATHER_SUNSET, UNIT_DEGREES +from esphome.const import ( + CONF_TYPE, + ICON_WEATHER_SUNSET, + STATE_CLASS_MEASUREMENT, + UNIT_DEGREES, +) from .. import CONF_SUN_ID, Sun, sun_ns @@ -20,6 +25,7 @@ CONFIG_SCHEMA = ( unit_of_measurement=UNIT_DEGREES, icon=ICON_WEATHER_SUNSET, accuracy_decimals=1, + state_class=STATE_CLASS_MEASUREMENT, ) .extend( { diff --git a/esphome/components/sun_gtil2/__init__.py b/esphome/components/sun_gtil2/__init__.py index d073c16e4e..c7082794db 100644 --- a/esphome/components/sun_gtil2/__init__.py +++ b/esphome/components/sun_gtil2/__init__.py @@ -13,11 +13,15 @@ sun_gtil2_ns = cg.esphome_ns.namespace("sun_gtil2") SunGTIL2Component = sun_gtil2_ns.class_("SunGTIL2", cg.Component, uart.UARTDevice) -CONFIG_SCHEMA = cv.Schema( - { - cv.GenerateID(): cv.declare_id(SunGTIL2Component), - } -).extend(uart.UART_DEVICE_SCHEMA) +CONFIG_SCHEMA = ( + cv.Schema( + { + cv.GenerateID(): cv.declare_id(SunGTIL2Component), + } + ) + .extend(uart.UART_DEVICE_SCHEMA) + .extend(cv.COMPONENT_SCHEMA) +) async def to_code(config): diff --git a/esphome/components/sun_gtil2/sensor.py b/esphome/components/sun_gtil2/sensor.py index d8c59bf1de..55c8195391 100644 --- a/esphome/components/sun_gtil2/sensor.py +++ b/esphome/components/sun_gtil2/sensor.py @@ -8,6 +8,7 @@ from esphome.const import ( DEVICE_CLASS_VOLTAGE, ICON_FLASH, ICON_THERMOMETER, + STATE_CLASS_MEASUREMENT, UNIT_CELSIUS, UNIT_VOLT, UNIT_WATT, @@ -30,36 +31,42 @@ CONFIG_SCHEMA = cv.All( icon=ICON_FLASH, accuracy_decimals=1, device_class=DEVICE_CLASS_VOLTAGE, + state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional(CONF_DC_VOLTAGE): sensor.sensor_schema( unit_of_measurement=UNIT_VOLT, icon=ICON_FLASH, accuracy_decimals=1, device_class=DEVICE_CLASS_VOLTAGE, + state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional(CONF_AC_POWER): sensor.sensor_schema( unit_of_measurement=UNIT_WATT, icon=ICON_FLASH, accuracy_decimals=1, device_class=DEVICE_CLASS_POWER, + state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional(CONF_DC_POWER): sensor.sensor_schema( unit_of_measurement=UNIT_WATT, icon=ICON_FLASH, accuracy_decimals=1, device_class=DEVICE_CLASS_POWER, + state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional(CONF_LIMITER_POWER): sensor.sensor_schema( unit_of_measurement=UNIT_WATT, icon=ICON_FLASH, accuracy_decimals=1, device_class=DEVICE_CLASS_POWER, + state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional(CONF_TEMPERATURE): sensor.sensor_schema( unit_of_measurement=UNIT_CELSIUS, icon=ICON_THERMOMETER, accuracy_decimals=1, device_class=DEVICE_CLASS_TEMPERATURE, + state_class=STATE_CLASS_MEASUREMENT, ), } ).extend(cv.COMPONENT_SCHEMA) diff --git a/esphome/components/switch/switch.cpp b/esphome/components/switch/switch.cpp index 11840db3a3..abc7338a62 100644 --- a/esphome/components/switch/switch.cpp +++ b/esphome/components/switch/switch.cpp @@ -18,15 +18,15 @@ void Switch::control(bool target_state) { } } void Switch::turn_on() { - ESP_LOGD(TAG, "'%s' Turning ON.", this->get_name().c_str()); + ESP_LOGV(TAG, "'%s' Turning ON.", this->get_name().c_str()); this->write_state(!this->inverted_); } void Switch::turn_off() { - ESP_LOGD(TAG, "'%s' Turning OFF.", this->get_name().c_str()); + ESP_LOGV(TAG, "'%s' Turning OFF.", this->get_name().c_str()); this->write_state(this->inverted_); } void Switch::toggle() { - ESP_LOGD(TAG, "'%s' Toggling %s.", this->get_name().c_str(), this->state ? "OFF" : "ON"); + ESP_LOGV(TAG, "'%s' Toggling %s.", this->get_name().c_str(), this->state ? "OFF" : "ON"); this->write_state(this->inverted_ == this->state); } optional Switch::get_initial_state() { diff --git a/esphome/components/sx1509/sx1509.cpp b/esphome/components/sx1509/sx1509.cpp index dfe1277297..1cdae76eaf 100644 --- a/esphome/components/sx1509/sx1509.cpp +++ b/esphome/components/sx1509/sx1509.cpp @@ -309,8 +309,8 @@ void SX1509Component::set_debounce_keypad_(uint8_t time, uint8_t num_rows, uint8 set_debounce_time_(time); for (uint16_t i = 0; i < num_rows; i++) set_debounce_pin_(i); - for (uint16_t i = 0; i < (8 + num_cols); i++) - set_debounce_pin_(i); + for (uint16_t i = 0; i < num_cols; i++) + set_debounce_pin_(i + 8); } } // namespace sx1509 diff --git a/esphome/components/teleinfo/sensor/__init__.py b/esphome/components/teleinfo/sensor/__init__.py index b436c70120..150484d97a 100644 --- a/esphome/components/teleinfo/sensor/__init__.py +++ b/esphome/components/teleinfo/sensor/__init__.py @@ -1,6 +1,12 @@ import esphome.codegen as cg from esphome.components import sensor -from esphome.const import CONF_ID, ICON_FLASH, UNIT_WATT_HOURS +from esphome.const import ( + CONF_ID, + DEVICE_CLASS_ENERGY, + ICON_FLASH, + STATE_CLASS_TOTAL_INCREASING, + UNIT_WATT_HOURS, +) from .. import CONF_TAG_NAME, CONF_TELEINFO_ID, TELEINFO_LISTENER_SCHEMA, teleinfo_ns @@ -11,6 +17,8 @@ CONFIG_SCHEMA = sensor.sensor_schema( unit_of_measurement=UNIT_WATT_HOURS, icon=ICON_FLASH, accuracy_decimals=0, + device_class=DEVICE_CLASS_ENERGY, + state_class=STATE_CLASS_TOTAL_INCREASING, ).extend(TELEINFO_LISTENER_SCHEMA) diff --git a/esphome/components/template/cover/__init__.py b/esphome/components/template/cover/__init__.py index cfc19c00cd..ea4da4e73c 100644 --- a/esphome/components/template/cover/__init__.py +++ b/esphome/components/template/cover/__init__.py @@ -108,7 +108,6 @@ async def to_code(config): cg.add(var.set_optimistic(config[CONF_OPTIMISTIC])) cg.add(var.set_assumed_state(config[CONF_ASSUMED_STATE])) cg.add(var.set_restore_mode(config[CONF_RESTORE_MODE])) - cg.add(var.set_has_position(config[CONF_HAS_POSITION])) @automation.register_action( diff --git a/esphome/components/text_sensor/__init__.py b/esphome/components/text_sensor/__init__.py index 51eedf9a95..5b07dd2915 100644 --- a/esphome/components/text_sensor/__init__.py +++ b/esphome/components/text_sensor/__init__.py @@ -116,7 +116,9 @@ async def substitute_filter_to_code(config, filter_id): ) for conf in config ] - return cg.new_Pvariable(filter_id, substitutions) + return cg.new_Pvariable( + filter_id, cg.TemplateArguments(len(substitutions)), substitutions + ) @FILTER_REGISTRY.register("map", MapFilter, cv.ensure_list(validate_mapping)) @@ -129,7 +131,7 @@ async def map_filter_to_code(config, filter_id): ) for conf in config ] - return cg.new_Pvariable(filter_id, mappings) + return cg.new_Pvariable(filter_id, cg.TemplateArguments(len(mappings)), mappings) validate_device_class = cv.one_of(*DEVICE_CLASSES, lower=True, space="_") diff --git a/esphome/components/text_sensor/filter.cpp b/esphome/components/text_sensor/filter.cpp index f7c6a695fb..d4e6b5b9bb 100644 --- a/esphome/components/text_sensor/filter.cpp +++ b/esphome/components/text_sensor/filter.cpp @@ -73,37 +73,29 @@ bool PrependFilter::new_value(std::string &value) { return true; } -// Substitute -SubstituteFilter::SubstituteFilter(const std::initializer_list &substitutions) - : substitutions_(substitutions) {} - -bool SubstituteFilter::new_value(std::string &value) { - for (const auto &sub : this->substitutions_) { - // Compute lengths once per substitution (strlen is fast, called infrequently) - const size_t from_len = strlen(sub.from); - const size_t to_len = strlen(sub.to); +// Substitute — non-template helper +bool substitute_filter_apply(const Substitution *substitutions, size_t count, std::string &value) { + for (size_t i = 0; i < count; i++) { + const size_t from_len = strlen(substitutions[i].from); + const size_t to_len = strlen(substitutions[i].to); std::size_t pos = 0; - while ((pos = value.find(sub.from, pos, from_len)) != std::string::npos) { - value.replace(pos, from_len, sub.to, to_len); - // Advance past the replacement to avoid infinite loop when - // the replacement contains the search pattern (e.g., f -> foo) + while ((pos = value.find(substitutions[i].from, pos, from_len)) != std::string::npos) { + value.replace(pos, from_len, substitutions[i].to, to_len); pos += to_len; } } return true; } -// Map -MapFilter::MapFilter(const std::initializer_list &mappings) : mappings_(mappings) {} - -bool MapFilter::new_value(std::string &value) { - for (const auto &mapping : this->mappings_) { - if (value == mapping.from) { - value.assign(mapping.to); +// Map — non-template helper +bool map_filter_apply(const Substitution *mappings, size_t count, std::string &value) { + for (size_t i = 0; i < count; i++) { + if (value == mappings[i].from) { + value.assign(mappings[i].to); return true; } } - return true; // Pass through if no match + return true; } } // namespace esphome::text_sensor diff --git a/esphome/components/text_sensor/filter.h b/esphome/components/text_sensor/filter.h index 8a8bc55c8e..6db76dcb64 100644 --- a/esphome/components/text_sensor/filter.h +++ b/esphome/components/text_sensor/filter.h @@ -3,6 +3,8 @@ #include "esphome/core/defines.h" #ifdef USE_TEXT_SENSOR_FILTER +#include + #include "esphome/core/component.h" #include "esphome/core/helpers.h" @@ -121,16 +123,25 @@ struct Substitution { const char *to; }; -/// A simple filter that replaces a substring with another substring -class SubstituteFilter : public Filter { +/// Non-template helper (implementation in filter.cpp) +bool substitute_filter_apply(const Substitution *substitutions, size_t count, std::string &value); + +/// A simple filter that replaces a substring with another substring. +/// N is set by code generation to match the exact number of substitutions configured in YAML. +template class SubstituteFilter : public Filter { public: - explicit SubstituteFilter(const std::initializer_list &substitutions); - bool new_value(std::string &value) override; + explicit SubstituteFilter(const std::initializer_list &substitutions) { + init_array_from(this->substitutions_, substitutions); + } + bool new_value(std::string &value) override { return substitute_filter_apply(this->substitutions_.data(), N, value); } protected: - FixedVector substitutions_; + std::array substitutions_{}; }; +/// Non-template helper (implementation in filter.cpp) +bool map_filter_apply(const Substitution *mappings, size_t count, std::string &value); + /** A filter that maps values from one set to another * * Uses linear search instead of std::map for typical small datasets (2-20 mappings). @@ -154,14 +165,18 @@ class SubstituteFilter : public Filter { * - Faster for typical ESPHome usage (2-10 mappings common, 20+ rare) * * Break-even point: ~35-40 mappings, but ESPHome configs rarely exceed 20 + * + * N is set by code generation to match the exact number of mappings configured in YAML. */ -class MapFilter : public Filter { +template class MapFilter : public Filter { public: - explicit MapFilter(const std::initializer_list &mappings); - bool new_value(std::string &value) override; + explicit MapFilter(const std::initializer_list &mappings) { + init_array_from(this->mappings_, mappings); + } + bool new_value(std::string &value) override { return map_filter_apply(this->mappings_.data(), N, value); } protected: - FixedVector mappings_; + std::array mappings_{}; }; } // namespace esphome::text_sensor diff --git a/esphome/components/thermostat/climate.py b/esphome/components/thermostat/climate.py index f7c1298d68..d609e22ac2 100644 --- a/esphome/components/thermostat/climate.py +++ b/esphome/components/thermostat/climate.py @@ -118,10 +118,8 @@ PRESET_CONFIG_SCHEMA = cv.Schema( cv.Optional(CONF_MODE): validate_climate_mode, cv.Optional(CONF_DEFAULT_TARGET_TEMPERATURE_HIGH): cv.temperature, cv.Optional(CONF_DEFAULT_TARGET_TEMPERATURE_LOW): cv.temperature, - cv.Optional(CONF_FAN_MODE): cv.templatable(climate.validate_climate_fan_mode), - cv.Optional(CONF_SWING_MODE): cv.templatable( - climate.validate_climate_swing_mode - ), + cv.Optional(CONF_FAN_MODE): climate.validate_climate_fan_mode, + cv.Optional(CONF_SWING_MODE): climate.validate_climate_swing_mode, } ) @@ -503,7 +501,7 @@ def validate_thermostat(config): # If restoring default preset on boot is true then ensure we have a default preset if ( CONF_ON_BOOT_RESTORE_FROM in config - and config[CONF_ON_BOOT_RESTORE_FROM] is OnBootRestoreFrom.DEFAULT_PRESET + and config[CONF_ON_BOOT_RESTORE_FROM] == "DEFAULT_PRESET" and CONF_DEFAULT_PRESET not in config ): raise cv.Invalid( @@ -631,7 +629,7 @@ CONFIG_SCHEMA = cv.All( ): automation.validate_automation(single=True), cv.Optional(CONF_HUMIDITY_HYSTERESIS, default=1.0): cv.percentage, cv.Optional(CONF_DEFAULT_MODE, default=None): cv.valid, - cv.Optional(CONF_DEFAULT_PRESET): cv.templatable(cv.string), + cv.Optional(CONF_DEFAULT_PRESET): cv.string, cv.Optional(CONF_DEFAULT_TARGET_TEMPERATURE_HIGH): cv.temperature, cv.Optional(CONF_DEFAULT_TARGET_TEMPERATURE_LOW): cv.temperature, cv.Optional( diff --git a/esphome/components/thermostat/thermostat_climate.cpp b/esphome/components/thermostat/thermostat_climate.cpp index d52a22f880..d979359c1f 100644 --- a/esphome/components/thermostat/thermostat_climate.cpp +++ b/esphome/components/thermostat/thermostat_climate.cpp @@ -2,6 +2,7 @@ #include "esphome/core/application.h" #include "esphome/core/helpers.h" #include "esphome/core/log.h" +#include namespace esphome::thermostat { @@ -606,6 +607,16 @@ void ThermostatClimate::switch_to_action_(climate::ClimateAction action, bool pu } void ThermostatClimate::switch_to_supplemental_action_(climate::ClimateAction action) { + // Always cancel max-runtime timers and clear exceeded flags when transitioning to idle/off, + // even if supplemental_action_ is already idle (early-return path). This prevents a stale + // heating_max_runtime_exceeded_ flag from triggering supplemental on the next heating cycle + // when HEATING_MAX_RUN_TIME fires while the main action is already IDLE. + if (action == climate::CLIMATE_ACTION_OFF || action == climate::CLIMATE_ACTION_IDLE) { + this->cancel_timer_(thermostat::THERMOSTAT_TIMER_COOLING_MAX_RUN_TIME); + this->cancel_timer_(thermostat::THERMOSTAT_TIMER_HEATING_MAX_RUN_TIME); + this->cooling_max_runtime_exceeded_ = false; + this->heating_max_runtime_exceeded_ = false; + } // setup_complete_ helps us ensure an action is called immediately after boot if ((action == this->supplemental_action_) && this->setup_complete_) { // already in target mode @@ -975,8 +986,10 @@ void ThermostatClimate::cooling_on_timer_callback_() { void ThermostatClimate::fan_mode_timer_callback_() { ESP_LOGVV(TAG, "fan_mode timer expired"); this->switch_to_fan_mode_(this->fan_mode.value_or(climate::CLIMATE_FAN_ON)); - if (this->supports_fan_only_action_uses_fan_mode_timer_) + if (this->supports_fan_only_action_uses_fan_mode_timer_) { this->switch_to_action_(this->compute_action_()); + this->switch_to_supplemental_action_(this->compute_supplemental_action_()); + } } void ThermostatClimate::fanning_off_timer_callback_() { @@ -1334,15 +1347,16 @@ void ThermostatClimate::set_timer_duration_in_sec_(ThermostatClimateTimerIndex t if (elapsed >= new_duration_ms) { // Timer should complete immediately (including when new_duration_ms is 0) - ESP_LOGVV(TAG, "timer %d completing immediately (elapsed %d >= new %d)", timer_index, elapsed, new_duration_ms); + ESP_LOGVV(TAG, "timer %d completing immediately (elapsed %" PRIu32 " >= new %" PRIu32 ")", timer_index, elapsed, + new_duration_ms); this->timer_[timer_index].active = false; // Trigger the timer callback immediately this->call_timer_callback_(timer_index); return; } else { // Adjust timer to run for remaining time - keep original start time - ESP_LOGVV(TAG, "timer %d adjusted: elapsed %d, new total %d, remaining %d", timer_index, elapsed, new_duration_ms, - new_duration_ms - elapsed); + ESP_LOGVV(TAG, "timer %d adjusted: elapsed %" PRIu32 ", new total %" PRIu32 ", remaining %" PRIu32, timer_index, + elapsed, new_duration_ms, new_duration_ms - elapsed); this->timer_[timer_index].time = new_duration_ms; return; } diff --git a/esphome/components/time/__init__.py b/esphome/components/time/__init__.py index c31ccbc7ea..7ac0abeee0 100644 --- a/esphome/components/time/__init__.py +++ b/esphome/components/time/__init__.py @@ -123,8 +123,8 @@ def _parse_cron_part(part, min_value, max_value, special_mapping): f"Can't have more than two '/' in one time expression, got {part}" ) offset, repeat = data - offset_n = 0 - if offset: + offset_n = min_value + if offset and offset not in ("*", "?"): offset_n = _parse_cron_int( offset, special_mapping, diff --git a/esphome/components/time/automation.cpp b/esphome/components/time/automation.cpp index 8bc87878d1..7eb99cfe74 100644 --- a/esphome/components/time/automation.cpp +++ b/esphome/components/time/automation.cpp @@ -20,7 +20,12 @@ bool CronTrigger::matches(const ESPTime &time) { return time.is_valid() && this->seconds_[time.second] && this->minutes_[time.minute] && this->hours_[time.hour] && this->days_of_month_[time.day_of_month] && this->months_[time.month] && this->days_of_week_[time.day_of_week]; } -void CronTrigger::loop() { +void CronTrigger::setup() { + // Cron resolution is 1 second — check once per second instead of every loop iteration + this->set_interval(1000, [this]() { this->check_time_(); }); +} + +void CronTrigger::check_time_() { ESPTime time = this->rtc_->now(); if (!time.is_valid()) return; diff --git a/esphome/components/time/automation.h b/esphome/components/time/automation.h index 4ccfc641d6..546c4a10de 100644 --- a/esphome/components/time/automation.h +++ b/esphome/components/time/automation.h @@ -26,10 +26,11 @@ class CronTrigger : public Trigger<>, public Component { void add_day_of_week(uint8_t day_of_week); void add_days_of_week(const std::vector &days_of_week); bool matches(const ESPTime &time); - void loop() override; + void setup() override; float get_setup_priority() const override; protected: + void check_time_(); std::bitset<61> seconds_; std::bitset<60> minutes_; std::bitset<24> hours_; diff --git a/esphome/components/time/posix_tz.cpp b/esphome/components/time/posix_tz.cpp index 4d1f0c74c2..c25248e457 100644 --- a/esphome/components/time/posix_tz.cpp +++ b/esphome/components/time/posix_tz.cpp @@ -4,6 +4,7 @@ #include "posix_tz.h" #include +#include namespace esphome::time { @@ -33,25 +34,43 @@ bool is_leap_year(int year) { return (year % 4 == 0 && year % 100 != 0) || (year // Get days in year (avoids duplicate is_leap_year calls) static inline int days_in_year(int year) { return is_leap_year(year) ? 366 : 365; } -// Convert days since epoch to year, updating days to remainder -static int __attribute__((noinline)) days_to_year(int64_t &days) { - int year = 1970; - int diy; - while (days >= (diy = days_in_year(year)) && year < 2200) { - days -= diy; +// Count leap years in [1, year] (i.e. up to and including year) +static constexpr int count_leap_years_up_to(int year) { return year / 4 - year / 100 + year / 400; } + +constexpr int EPOCH_YEAR = 1970; +constexpr int LEAP_YEARS_BEFORE_EPOCH = count_leap_years_up_to(EPOCH_YEAR - 1); +constexpr int DAYS_PER_YEAR = 365; +constexpr int SECONDS_PER_DAY = 86400; + +// Days from epoch (Jan 1 1970) to Jan 1 of given year — O(1) +static inline int64_t days_to_year_start(int year) { + return static_cast(DAYS_PER_YEAR) * (year - EPOCH_YEAR) + + (count_leap_years_up_to(year - 1) - LEAP_YEARS_BEFORE_EPOCH); +} + +// Convert days since epoch to year, updating days to day-of-year remainder. +// The initial estimate from days/365 can overshoot by multiple years for +// far-future dates (e.g., year 5000+) due to accumulated leap days, +// so we use loops rather than single-step correction. +static int days_to_year(int64_t &days) { + int year = static_cast(EPOCH_YEAR + days / DAYS_PER_YEAR); + int64_t year_start = days_to_year_start(year); + while (days < year_start) { + year--; + year_start = days_to_year_start(year); + } + while (days >= year_start + days_in_year(year)) { + year_start += days_in_year(year); year++; } - while (days < 0 && year > 1900) { - year--; - days += days_in_year(year); - } + days -= year_start; return year; } -// Extract just the year from a UTC epoch +// Extract just the year from a UTC epoch — O(1) static int epoch_to_year(time_t epoch) { - int64_t days = epoch / 86400; - if (epoch < 0 && epoch % 86400 != 0) + int64_t days = epoch / SECONDS_PER_DAY; + if (epoch < 0 && epoch % SECONDS_PER_DAY != 0) days--; return days_to_year(days); } @@ -86,11 +105,11 @@ int __attribute__((noinline)) day_of_week(int year, int month, int day) { void __attribute__((noinline)) epoch_to_tm_utc(time_t epoch, struct tm *out_tm) { // Days since epoch - int64_t days = epoch / 86400; - int32_t remaining_secs = epoch % 86400; + int64_t days = epoch / SECONDS_PER_DAY; + int32_t remaining_secs = epoch % SECONDS_PER_DAY; if (remaining_secs < 0) { days--; - remaining_secs += 86400; + remaining_secs += SECONDS_PER_DAY; } out_tm->tm_sec = remaining_secs % 60; @@ -279,17 +298,6 @@ static int __attribute__((noinline)) days_from_year_start(int year, int month, i return days; } -// Calculate days from epoch to Jan 1 of given year (for DST transition calculations) -// Only supports years >= 1970. Timezone is either compiled in from YAML or set by -// Home Assistant, so pre-1970 dates are not a concern. -static int64_t __attribute__((noinline)) days_to_year_start(int year) { - int64_t days = 0; - for (int y = 1970; y < year; y++) { - days += days_in_year(y); - } - return days; -} - time_t __attribute__((noinline)) calculate_dst_transition(int year, const DSTRule &rule, int32_t base_offset_seconds) { int month, day; @@ -338,7 +346,7 @@ time_t __attribute__((noinline)) calculate_dst_transition(int year, const DSTRul int64_t days = days_to_year_start(year) + days_from_year_start(year, month, day); // Convert to epoch and add transition time and base offset - return days * 86400 + rule.time_seconds + base_offset_seconds; + return days * SECONDS_PER_DAY + rule.time_seconds + base_offset_seconds; } } // namespace internal @@ -442,6 +450,18 @@ bool parse_posix_tz(const char *tz_string, ParsedTimezone &result) { return internal::parse_dst_rule(p, result.dst_end); } +// Format a POSIX offset (positive = west) as "+HHMM" / "-HHMM" for display. +// Convention: negate POSIX sign so east-of-UTC is positive (ISO 8601 / RFC 2822). +void format_designation(int32_t posix_offset, char *buf, size_t buf_size) { + int32_t display = -posix_offset; + char sign = display >= 0 ? '+' : '-'; + if (display < 0) + display = -display; + int h = display / 3600; + int m = (display % 3600) / 60; + snprintf(buf, buf_size, "%c%02d%02d", sign, h, m); +} + bool epoch_to_local_tm(time_t utc_epoch, const ParsedTimezone &tz, struct tm *out_tm) { if (!out_tm) { return false; diff --git a/esphome/components/time/posix_tz.h b/esphome/components/time/posix_tz.h index c71ba15cd1..be1ddfd689 100644 --- a/esphome/components/time/posix_tz.h +++ b/esphome/components/time/posix_tz.h @@ -36,6 +36,9 @@ struct ParsedTimezone { bool has_dst() const { return this->dst_start.type != DSTRuleType::NONE; } }; +/// Format a POSIX offset as "+HHMM"/"-HHMM" into buf (must be >= 6 bytes). +void format_designation(int32_t posix_offset, char *buf, size_t buf_size); + /// Parse a POSIX TZ string into a ParsedTimezone struct. /// /// @deprecated Remove before 2026.9.0 (bridge code for backward compatibility). diff --git a/esphome/components/time_based/__init__.py b/esphome/components/time_based/__init__.py index e69de29bb2..ce2f453bda 100644 --- a/esphome/components/time_based/__init__.py +++ b/esphome/components/time_based/__init__.py @@ -0,0 +1,3 @@ +import esphome.codegen as cg + +time_based_ns = cg.esphome_ns.namespace("time_based") diff --git a/esphome/components/time_based/cover.py b/esphome/components/time_based/cover/__init__.py similarity index 97% rename from esphome/components/time_based/cover.py rename to esphome/components/time_based/cover/__init__.py index d14332d453..022b48d249 100644 --- a/esphome/components/time_based/cover.py +++ b/esphome/components/time_based/cover/__init__.py @@ -11,7 +11,8 @@ from esphome.const import ( CONF_STOP_ACTION, ) -time_based_ns = cg.esphome_ns.namespace("time_based") +from .. import time_based_ns + TimeBasedCover = time_based_ns.class_("TimeBasedCover", cover.Cover, cg.Component) CONF_HAS_BUILT_IN_ENDSTOP = "has_built_in_endstop" diff --git a/esphome/components/time_based/time_based_cover.cpp b/esphome/components/time_based/cover/time_based_cover.cpp similarity index 100% rename from esphome/components/time_based/time_based_cover.cpp rename to esphome/components/time_based/cover/time_based_cover.cpp diff --git a/esphome/components/time_based/time_based_cover.h b/esphome/components/time_based/cover/time_based_cover.h similarity index 100% rename from esphome/components/time_based/time_based_cover.h rename to esphome/components/time_based/cover/time_based_cover.h diff --git a/esphome/components/tm1637/tm1637.cpp b/esphome/components/tm1637/tm1637.cpp index f9c876f40c..da9adb59a4 100644 --- a/esphome/components/tm1637/tm1637.cpp +++ b/esphome/components/tm1637/tm1637.cpp @@ -348,6 +348,12 @@ uint8_t TM1637Display::print(uint8_t start_pos, const char *str) { return pos - start_pos; } uint8_t TM1637Display::print(const char *str) { return this->print(0, str); } + +void TM1637Display::set_buffer(const uint8_t *data, uint8_t length) { + uint8_t len = std::min(length, (uint8_t) sizeof(this->buffer_)); + memcpy(this->buffer_, data, len); +} + uint8_t TM1637Display::printf(uint8_t pos, const char *format, ...) { va_list arg; va_start(arg, format); diff --git a/esphome/components/tm1637/tm1637.h b/esphome/components/tm1637/tm1637.h index b9e96119e9..c1fbabb21b 100644 --- a/esphome/components/tm1637/tm1637.h +++ b/esphome/components/tm1637/tm1637.h @@ -47,6 +47,9 @@ class TM1637Display : public PollingComponent { /// Print `str` at position 0. uint8_t print(const char *str); + /// Set raw buffer bytes from data array up to length bytes. + void set_buffer(const uint8_t *data, uint8_t length); + void set_intensity(uint8_t intensity) { this->intensity_ = intensity; } void set_inverted(bool inverted) { this->inverted_ = inverted; } void set_length(uint8_t length) { this->length_ = length; } diff --git a/esphome/components/tm1651/__init__.py b/esphome/components/tm1651/__init__.py index fb35eb21b5..7d957df3be 100644 --- a/esphome/components/tm1651/__init__.py +++ b/esphome/components/tm1651/__init__.py @@ -39,7 +39,7 @@ CONFIG_SCHEMA = cv.All( cv.Required(CONF_CLK_PIN): pins.internal_gpio_output_pin_schema, cv.Required(CONF_DIO_PIN): pins.internal_gpio_output_pin_schema, } - ), + ).extend(cv.COMPONENT_SCHEMA), ) diff --git a/esphome/components/tmp117/tmp117.cpp b/esphome/components/tmp117/tmp117.cpp index f8f52266e0..b3e900f5b6 100644 --- a/esphome/components/tmp117/tmp117.cpp +++ b/esphome/components/tmp117/tmp117.cpp @@ -4,8 +4,7 @@ #include "tmp117.h" #include "esphome/core/log.h" -namespace esphome { -namespace tmp117 { +namespace esphome::tmp117 { static const char *const TAG = "tmp117"; @@ -18,11 +17,10 @@ void TMP117Component::update() { if ((uint16_t) data != 0x8000) { float temperature = data * 0.0078125f; - ESP_LOGD(TAG, "Got temperature=%.2f°C", temperature); this->publish_state(temperature); this->status_clear_warning(); } else { - ESP_LOGD(TAG, "TMP117 not ready"); + ESP_LOGD(TAG, "Not ready"); } } void TMP117Component::setup() { @@ -38,7 +36,7 @@ void TMP117Component::setup() { } } void TMP117Component::dump_config() { - ESP_LOGD(TAG, "TMP117:"); + ESP_LOGCONFIG(TAG, "TMP117:"); LOG_I2C_DEVICE(this); if (this->is_failed()) { ESP_LOGE(TAG, ESP_LOG_MSG_COMM_FAIL); @@ -48,7 +46,7 @@ void TMP117Component::dump_config() { bool TMP117Component::read_data_(int16_t *data) { if (!this->read_byte_16(0, (uint16_t *) data)) { - ESP_LOGW(TAG, "Updating TMP117 failed!"); + ESP_LOGW(TAG, "Updating failed"); return false; } return true; @@ -56,7 +54,7 @@ bool TMP117Component::read_data_(int16_t *data) { bool TMP117Component::read_config_(uint16_t *config) { if (!this->read_byte_16(1, (uint16_t *) config)) { - ESP_LOGW(TAG, "Reading TMP117 config failed!"); + ESP_LOGW(TAG, "Reading config failed"); return false; } return true; @@ -64,11 +62,10 @@ bool TMP117Component::read_config_(uint16_t *config) { bool TMP117Component::write_config_(uint16_t config) { if (!this->write_byte_16(1, config)) { - ESP_LOGE(TAG, "Writing TMP117 config failed!"); + ESP_LOGE(TAG, "Writing config failed"); return false; } return true; } -} // namespace tmp117 -} // namespace esphome +} // namespace esphome::tmp117 diff --git a/esphome/components/tmp117/tmp117.h b/esphome/components/tmp117/tmp117.h index f501ee270c..a8fe7ac7ce 100644 --- a/esphome/components/tmp117/tmp117.h +++ b/esphome/components/tmp117/tmp117.h @@ -4,8 +4,7 @@ #include "esphome/components/sensor/sensor.h" #include "esphome/components/i2c/i2c.h" -namespace esphome { -namespace tmp117 { +namespace esphome::tmp117 { class TMP117Component : public PollingComponent, public i2c::I2CDevice, public sensor::Sensor { public: @@ -22,5 +21,4 @@ class TMP117Component : public PollingComponent, public i2c::I2CDevice, public s uint16_t config_; }; -} // namespace tmp117 -} // namespace esphome +} // namespace esphome::tmp117 diff --git a/esphome/components/tormatic/tormatic_cover.cpp b/esphome/components/tormatic/tormatic_cover.cpp index 37a269088e..a58228a219 100644 --- a/esphome/components/tormatic/tormatic_cover.cpp +++ b/esphome/components/tormatic/tormatic_cover.cpp @@ -1,3 +1,4 @@ +#include #include #include "tormatic_cover.h" @@ -9,6 +10,10 @@ namespace tormatic { static const char *const TAG = "tormatic.cover"; +// Time to poll the UART when flushing after desync. At 9600 baud, a full +// 12-byte message takes ~12.5ms, so 15ms guarantees all bytes have arrived. +static constexpr uint32_t DRAIN_TIMEOUT_MS = 15; + using namespace esphome::cover; void Tormatic::setup() { @@ -120,11 +125,11 @@ void Tormatic::recalibrate_duration_(GateStatus s) { if (s == OPENED) { this->open_duration_ = now - this->direction_start_time_; - ESP_LOGI(TAG, "Recalibrated the gate's open duration to %dms", this->open_duration_); + ESP_LOGI(TAG, "Recalibrated the gate's open duration to %" PRIu32 "ms", this->open_duration_); } if (s == CLOSED) { this->close_duration_ = now - this->direction_start_time_; - ESP_LOGI(TAG, "Recalibrated the gate's close duration to %dms", this->close_duration_); + ESP_LOGI(TAG, "Recalibrated the gate's close duration to %" PRIu32 "ms", this->close_duration_); } this->direction_start_time_ = 0; @@ -255,32 +260,51 @@ void Tormatic::stop_at_target_() { // Read a GateStatus from the unit. The unit only sends messages in response to // status requests or commands, so a message needs to be sent first. optional Tormatic::read_gate_status_() { - if (this->available() < sizeof(MessageHeader)) { + if (!this->pending_hdr_) { + if (this->available() < sizeof(MessageHeader)) { + return {}; + } + + this->pending_hdr_ = this->read_data_(); + if (!this->pending_hdr_) { + return {}; + } + + // Sanity check: valid messages have small payloads (3-4 bytes). A large + // or impossible payload_size means the stream is out of sync (corrupted + // byte, dropped data, etc.). Flush the buffer so we can resync on the + // next request/response cycle. + if (this->pending_hdr_->payload_size() > sizeof(CommandRequestReply)) { + ESP_LOGW(TAG, "Unexpected payload size %" PRIu32 ", flushing rx buffer", this->pending_hdr_->payload_size()); + this->pending_hdr_.reset(); + this->drain_rx_(); + return {}; + } + } + + // Wait for all payload bytes to arrive before processing. + if (this->available() < this->pending_hdr_->payload_size()) { return {}; } - auto o_hdr = this->read_data_(); - if (!o_hdr) { - ESP_LOGE(TAG, "Timeout reading message header"); - return {}; - } - auto hdr = o_hdr.value(); + auto hdr = *this->pending_hdr_; + this->pending_hdr_.reset(); switch (hdr.type) { case STATUS: { if (hdr.payload_size() != sizeof(StatusReply)) { - ESP_LOGE(TAG, "Header specifies payload size %d but size of StatusReply is %d", hdr.payload_size(), + ESP_LOGE(TAG, "Header specifies payload size %" PRIu32 " but size of StatusReply is %zu", hdr.payload_size(), sizeof(StatusReply)); + this->drain_rx_(hdr.payload_size()); + return {}; } - // Read a StatusReply requested by update(). auto o_status = this->read_data_(); if (!o_status) { return {}; } - auto status = o_status.value(); - return status.state; + return o_status->state; } case COMMAND: @@ -294,7 +318,7 @@ optional Tormatic::read_gate_status_() { default: // Unknown message type, drain the remaining amount of bytes specified in // the header. - ESP_LOGE(TAG, "Reading remaining %d payload bytes of unknown type 0x%x", hdr.payload_size(), hdr.type); + ESP_LOGE(TAG, "Reading remaining %" PRIu32 " payload bytes of unknown type 0x%x", hdr.payload_size(), hdr.type); break; } @@ -339,20 +363,28 @@ template optional Tormatic::read_data_() { } obj.byteswap(); - ESP_LOGV(TAG, "Read %s in %d ms", obj.print().c_str(), millis() - start); + ESP_LOGV(TAG, "Read %s in %" PRIu32 " ms", obj.print().c_str(), millis() - start); return obj; } -// Drain up to n amount of bytes from the uart rx buffer. +// Drain bytes from the uart rx buffer. When n > 0, drain exactly n bytes +// (caller must ensure they are available). When n == 0, poll for 15ms to +// guarantee a full packet time at 9600 baud has elapsed, consuming any +// bytes still in transit. void Tormatic::drain_rx_(uint16_t n) { uint8_t data; - uint16_t count = 0; - while (this->available()) { - this->read_byte(&data); - count++; - - if (n > 0 && count >= n) { - return; + if (n > 0) { + for (uint16_t i = 0; i < n; i++) { + if (!this->read_byte(&data)) { + return; + } + } + } else { + uint32_t start = millis(); + while (millis() - start < DRAIN_TIMEOUT_MS) { + if (this->available()) { + this->read_byte(&data); + } } } } diff --git a/esphome/components/tormatic/tormatic_cover.h b/esphome/components/tormatic/tormatic_cover.h index 534d4bef14..34483ed6a3 100644 --- a/esphome/components/tormatic/tormatic_cover.h +++ b/esphome/components/tormatic/tormatic_cover.h @@ -43,6 +43,7 @@ class Tormatic : public cover::Cover, public uart::UARTDevice, public PollingCom void handle_gate_status_(GateStatus s); uint32_t seq_tx_{0}; + optional pending_hdr_{}; GateStatus current_status_{PAUSED}; diff --git a/esphome/components/tormatic/tormatic_protocol.h b/esphome/components/tormatic/tormatic_protocol.h index 26a634b630..269b63ff78 100644 --- a/esphome/components/tormatic/tormatic_protocol.h +++ b/esphome/components/tormatic/tormatic_protocol.h @@ -1,5 +1,7 @@ #pragma once +#include + #include "esphome/components/cover/cover.h" /** @@ -86,7 +88,7 @@ struct MessageHeader { std::string print() { // 64 bytes: "MessageHeader: seq " + uint16 + ", len " + uint32 + ", type " + type + safety margin char buf[64]; - buf_append_printf(buf, sizeof(buf), 0, "MessageHeader: seq %d, len %d, type %s", this->seq, this->len, + buf_append_printf(buf, sizeof(buf), 0, "MessageHeader: seq %d, len %" PRIu32 ", type %s", this->seq, this->len, message_type_to_str(this->type)); return buf; } diff --git a/esphome/components/total_daily_energy/total_daily_energy.cpp b/esphome/components/total_daily_energy/total_daily_energy.cpp index e7a45a5edf..161c712cc1 100644 --- a/esphome/components/total_daily_energy/total_daily_energy.cpp +++ b/esphome/components/total_daily_energy/total_daily_energy.cpp @@ -1,10 +1,21 @@ #include "total_daily_energy.h" +#include "esphome/core/application.h" #include "esphome/core/log.h" -namespace esphome { -namespace total_daily_energy { +namespace esphome::total_daily_energy { static const char *const TAG = "total_daily_energy"; +static constexpr uint32_t TIMEOUT_ID_MIDNIGHT = 1; +static constexpr uint8_t SECONDS_PER_MINUTE = 60; +static constexpr uint8_t MINUTES_PER_HOUR = 60; +static constexpr uint8_t HOURS_PER_DAY = 24; +static constexpr uint32_t SECONDS_PER_HOUR = SECONDS_PER_MINUTE * MINUTES_PER_HOUR; +static constexpr uint16_t MILLIS_PER_SECOND = 1000; +// Wake up 90 minutes before midnight to recalculate, ensuring DST transitions +// (which shift wall clock by 1 hour but don't change millis()) don't cause +// the midnight reset to fire late. DST transitions don't trigger the time sync +// callback since they change local time interpretation, not the epoch. +static constexpr uint32_t PRE_MIDNIGHT_SECONDS = 90 * SECONDS_PER_MINUTE; void TotalDailyEnergy::setup() { float initial_value = 0; @@ -15,28 +26,55 @@ void TotalDailyEnergy::setup() { } this->publish_state_and_save(initial_value); - this->last_update_ = millis(); + this->last_update_ = App.get_loop_component_start_time(); this->parent_->add_on_state_callback([this](float state) { this->process_new_state_(state); }); + + // Schedule initial midnight reset if time is already valid, otherwise + // the time sync callback will handle it once time becomes available. + this->schedule_midnight_reset_(); + // Re-schedule on every NTP sync in case the clock jumped across midnight. + this->time_->add_on_time_sync_callback([this]() { this->schedule_midnight_reset_(); }); } void TotalDailyEnergy::dump_config() { LOG_SENSOR("", "Total Daily Energy", this); } -void TotalDailyEnergy::loop() { +void TotalDailyEnergy::schedule_midnight_reset_() { auto t = this->time_->now(); if (!t.is_valid()) return; - if (this->last_day_of_year_ == 0) { + // Check if the day changed (time sync moved us past midnight, or first call) + if (this->last_day_of_year_ != t.day_of_year) { + if (this->last_day_of_year_ != 0) { + // Day actually changed — reset energy + this->total_energy_ = 0; + this->publish_state_and_save(0); + } this->last_day_of_year_ = t.day_of_year; - return; } - if (t.day_of_year != this->last_day_of_year_) { - this->last_day_of_year_ = t.day_of_year; - this->total_energy_ = 0; - this->publish_state_and_save(0); + // Calculate seconds until next midnight. + // Uses the same TIMEOUT_ID_MIDNIGHT ID so re-scheduling (e.g. from time sync) cancels + // any previously pending timeout. + uint32_t seconds_until_midnight = + ((HOURS_PER_DAY - 1 - t.hour) * MINUTES_PER_HOUR + (MINUTES_PER_HOUR - 1 - t.minute)) * SECONDS_PER_MINUTE + + (SECONDS_PER_MINUTE - t.second); + + // set_timeout counts real elapsed millis, but DST shifts wall clock by up to 1 hour + // without changing millis. To avoid firing up to 1 hour late/early, we use two stages: + // 1) Wake up 90 minutes before midnight to recalculate with current wall clock + // 2) From there, schedule the precise midnight reset + uint32_t timeout_seconds; + if (seconds_until_midnight > PRE_MIDNIGHT_SECONDS) { + timeout_seconds = seconds_until_midnight - PRE_MIDNIGHT_SECONDS; + } else { + timeout_seconds = seconds_until_midnight + 1; } + + ESP_LOGD(TAG, "Scheduling midnight check in %us", timeout_seconds); + this->set_timeout(TIMEOUT_ID_MIDNIGHT, timeout_seconds * MILLIS_PER_SECOND, + [this]() { this->schedule_midnight_reset_(); }); } void TotalDailyEnergy::publish_state_and_save(float state) { @@ -50,14 +88,14 @@ void TotalDailyEnergy::publish_state_and_save(float state) { void TotalDailyEnergy::process_new_state_(float state) { if (std::isnan(state)) return; - const uint32_t now = millis(); + const uint32_t now = App.get_loop_component_start_time(); const float old_state = this->last_power_state_; const float new_state = state; - float delta_hours = (now - this->last_update_) / 1000.0f / 60.0f / 60.0f; + float delta_hours = (now - this->last_update_) / static_cast(MILLIS_PER_SECOND) / SECONDS_PER_HOUR; float delta_energy = 0.0f; switch (this->method_) { case TOTAL_DAILY_ENERGY_METHOD_TRAPEZOID: - delta_energy = delta_hours * (old_state + new_state) / 2.0; + delta_energy = delta_hours * (old_state + new_state) / 2.0f; break; case TOTAL_DAILY_ENERGY_METHOD_LEFT: delta_energy = delta_hours * old_state; @@ -71,5 +109,4 @@ void TotalDailyEnergy::process_new_state_(float state) { this->publish_state_and_save(this->total_energy_ + delta_energy); } -} // namespace total_daily_energy -} // namespace esphome +} // namespace esphome::total_daily_energy diff --git a/esphome/components/total_daily_energy/total_daily_energy.h b/esphome/components/total_daily_energy/total_daily_energy.h index 1145f54f95..9a20ecea01 100644 --- a/esphome/components/total_daily_energy/total_daily_energy.h +++ b/esphome/components/total_daily_energy/total_daily_energy.h @@ -6,8 +6,7 @@ #include "esphome/components/sensor/sensor.h" #include "esphome/components/time/real_time_clock.h" -namespace esphome { -namespace total_daily_energy { +namespace esphome::total_daily_energy { enum TotalDailyEnergyMethod { TOTAL_DAILY_ENERGY_METHOD_TRAPEZOID = 0, @@ -23,12 +22,12 @@ class TotalDailyEnergy : public sensor::Sensor, public Component { void set_method(TotalDailyEnergyMethod method) { method_ = method; } void setup() override; void dump_config() override; - void loop() override; void publish_state_and_save(float state); protected: void process_new_state_(float state); + void schedule_midnight_reset_(); ESPPreferenceObject pref_; time::RealTimeClock *time_; @@ -41,5 +40,4 @@ class TotalDailyEnergy : public sensor::Sensor, public Component { float last_power_state_{0.0f}; }; -} // namespace total_daily_energy -} // namespace esphome +} // namespace esphome::total_daily_energy diff --git a/esphome/components/tt21100/binary_sensor/__init__.py b/esphome/components/tt21100/binary_sensor/__init__.py index f79eff0e01..081bd17c20 100644 --- a/esphome/components/tt21100/binary_sensor/__init__.py +++ b/esphome/components/tt21100/binary_sensor/__init__.py @@ -16,11 +16,15 @@ TT21100Button = tt21100_ns.class_( cg.Parented.template(TT21100Touchscreen), ) -CONFIG_SCHEMA = binary_sensor.binary_sensor_schema(TT21100Button).extend( - { - cv.GenerateID(CONF_TT21100_ID): cv.use_id(TT21100Touchscreen), - cv.Required(CONF_INDEX): cv.int_range(min=0, max=3), - } +CONFIG_SCHEMA = ( + binary_sensor.binary_sensor_schema(TT21100Button) + .extend( + { + cv.GenerateID(CONF_TT21100_ID): cv.use_id(TT21100Touchscreen), + cv.Required(CONF_INDEX): cv.int_range(min=0, max=3), + } + ) + .extend(cv.COMPONENT_SCHEMA) ) diff --git a/esphome/components/tuya/climate/tuya_climate.h b/esphome/components/tuya/climate/tuya_climate.h index 31bef57639..09f3fd30c3 100644 --- a/esphome/components/tuya/climate/tuya_climate.h +++ b/esphome/components/tuya/climate/tuya_climate.h @@ -105,8 +105,8 @@ class TuyaClimate : public climate::Climate, public Component { optional sleep_id_{}; optional eco_temperature_{}; TuyaDatapointType eco_type_{}; - uint8_t active_state_; - uint8_t fan_state_; + uint8_t active_state_{0}; + uint8_t fan_state_{0}; optional swing_vertical_id_{}; optional swing_horizontal_id_{}; optional fan_speed_id_{}; @@ -119,9 +119,9 @@ class TuyaClimate : public climate::Climate, public Component { bool swing_horizontal_{false}; bool heating_state_{false}; bool cooling_state_{false}; - float manual_temperature_; - bool eco_; - bool sleep_; + float manual_temperature_{NAN}; + bool eco_{false}; + bool sleep_{false}; bool reports_fahrenheit_{false}; }; diff --git a/esphome/components/tx20/sensor.py b/esphome/components/tx20/sensor.py index 4f1582072e..1bb5ab0706 100644 --- a/esphome/components/tx20/sensor.py +++ b/esphome/components/tx20/sensor.py @@ -7,6 +7,7 @@ from esphome.const import ( CONF_PIN, CONF_WIND_DIRECTION_DEGREES, CONF_WIND_SPEED, + DEVICE_CLASS_WIND_SPEED, ICON_SIGN_DIRECTION, ICON_WEATHER_WINDY, STATE_CLASS_MEASUREMENT, @@ -24,12 +25,14 @@ CONFIG_SCHEMA = cv.Schema( unit_of_measurement=UNIT_KILOMETER_PER_HOUR, icon=ICON_WEATHER_WINDY, accuracy_decimals=1, + device_class=DEVICE_CLASS_WIND_SPEED, state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional(CONF_WIND_DIRECTION_DEGREES): sensor.sensor_schema( unit_of_measurement=UNIT_DEGREES, icon=ICON_SIGN_DIRECTION, accuracy_decimals=1, + state_class=STATE_CLASS_MEASUREMENT, ), cv.Required(CONF_PIN): cv.All(pins.internal_gpio_input_pin_schema), } diff --git a/esphome/components/uart/__init__.py b/esphome/components/uart/__init__.py index 83649cc209..7075228743 100644 --- a/esphome/components/uart/__init__.py +++ b/esphome/components/uart/__init__.py @@ -42,16 +42,6 @@ CODEOWNERS = ["@esphome/core"] DOMAIN = "uart" -def AUTO_LOAD() -> list[str]: - """Ideally, we would only auto-load socket only when wake_loop_on_rx is requested; - however, AUTO_LOAD is examined before wake_loop_on_rx is set, so instead, since ESP32 - always uses socket select support in the main app, we'll just ensure it's loaded here. - """ - if CORE.is_esp32: - return ["socket"] - return [] - - uart_ns = cg.esphome_ns.namespace("uart") UARTComponent = uart_ns.class_("UARTComponent") @@ -527,10 +517,6 @@ async def final_step(): # Wake-on-RX is essentially free on ESP32 (just an ISR function pointer # registration) — enable by default to reduce RX buffer overflow risk # by waking the main loop immediately when data arrives. - # Requires networking for the wake_loop_isrsafe() infrastructure. - from esphome.components import socket - - socket.require_wake_loop_threadsafe() cg.add_define("USE_UART_WAKE_LOOP_ON_RX") diff --git a/esphome/components/uart/uart_component.h b/esphome/components/uart/uart_component.h index abc77fbae8..afd3ad5777 100644 --- a/esphome/components/uart/uart_component.h +++ b/esphome/components/uart/uart_component.h @@ -85,6 +85,10 @@ class UARTComponent { // @return UARTFlushResult indicating whether the flush was confirmed, timed out, failed, or assumed successful. virtual UARTFlushResult flush() = 0; + // Returns true if the underlying transport is connected and operational. + // Hardware UARTs always return true. USB-backed UARTs override to reflect actual connection state. + virtual bool is_connected() { return true; } + // Sets the maximum time to wait for TX to drain during flush(). // Only meaningful on ESP32 (IDF). Other platforms ignore this value. // @param flush_timeout_ms Timeout in milliseconds; 0 means wait indefinitely. diff --git a/esphome/components/uart/uart_component_esp_idf.cpp b/esphome/components/uart/uart_component_esp_idf.cpp index cd77cd1189..93e43e0372 100644 --- a/esphome/components/uart/uart_component_esp_idf.cpp +++ b/esphome/components/uart/uart_component_esp_idf.cpp @@ -147,6 +147,20 @@ void IDFUARTComponent::load_settings(bool dump_config) { return; } + // uart_param_config must be called after uart_driver_install and before any + // other uart_set_*() calls. The driver installation resets the UART peripheral + // registers to their default state, overwriting any previously configured baud + // rate or framing settings. Calling uart_param_config here ensures the requested + // settings are applied after the reset and before pin routing, inversion, and + // threshold configuration. + uart_config_t uart_config = this->get_config_(); + err = uart_param_config(this->uart_num_, &uart_config); + if (err != ESP_OK) { + ESP_LOGW(TAG, "uart_param_config failed: %s", esp_err_to_name(err)); + this->mark_failed(); + return; + } + int8_t tx = this->tx_pin_ != nullptr ? this->tx_pin_->get_pin() : -1; int8_t rx = this->rx_pin_ != nullptr ? this->rx_pin_->get_pin() : -1; int8_t flow_control = this->flow_control_pin_ != nullptr ? this->flow_control_pin_->get_pin() : -1; @@ -214,22 +228,15 @@ void IDFUARTComponent::load_settings(bool dump_config) { return; } + // Per ESP-IDF docs, uart_set_mode() must be called only after uart_driver_install(). auto mode = this->flow_control_pin_ != nullptr ? UART_MODE_RS485_HALF_DUPLEX : UART_MODE_UART; - err = uart_set_mode(this->uart_num_, mode); // per docs, must be called only after uart_driver_install() + err = uart_set_mode(this->uart_num_, mode); if (err != ESP_OK) { ESP_LOGW(TAG, "uart_set_mode failed: %s", esp_err_to_name(err)); this->mark_failed(); return; } - uart_config_t uart_config = this->get_config_(); - err = uart_param_config(this->uart_num_, &uart_config); - if (err != ESP_OK) { - ESP_LOGW(TAG, "uart_param_config failed: %s", esp_err_to_name(err)); - this->mark_failed(); - return; - } - #ifdef USE_UART_WAKE_LOOP_ON_RX // Register ISR callback to wake the main loop when UART data arrives. // The callback runs in ISR context and uses vTaskNotifyGiveFromISR() to @@ -296,7 +303,7 @@ void IDFUARTComponent::set_rx_timeout(size_t rx_timeout) { void IDFUARTComponent::write_array(const uint8_t *data, size_t len) { int32_t write_len = uart_write_bytes(this->uart_num_, data, len); if (write_len != (int32_t) len) { - ESP_LOGW(TAG, "uart_write_bytes failed: %d != %zu", write_len, len); + ESP_LOGW(TAG, "uart_write_bytes failed: %" PRId32 " != %zu", write_len, len); this->mark_failed(); } #ifdef USE_UART_DEBUGGER diff --git a/esphome/components/uponor_smatrix/climate/uponor_smatrix_climate.cpp b/esphome/components/uponor_smatrix/climate/uponor_smatrix_climate.cpp index 3eae4d2d96..512a258122 100644 --- a/esphome/components/uponor_smatrix/climate/uponor_smatrix_climate.cpp +++ b/esphome/components/uponor_smatrix/climate/uponor_smatrix_climate.cpp @@ -3,6 +3,8 @@ #include "esphome/core/helpers.h" #include "esphome/core/log.h" +#include + namespace esphome { namespace uponor_smatrix { @@ -10,7 +12,7 @@ static const char *const TAG = "uponor_smatrix.climate"; void UponorSmatrixClimate::dump_config() { LOG_CLIMATE("", "Uponor Smatrix Climate", this); - ESP_LOGCONFIG(TAG, " Device address: 0x%08X", this->address_); + ESP_LOGCONFIG(TAG, " Device address: 0x%08" PRIX32, this->address_); } void UponorSmatrixClimate::loop() { diff --git a/esphome/components/uponor_smatrix/sensor/uponor_smatrix_sensor.cpp b/esphome/components/uponor_smatrix/sensor/uponor_smatrix_sensor.cpp index 7ee12edcdb..5f690a6879 100644 --- a/esphome/components/uponor_smatrix/sensor/uponor_smatrix_sensor.cpp +++ b/esphome/components/uponor_smatrix/sensor/uponor_smatrix_sensor.cpp @@ -1,6 +1,8 @@ #include "uponor_smatrix_sensor.h" #include "esphome/core/log.h" +#include + namespace esphome { namespace uponor_smatrix { @@ -9,7 +11,7 @@ static const char *const TAG = "uponor_smatrix.sensor"; void UponorSmatrixSensor::dump_config() { ESP_LOGCONFIG(TAG, "Uponor Smatrix Sensor\n" - " Device address: 0x%08X", + " Device address: 0x%08" PRIX32, this->address_); LOG_SENSOR(" ", "Temperature", this->temperature_sensor_); LOG_SENSOR(" ", "External Temperature", this->external_temperature_sensor_); diff --git a/esphome/components/uponor_smatrix/uponor_smatrix.cpp b/esphome/components/uponor_smatrix/uponor_smatrix.cpp index 4c3a4b05df..1fd53955a0 100644 --- a/esphome/components/uponor_smatrix/uponor_smatrix.cpp +++ b/esphome/components/uponor_smatrix/uponor_smatrix.cpp @@ -3,6 +3,8 @@ #include "esphome/core/helpers.h" #include "esphome/core/log.h" +#include + namespace esphome { namespace uponor_smatrix { @@ -24,7 +26,7 @@ void UponorSmatrixComponent::dump_config() { #ifdef USE_TIME if (this->time_id_ != nullptr) { ESP_LOGCONFIG(TAG, " Time synchronization: YES"); - ESP_LOGCONFIG(TAG, " Time master device address: 0x%08X", this->time_device_address_); + ESP_LOGCONFIG(TAG, " Time master device address: 0x%08" PRIX32 "", this->time_device_address_); } #endif @@ -33,7 +35,7 @@ void UponorSmatrixComponent::dump_config() { if (!this->unknown_devices_.empty()) { ESP_LOGCONFIG(TAG, " Detected unknown device addresses:"); for (auto device_address : this->unknown_devices_) { - ESP_LOGCONFIG(TAG, " 0x%08X", device_address); + ESP_LOGCONFIG(TAG, " 0x%08" PRIX32 "", device_address); } } } @@ -103,14 +105,14 @@ bool UponorSmatrixComponent::parse_byte_(uint8_t byte) { #if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE char hex_buf[format_hex_size(UPONOR_MAX_LOG_BYTES)]; #endif - ESP_LOGV(TAG, "Received packet: addr=%08X, data=%s, crc=%04X", device_address, + ESP_LOGV(TAG, "Received packet: addr=%08" PRIX32 ", data=%s, crc=%04X", device_address, format_hex_to(hex_buf, &packet[4], packet_len - 6), crc); // Handle packet size_t data_len = (packet_len - 6) / 3; if (data_len == 0) { if (packet[4] == UPONOR_ID_REQUEST) - ESP_LOGVV(TAG, "Ignoring request packet for device 0x%08X", device_address); + ESP_LOGVV(TAG, "Ignoring request packet for device 0x%08" PRIX32 "", device_address); return true; } @@ -135,7 +137,7 @@ bool UponorSmatrixComponent::parse_byte_(uint8_t byte) { if (data[i].id == UPONOR_ID_DATETIME1) found_time = true; if (found_temperature && found_time) { - ESP_LOGI(TAG, "Using detected time device address 0x%08X", device_address); + ESP_LOGI(TAG, "Using detected time device address 0x%08" PRIX32 "", device_address); this->time_device_address_ = device_address; break; } @@ -154,7 +156,7 @@ bool UponorSmatrixComponent::parse_byte_(uint8_t byte) { // Log unknown device addresses if (!found && !this->unknown_devices_.count(device_address)) { - ESP_LOGI(TAG, "Received packet for unknown device address 0x%08X ", device_address); + ESP_LOGI(TAG, "Received packet for unknown device address 0x%08" PRIX32 " ", device_address); this->unknown_devices_.insert(device_address); } diff --git a/esphome/components/uptime/text_sensor/uptime_text_sensor.cpp b/esphome/components/uptime/text_sensor/uptime_text_sensor.cpp index c89d23672e..7a56654804 100644 --- a/esphome/components/uptime/text_sensor/uptime_text_sensor.cpp +++ b/esphome/components/uptime/text_sensor/uptime_text_sensor.cpp @@ -70,7 +70,7 @@ void UptimeTextSensor::update() { if (show_seconds) append_unit(buf, sizeof(buf), pos, this->separator_, seconds, this->seconds_text_); - this->publish_state(buf); + this->publish_state(buf, pos); } float UptimeTextSensor::get_setup_priority() const { return setup_priority::HARDWARE; } diff --git a/esphome/components/usb_cdc_acm/usb_cdc_acm.cpp b/esphome/components/usb_cdc_acm/usb_cdc_acm.cpp index 253626f0a3..40f7f2e28b 100644 --- a/esphome/components/usb_cdc_acm/usb_cdc_acm.cpp +++ b/esphome/components/usb_cdc_acm/usb_cdc_acm.cpp @@ -29,10 +29,7 @@ void USBCDCACMInstance::queue_line_state_event(bool dtr, bool rts) { // Push always succeeds: pool is sized to queue capacity (SIZE-1), so if // allocate() returned non-null, the queue cannot be full. this->event_queue_.push(event); - -#if defined(USE_SOCKET_SELECT_SUPPORT) && defined(USE_WAKE_LOOP_THREADSAFE) App.wake_loop_threadsafe(); -#endif } void USBCDCACMInstance::queue_line_coding_event(uint32_t bit_rate, uint8_t stop_bits, uint8_t parity, @@ -53,10 +50,7 @@ void USBCDCACMInstance::queue_line_coding_event(uint32_t bit_rate, uint8_t stop_ // Push always succeeds: pool is sized to queue capacity (SIZE-1), so if // allocate() returned non-null, the queue cannot be full. this->event_queue_.push(event); - -#if defined(USE_SOCKET_SELECT_SUPPORT) && defined(USE_WAKE_LOOP_THREADSAFE) App.wake_loop_threadsafe(); -#endif } void USBCDCACMInstance::process_events_() { diff --git a/esphome/components/usb_host/__init__.py b/esphome/components/usb_host/__init__.py index 5eb0371e5c..338bd8d572 100644 --- a/esphome/components/usb_host/__init__.py +++ b/esphome/components/usb_host/__init__.py @@ -1,5 +1,4 @@ import esphome.codegen as cg -from esphome.components import socket from esphome.components.esp32 import ( VARIANT_ESP32P4, VARIANT_ESP32S2, @@ -14,7 +13,7 @@ from esphome.const import CONF_DEVICES, CONF_ID from esphome.cpp_types import Component from esphome.types import ConfigType -AUTO_LOAD = ["bytebuffer", "socket"] +AUTO_LOAD = ["bytebuffer"] CODEOWNERS = ["@clydebarrow"] DEPENDENCIES = ["esp32"] usb_host_ns = cg.esphome_ns.namespace("usb_host") @@ -76,11 +75,6 @@ async def to_code(config: ConfigType) -> None: max_requests = config[CONF_MAX_TRANSFER_REQUESTS] cg.add_define("USB_HOST_MAX_REQUESTS", max_requests) - # USB uses the socket wake_loop_threadsafe() mechanism to wake the main loop from USB task - # This enables low-latency (~12μs) USB event processing instead of waiting for - # select() timeout (0-16ms). The wake socket is shared across all components. - socket.require_wake_loop_threadsafe() - var = cg.new_Pvariable(config[CONF_ID]) await cg.register_component(var, config) for device in config.get(CONF_DEVICES) or (): diff --git a/esphome/components/usb_host/usb_host_client.cpp b/esphome/components/usb_host/usb_host_client.cpp index 18d938344c..c34c7ef67d 100644 --- a/esphome/components/usb_host/usb_host_client.cpp +++ b/esphome/components/usb_host/usb_host_client.cpp @@ -200,10 +200,8 @@ static void client_event_cb(const usb_host_client_event_msg_t *event_msg, void * // Re-enable component loop to process the queued event client->enable_loop_soon_any_context(); - // Wake main loop immediately to process USB event instead of waiting for select() timeout -#if defined(USE_SOCKET_SELECT_SUPPORT) && defined(USE_WAKE_LOOP_THREADSAFE) + // Wake main loop immediately to process USB event App.wake_loop_threadsafe(); -#endif } void USBClient::setup() { usb_host_client_config_t config{.is_synchronous = false, diff --git a/esphome/components/usb_uart/__init__.py b/esphome/components/usb_uart/__init__.py index 2d85723d72..0e8994a3ed 100644 --- a/esphome/components/usb_uart/__init__.py +++ b/esphome/components/usb_uart/__init__.py @@ -1,5 +1,4 @@ import esphome.codegen as cg -from esphome.components import socket from esphome.components.const import CONF_DATA_BITS, CONF_PARITY, CONF_STOP_BITS from esphome.components.uart import CONF_DEBUG_PREFIX, CONF_FLUSH_TIMEOUT, UARTComponent from esphome.components.usb_host import register_usb_client, usb_device_schema @@ -14,7 +13,7 @@ from esphome.const import ( ) from esphome.cpp_types import Component -AUTO_LOAD = ["uart", "usb_host", "bytebuffer", "socket"] +AUTO_LOAD = ["uart", "usb_host", "bytebuffer"] CODEOWNERS = ["@clydebarrow"] usb_uart_ns = cg.esphome_ns.namespace("usb_uart") @@ -117,10 +116,6 @@ CONFIG_SCHEMA = cv.ensure_list( async def to_code(config): - # Enable wake_loop_threadsafe for low-latency USB data processing - # The USB task queues data events that need immediate processing - socket.require_wake_loop_threadsafe() - for device in config: var = await register_usb_client(device) for index, channel in enumerate(device[CONF_CHANNELS]): diff --git a/esphome/components/usb_uart/usb_uart.cpp b/esphome/components/usb_uart/usb_uart.cpp index 0b8589f671..30ec61fdc4 100644 --- a/esphome/components/usb_uart/usb_uart.cpp +++ b/esphome/components/usb_uart/usb_uart.cpp @@ -325,10 +325,8 @@ void USBUartComponent::start_input(USBUartChannel *channel) { // Re-enable component loop to process the queued data this->enable_loop_soon_any_context(); - // Wake main loop immediately to process USB data instead of waiting for select() timeout -#if defined(USE_SOCKET_SELECT_SUPPORT) && defined(USE_WAKE_LOOP_THREADSAFE) + // Wake main loop immediately to process USB data App.wake_loop_threadsafe(); -#endif } // On success, restart input immediately from USB task for performance diff --git a/esphome/components/usb_uart/usb_uart.h b/esphome/components/usb_uart/usb_uart.h index 8a47f0cf4b..8e8e65032d 100644 --- a/esphome/components/usb_uart/usb_uart.h +++ b/esphome/components/usb_uart/usb_uart.h @@ -140,6 +140,7 @@ class USBUartChannel : public uart::UARTComponent, public Parentedinput_buffer_.get_available(); } + bool is_connected() override { return this->initialised_.load(); } uart::UARTFlushResult flush() override; void check_logger_conflict() override {} void set_parity(UARTParityOptions parity) { this->parity_ = parity; } diff --git a/esphome/components/version/version_text_sensor.cpp b/esphome/components/version/version_text_sensor.cpp index 8aec98d2da..34c7aae6bc 100644 --- a/esphome/components/version/version_text_sensor.cpp +++ b/esphome/components/version/version_text_sensor.cpp @@ -1,6 +1,5 @@ #include "version_text_sensor.h" #include "esphome/core/application.h" -#include "esphome/core/build_info_data.h" #include "esphome/core/helpers.h" #include "esphome/core/log.h" #include "esphome/core/progmem.h" @@ -36,7 +35,9 @@ void VersionTextSensor::setup() { if (!this->hide_timestamp_) { size_t len = strlen(version_str); ESPHOME_strncat_P(version_str, BUILT_STR, sizeof(version_str) - len - 1); - ESPHOME_strncat_P(version_str, ESPHOME_BUILD_TIME_STR, sizeof(version_str) - strlen(version_str) - 1); + char build_time_buf[Application::BUILD_TIME_STR_SIZE]; + App.get_build_time_string(build_time_buf); + strncat(version_str, build_time_buf, sizeof(version_str) - strlen(version_str) - 1); } // The closing parenthesis is part of the config-hash suffix and must diff --git a/esphome/components/vl53l0x/vl53l0x_sensor.cpp b/esphome/components/vl53l0x/vl53l0x_sensor.cpp index 8a76ed7760..58b5a42675 100644 --- a/esphome/components/vl53l0x/vl53l0x_sensor.cpp +++ b/esphome/components/vl53l0x/vl53l0x_sensor.cpp @@ -1,6 +1,8 @@ #include "vl53l0x_sensor.h" #include "esphome/core/log.h" +#include + /* * Most of the code in this integration is based on the VL53L0x library * by Pololu (Pololu Corporation), which in turn is based on the VL53L0X @@ -28,8 +30,8 @@ void VL53L0XSensor::dump_config() { LOG_PIN(" Enable Pin: ", this->enable_pin_); } ESP_LOGCONFIG(TAG, - " Timeout: %u%s\n" - " Timing Budget %uus ", + " Timeout: %" PRIu32 "%s\n" + " Timing Budget %" PRIu32 "us ", this->timeout_us_, this->timeout_us_ > 0 ? "us" : " (no timeout)", this->measurement_timing_budget_us_); } diff --git a/esphome/components/voice_assistant/voice_assistant.cpp b/esphome/components/voice_assistant/voice_assistant.cpp index 15124e422f..ddce606b2c 100644 --- a/esphome/components/voice_assistant/voice_assistant.cpp +++ b/esphome/components/voice_assistant/voice_assistant.cpp @@ -39,8 +39,8 @@ void VoiceAssistant::setup() { #ifdef USE_MEDIA_PLAYER if (this->media_player_ != nullptr) { - this->media_player_->add_on_state_callback([this]() { - switch (this->media_player_->state) { + this->media_player_->add_on_state_callback([this](media_player::MediaPlayerState state) { + switch (state) { case media_player::MediaPlayerState::MEDIA_PLAYER_STATE_ANNOUNCING: if (this->media_player_response_state_ == MediaPlayerResponseState::URL_SENT) { // State changed to announcing after receiving the url diff --git a/esphome/components/water_heater/water_heater.cpp b/esphome/components/water_heater/water_heater.cpp index 9a74877f0a..9ee8faadee 100644 --- a/esphome/components/water_heater/water_heater.cpp +++ b/esphome/components/water_heater/water_heater.cpp @@ -1,5 +1,7 @@ #include "water_heater.h" #include "esphome/core/log.h" + +#include #include "esphome/core/application.h" #include "esphome/core/controller_registry.h" #include "esphome/core/progmem.h" @@ -110,7 +112,8 @@ void WaterHeaterCall::validate_() { auto traits = this->parent_->get_traits(); if (this->mode_.has_value()) { if (!traits.supports_mode(*this->mode_)) { - ESP_LOGW(TAG, "'%s' - Mode %d not supported", this->parent_->get_name().c_str(), *this->mode_); + ESP_LOGW(TAG, "'%s' - Mode %" PRIu32 " not supported", this->parent_->get_name().c_str(), + static_cast(*this->mode_)); this->mode_.reset(); } } diff --git a/esphome/components/web_server/web_server.cpp b/esphome/components/web_server/web_server.cpp index 1dda6204fe..a57a8d26ff 100644 --- a/esphome/components/web_server/web_server.cpp +++ b/esphome/components/web_server/web_server.cpp @@ -286,10 +286,11 @@ void DeferredUpdateEventSource::try_send_nodefer(const char *message, const char this->send(message, event, id, reconnect); } -void DeferredUpdateEventSourceList::loop() { +bool DeferredUpdateEventSourceList::loop() { for (DeferredUpdateEventSource *dues : *this) { dues->loop(); } + return !this->empty(); } void DeferredUpdateEventSourceList::deferrable_send_state(void *source, const char *event_type, @@ -318,6 +319,7 @@ void DeferredUpdateEventSourceList::add_new_client(WebServer *ws, AsyncWebServer es->onDisconnect([this, es](AsyncEventSourceClient *client) { this->on_client_disconnect_(es); }); es->handleRequest(request); + ws->enable_loop_soon_any_context(); } void DeferredUpdateEventSourceList::on_client_connect_(DeferredUpdateEventSource *source) { @@ -413,13 +415,24 @@ void WebServer::setup() { // doesn't need defer functionality - if the queue is full, the client JS knows it's alive because it's clearly // getting a lot of events this->set_interval(10000, [this]() { + if (this->events_.empty()) + return; char buf[32]; auto uptime = static_cast(millis_64() / 1000); buf_append_printf(buf, sizeof(buf), 0, "{\"uptime\":%" PRIu32 "}", uptime); this->events_.try_send_nodefer(buf, "ping", millis(), 30000); }); } -void WebServer::loop() { this->events_.loop(); } +void WebServer::loop() { + // No SSE clients connected; stop looping until a new client connects via + // enable_loop_soon_any_context(). This is safe because: + // - set_interval/set_timeout/defer run via the Scheduler, independent of loop() + // - deferrable_send_state early-outs when no clients are connected + // - try_send_nodefer (log, ping) iterates sessions which are empty + // - REST API handlers use defer() which runs via the Scheduler + if (!this->events_.loop()) + this->disable_loop(); +} #ifdef USE_LOGGER void WebServer::on_log(uint8_t level, const char *tag, const char *message, size_t message_len) { diff --git a/esphome/components/web_server/web_server.h b/esphome/components/web_server/web_server.h index 6152dfbfd3..8e8b1de8c4 100644 --- a/esphome/components/web_server/web_server.h +++ b/esphome/components/web_server/web_server.h @@ -169,7 +169,8 @@ class DeferredUpdateEventSourceList final : public std::list handlers_; diff --git a/esphome/components/web_server_idf/web_server_idf.cpp b/esphome/components/web_server_idf/web_server_idf.cpp index 38ccfccb76..8f464ae912 100644 --- a/esphome/components/web_server_idf/web_server_idf.cpp +++ b/esphome/components/web_server_idf/web_server_idf.cpp @@ -484,9 +484,12 @@ void AsyncEventSource::handleRequest(AsyncWebServerRequest *request) { this->on_connect_(rsp); } this->sessions_.push_back(rsp); + // Wake up WebServer::loop() to drain deferred event queues for this client. + // Safe from httpd task context via the pending_enable_loop_ flag. + this->web_server_->enable_loop_soon_any_context(); } -void AsyncEventSource::loop() { +bool AsyncEventSource::loop() { // Clean up dead sessions safely // This follows the ESP-IDF pattern where free_ctx marks resources as dead // and the main loop handles the actual cleanup to avoid race conditions @@ -504,6 +507,7 @@ void AsyncEventSource::loop() { ++i; } } + return !this->sessions_.empty(); } void AsyncEventSource::try_send_nodefer(const char *message, const char *event, uint32_t id, uint32_t reconnect) { diff --git a/esphome/components/web_server_idf/web_server_idf.h b/esphome/components/web_server_idf/web_server_idf.h index 81683e8d85..f2931fb507 100644 --- a/esphome/components/web_server_idf/web_server_idf.h +++ b/esphome/components/web_server_idf/web_server_idf.h @@ -340,7 +340,8 @@ class AsyncEventSource : public AsyncWebHandler { void try_send_nodefer(const char *message, const char *event = nullptr, uint32_t id = 0, uint32_t reconnect = 0); void deferrable_send_state(void *source, const char *event_type, message_generator_t *message_generator); - void loop(); + /// Returns true if there are sessions remaining (including pending cleanup). + bool loop(); bool empty() { return this->count() == 0; } size_t count() const { return this->sessions_.size(); } diff --git a/esphome/components/wiegand/__init__.py b/esphome/components/wiegand/__init__.py index 962ac4c373..36ec7bd43f 100644 --- a/esphome/components/wiegand/__init__.py +++ b/esphome/components/wiegand/__init__.py @@ -48,7 +48,7 @@ CONFIG_SCHEMA = cv.Schema( } ), } -) +).extend(cv.COMPONENT_SCHEMA) async def to_code(config): diff --git a/esphome/components/wifi/wifi_component.cpp b/esphome/components/wifi/wifi_component.cpp index 620d1a083d..7b31a22ed5 100644 --- a/esphome/components/wifi/wifi_component.cpp +++ b/esphome/components/wifi/wifi_component.cpp @@ -784,7 +784,8 @@ void WiFiComponent::loop() { } case WIFI_COMPONENT_STATE_STA_CONNECTED: { - if (!this->is_connected_()) { + // Use cached connected_ set unconditionally at the top of loop() + if (!this->connected_) { ESP_LOGW(TAG, "Connection lost; reconnecting"); this->state_ = WIFI_COMPONENT_STATE_STA_CONNECTING; this->retry_connect(); @@ -970,12 +971,6 @@ void WiFiComponent::set_ap(const WiFiAP &ap) { } #endif // USE_WIFI_AP -#ifdef USE_LOOP_PRIORITY -float WiFiComponent::get_loop_priority() const { - return 10.0f; // before other loop components -} -#endif - void WiFiComponent::init_sta(size_t count) { this->sta_.init(count); } void WiFiComponent::add_sta(const WiFiAP &ap) { this->sta_.push_back(ap); } void WiFiComponent::clear_sta() { @@ -2135,11 +2130,6 @@ void WiFiComponent::retry_connect() { } void WiFiComponent::set_reboot_timeout(uint32_t reboot_timeout) { this->reboot_timeout_ = reboot_timeout; } -bool WiFiComponent::is_connected_() const { - return this->state_ == WIFI_COMPONENT_STATE_STA_CONNECTED && - this->wifi_sta_connect_status_() == WiFiSTAConnectStatus::CONNECTED && !this->error_from_callback_; -} -void WiFiComponent::update_connected_state_() { this->connected_ = this->is_connected_(); } void WiFiComponent::set_power_save_mode(WiFiPowerSaveMode power_save) { this->power_save_ = power_save; #if defined(USE_ESP32) && defined(USE_WIFI_RUNTIME_POWER_SAVE) diff --git a/esphome/components/wifi/wifi_component.h b/esphome/components/wifi/wifi_component.h index 55e532c37d..665dec37d5 100644 --- a/esphome/components/wifi/wifi_component.h +++ b/esphome/components/wifi/wifi_component.h @@ -6,6 +6,9 @@ #include "esphome/core/automation.h" #include "esphome/core/component.h" #include "esphome/core/helpers.h" +#ifdef USE_ESP32 +#include "esphome/core/lock_free_queue.h" +#endif #include "esphome/core/string_ref.h" #include @@ -463,10 +466,6 @@ class WiFiComponent final : public Component { void restart_adapter(); /// WIFI setup_priority. float get_setup_priority() const override; -#ifdef USE_LOOP_PRIORITY - float get_loop_priority() const override; -#endif - /// Reconnect WiFi if required. void loop() override; @@ -674,8 +673,11 @@ class WiFiComponent final : public Component { bool wifi_sta_connect_(const WiFiAP &ap); void wifi_pre_setup_(); WiFiSTAConnectStatus wifi_sta_connect_status_() const; - bool is_connected_() const; - void update_connected_state_(); + bool is_connected_() const { + return this->state_ == WIFI_COMPONENT_STATE_STA_CONNECTED && + this->wifi_sta_connect_status_() == WiFiSTAConnectStatus::CONNECTED && !this->error_from_callback_; + } + void update_connected_state_() { this->connected_ = this->is_connected_(); } bool wifi_scan_start_(bool passive); #ifdef USE_WIFI_AP @@ -728,6 +730,7 @@ class WiFiComponent final : public Component { #ifdef USE_ESP32 void wifi_process_event_(IDFWiFiEvent *data); + friend void event_handler(void *arg, esp_event_base_t event_base, int32_t event_id, void *event_data); #endif #ifdef USE_RP2040 @@ -815,6 +818,12 @@ class WiFiComponent final : public Component { uint8_t num_ipv6_addresses_{0}; #endif /* USE_NETWORK_IPV6 */ bool error_from_callback_{false}; +#if defined(USE_ESP8266) || defined(USE_LIBRETINY) + // Platform-specific STA state enum, defined in platform cpp file. + // On ESP8266, written from SDK system context (wifi_event_callback) — + // uint8_t writes are atomic on Xtensa LX106 so no synchronization is needed. + uint8_t sta_state_{0}; +#endif RetryHiddenMode retry_hidden_mode_{RetryHiddenMode::BLIND_RETRY}; RoamingState roaming_state_{RoamingState::IDLE}; bssid_t roaming_target_bssid_{}; // BSSID of the AP we're trying to roam to @@ -866,6 +875,13 @@ class WiFiComponent final : public Component { bool is_high_performance_mode_{false}; #endif +#ifdef USE_ESP32 + // Lock-free SPSC queue for WiFi events from ESP-IDF event handler. + // 17 slots = 16 usable (ring buffer reserves one slot). WiFi events are rare. + // Placed at end of class to avoid padding between smaller fields. + LockFreeQueue event_queue_; +#endif + private: // Stores a pointer to a string literal (static storage duration). // ONLY set from Python-generated code with string literals - never dynamic strings. diff --git a/esphome/components/wifi/wifi_component_esp8266.cpp b/esphome/components/wifi/wifi_component_esp8266.cpp index 03800cc3a9..cb53d3ac1b 100644 --- a/esphome/components/wifi/wifi_component_esp8266.cpp +++ b/esphome/components/wifi/wifi_component_esp8266.cpp @@ -44,11 +44,14 @@ namespace esphome::wifi { static const char *const TAG = "wifi_esp8266"; -static bool s_sta_connected = false; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) -static bool s_sta_got_ip = false; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) -static bool s_sta_connect_not_found = false; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) -static bool s_sta_connect_error = false; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) -static bool s_sta_connecting = false; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) +enum class ESP8266WiFiSTAState : uint8_t { + IDLE, // Not connecting + CONNECTING, // Connection in progress + ASSOCIATED, // Associated to AP, waiting for IP + CONNECTED, // Successfully connected with IP + ERROR_NOT_FOUND, // AP not found (probe failed) + ERROR_FAILED, // Connection failed (auth, timeout, etc.) +}; bool WiFiComponent::wifi_mode_(optional sta, optional ap) { uint8_t current_mode = wifi_get_opmode(); @@ -359,11 +362,7 @@ bool WiFiComponent::wifi_sta_connect_(const WiFiAP &ap) { // Reset flags, do this _before_ wifi_station_connect as the callback method // may be called from wifi_station_connect - s_sta_connecting = true; - s_sta_connected = false; - s_sta_got_ip = false; - s_sta_connect_error = false; - s_sta_connect_not_found = false; + this->sta_state_ = static_cast(ESP8266WiFiSTAState::CONNECTING); ETS_UART_INTR_DISABLE(); ret = wifi_station_connect(); @@ -493,7 +492,7 @@ void WiFiComponent::wifi_event_callback(System_Event_t *event) { ESP_LOGV(TAG, "Connected ssid='%.*s' bssid=%s channel=%u", it.ssid_len, (const char *) it.ssid, bssid_buf, it.channel); #endif - s_sta_connected = true; + global_wifi_component->sta_state_ = static_cast(ESP8266WiFiSTAState::ASSOCIATED); #ifdef USE_WIFI_CONNECT_STATE_LISTENERS // Defer listener notification until state machine reaches STA_CONNECTED // This ensures wifi.connected condition returns true in listener automations @@ -506,16 +505,14 @@ void WiFiComponent::wifi_event_callback(System_Event_t *event) { if (it.reason == REASON_NO_AP_FOUND) { ESP_LOGW(TAG, "Disconnected ssid='%.*s' reason='Probe Request Unsuccessful'", it.ssid_len, (const char *) it.ssid); - s_sta_connect_not_found = true; + global_wifi_component->sta_state_ = static_cast(ESP8266WiFiSTAState::ERROR_NOT_FOUND); } else { char bssid_s[18]; format_mac_addr_upper(it.bssid, bssid_s); ESP_LOGW(TAG, "Disconnected ssid='%.*s' bssid=" LOG_SECRET("%s") " reason='%s'", it.ssid_len, (const char *) it.ssid, bssid_s, LOG_STR_ARG(get_disconnect_reason_str(it.reason))); - s_sta_connect_error = true; + global_wifi_component->sta_state_ = static_cast(ESP8266WiFiSTAState::ERROR_FAILED); } - s_sta_connected = false; - s_sta_connecting = false; global_wifi_component->error_from_callback_ = true; #ifdef USE_WIFI_CONNECT_STATE_LISTENERS global_wifi_component->pending_.disconnect = true; @@ -541,7 +538,7 @@ void WiFiComponent::wifi_event_callback(System_Event_t *event) { mask_buf[network::IP_ADDRESS_BUFFER_SIZE]; ESP_LOGV(TAG, "static_ip=%s gateway=%s netmask=%s", network::IPAddress(&it.ip).str_to(ip_buf), network::IPAddress(&it.gw).str_to(gw_buf), network::IPAddress(&it.mask).str_to(mask_buf)); - s_sta_got_ip = true; + global_wifi_component->sta_state_ = static_cast(ESP8266WiFiSTAState::CONNECTED); #ifdef USE_WIFI_IP_STATE_LISTENERS // Defer listener callbacks to main loop - system context has limited stack global_wifi_component->pending_.got_ip = true; @@ -636,17 +633,22 @@ void WiFiComponent::wifi_pre_setup_() { } WiFiSTAConnectStatus WiFiComponent::wifi_sta_connect_status_() const { - station_status_t status = wifi_station_get_connect_status(); - if (status == STATION_GOT_IP) + // Use cached state from wifi_event_callback() instead of calling + // wifi_station_get_connect_status() which queries the SDK every time. + // Use if statements with early returns instead of switch to avoid GCC + // generating a CSWTCH lookup table in .rodata (flash) on ESP8266. + auto state = static_cast(this->sta_state_); + if (state == ESP8266WiFiSTAState::CONNECTED) return WiFiSTAConnectStatus::CONNECTED; - if (status == STATION_NO_AP_FOUND) + if (state == ESP8266WiFiSTAState::ERROR_NOT_FOUND) return WiFiSTAConnectStatus::ERROR_NETWORK_NOT_FOUND; - if (status == STATION_CONNECT_FAIL || status == STATION_WRONG_PASSWORD) + if (state == ESP8266WiFiSTAState::ERROR_FAILED) return WiFiSTAConnectStatus::ERROR_CONNECT_FAILED; - if (status == STATION_CONNECTING) + if (state == ESP8266WiFiSTAState::CONNECTING || state == ESP8266WiFiSTAState::ASSOCIATED) return WiFiSTAConnectStatus::CONNECTING; return WiFiSTAConnectStatus::IDLE; } + bool WiFiComponent::wifi_scan_start_(bool passive) { // enable STA if (!this->wifi_mode_(true, {})) diff --git a/esphome/components/wifi/wifi_component_esp_idf.cpp b/esphome/components/wifi/wifi_component_esp_idf.cpp index d8b3db9667..4097df80af 100644 --- a/esphome/components/wifi/wifi_component_esp_idf.cpp +++ b/esphome/components/wifi/wifi_component_esp_idf.cpp @@ -47,7 +47,6 @@ namespace esphome::wifi { static const char *const TAG = "wifi_esp32"; static EventGroupHandle_t s_wifi_event_group; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) -static QueueHandle_t s_event_queue; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) static esp_netif_t *s_sta_netif = nullptr; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) #ifdef USE_WIFI_AP static esp_netif_t *s_ap_netif = nullptr; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) @@ -132,11 +131,10 @@ void event_handler(void *arg, esp_event_base_t event_base, int32_t event_id, voi return; } - // copy to heap to keep queue object small + // copy to heap — WiFi events are rare so heap alloc is fine auto *to_send = new IDFWiFiEvent; // NOLINT(cppcoreguidelines-owning-memory) memcpy(to_send, &event, sizeof(IDFWiFiEvent)); - // don't block, we may miss events but the core can handle that - if (xQueueSend(s_event_queue, &to_send, 0L) != pdPASS) { + if (!global_wifi_component->event_queue_.push(to_send)) { delete to_send; // NOLINT(cppcoreguidelines-owning-memory) } } @@ -157,12 +155,6 @@ void WiFiComponent::wifi_pre_setup_() { ESP_LOGE(TAG, "xEventGroupCreate failed"); return; } - // NOLINTNEXTLINE(bugprone-sizeof-expression) - s_event_queue = xQueueCreate(64, sizeof(IDFWiFiEvent *)); - if (s_event_queue == nullptr) { - ESP_LOGE(TAG, "xQueueCreate failed"); - return; - } err = esp_event_loop_create_default(); if (err != ERR_OK) { ESP_LOGE(TAG, "esp_event_loop_create_default failed: %s", esp_err_to_name(err)); @@ -724,16 +716,14 @@ const char *get_disconnect_reason_str(uint8_t reason) { } void WiFiComponent::wifi_loop_() { - while (true) { - IDFWiFiEvent *data; - if (xQueueReceive(s_event_queue, &data, 0L) != pdTRUE) { - // no event ready - break; - } + uint16_t dropped = this->event_queue_.get_and_reset_dropped_count(); + if (dropped > 0) { + ESP_LOGW(TAG, "Dropped %u WiFi events due to buffer overflow", dropped); + } - // process event + IDFWiFiEvent *data; + while ((data = this->event_queue_.pop()) != nullptr) { wifi_process_event_(data); - delete data; // NOLINT(cppcoreguidelines-owning-memory) } } diff --git a/esphome/components/wifi/wifi_component_libretiny.cpp b/esphome/components/wifi/wifi_component_libretiny.cpp index b049a0413c..9565ffa747 100644 --- a/esphome/components/wifi/wifi_component_libretiny.cpp +++ b/esphome/components/wifi/wifi_component_libretiny.cpp @@ -97,8 +97,6 @@ enum class LTWiFiSTAState : uint8_t { ERROR_FAILED, // Connection failed (auth, timeout, etc.) }; -static LTWiFiSTAState s_sta_state = LTWiFiSTAState::IDLE; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) - // Count of ignored disconnect events during connection - too many indicates real failure static uint8_t s_ignored_disconnect_count = 0; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) // Threshold for ignored disconnect events before treating as connection failure @@ -223,7 +221,7 @@ bool WiFiComponent::wifi_sta_connect_(const WiFiAP &ap) { this->wifi_apply_hostname_(); // Reset state machine and disconnect counter before connecting - s_sta_state = LTWiFiSTAState::CONNECTING; + this->sta_state_ = static_cast(LTWiFiSTAState::CONNECTING); s_ignored_disconnect_count = 0; WiFiStatus status = WiFi.begin(ap.ssid_.c_str(), ap.password_.empty() ? NULL : ap.password_.c_str(), @@ -459,7 +457,7 @@ void WiFiComponent::wifi_process_event_(LTWiFiEvent *event) { } case ESPHOME_EVENT_ID_WIFI_STA_STOP: { ESP_LOGV(TAG, "STA stop"); - s_sta_state = LTWiFiSTAState::IDLE; + this->sta_state_ = static_cast(LTWiFiSTAState::IDLE); break; } case ESPHOME_EVENT_ID_WIFI_STA_CONNECTED: { @@ -479,7 +477,7 @@ void WiFiComponent::wifi_process_event_(LTWiFiEvent *event) { // For static IP configurations, GOT_IP event may not fire, so set connected state here #ifdef USE_WIFI_MANUAL_IP if (const WiFiAP *config = this->get_selected_sta_(); config && config->get_manual_ip().has_value()) { - s_sta_state = LTWiFiSTAState::CONNECTED; + this->sta_state_ = static_cast(LTWiFiSTAState::CONNECTED); #ifdef USE_WIFI_IP_STATE_LISTENERS this->notify_ip_state_listeners_(); #endif @@ -501,12 +499,13 @@ void WiFiComponent::wifi_process_event_(LTWiFiEvent *event) { // Only ignore benign reasons - real failures like NO_AP_FOUND should still be processed. // However, if we get too many of these events (IGNORED_DISCONNECT_THRESHOLD), treat it // as a real connection failure to avoid waiting the full timeout for a failing connection. - if (it.ssid_len == 0 && s_sta_state == LTWiFiSTAState::CONNECTING && it.reason != WIFI_REASON_NO_AP_FOUND) { + if (it.ssid_len == 0 && this->sta_state_ == static_cast(LTWiFiSTAState::CONNECTING) && + it.reason != WIFI_REASON_NO_AP_FOUND) { s_ignored_disconnect_count++; if (s_ignored_disconnect_count >= IGNORED_DISCONNECT_THRESHOLD) { ESP_LOGW(TAG, "Too many disconnect events (%u) while connecting, treating as failure (reason=%s)", s_ignored_disconnect_count, get_disconnect_reason_str(it.reason)); - s_sta_state = LTWiFiSTAState::ERROR_FAILED; + this->sta_state_ = static_cast(LTWiFiSTAState::ERROR_FAILED); WiFi.disconnect(); this->error_from_callback_ = true; // Don't break - fall through to notify listeners @@ -520,13 +519,13 @@ void WiFiComponent::wifi_process_event_(LTWiFiEvent *event) { if (it.reason == WIFI_REASON_NO_AP_FOUND) { ESP_LOGW(TAG, "Disconnected ssid='%.*s' reason='Probe Request Unsuccessful'", it.ssid_len, (const char *) it.ssid); - s_sta_state = LTWiFiSTAState::ERROR_NOT_FOUND; + this->sta_state_ = static_cast(LTWiFiSTAState::ERROR_NOT_FOUND); } else { char bssid_s[MAC_ADDRESS_PRETTY_BUFFER_SIZE]; format_mac_addr_upper(it.bssid, bssid_s); ESP_LOGW(TAG, "Disconnected ssid='%.*s' bssid=" LOG_SECRET("%s") " reason='%s'", it.ssid_len, (const char *) it.ssid, bssid_s, get_disconnect_reason_str(it.reason)); - s_sta_state = LTWiFiSTAState::ERROR_FAILED; + this->sta_state_ = static_cast(LTWiFiSTAState::ERROR_FAILED); } uint8_t reason = it.reason; @@ -551,7 +550,7 @@ void WiFiComponent::wifi_process_event_(LTWiFiEvent *event) { ESP_LOGW(TAG, "Potential Authmode downgrade detected, disconnecting"); WiFi.disconnect(); this->error_from_callback_ = true; - s_sta_state = LTWiFiSTAState::ERROR_FAILED; + this->sta_state_ = static_cast(LTWiFiSTAState::ERROR_FAILED); } break; } @@ -559,7 +558,7 @@ void WiFiComponent::wifi_process_event_(LTWiFiEvent *event) { char ip_buf[network::IP_ADDRESS_BUFFER_SIZE], gw_buf[network::IP_ADDRESS_BUFFER_SIZE]; ESP_LOGV(TAG, "static_ip=%s gateway=%s", network::IPAddress(WiFi.localIP()).str_to(ip_buf), network::IPAddress(WiFi.gatewayIP()).str_to(gw_buf)); - s_sta_state = LTWiFiSTAState::CONNECTED; + this->sta_state_ = static_cast(LTWiFiSTAState::CONNECTED); #ifdef USE_WIFI_IP_STATE_LISTENERS this->notify_ip_state_listeners_(); #endif @@ -637,7 +636,7 @@ void WiFiComponent::wifi_pre_setup_() { WiFiSTAConnectStatus WiFiComponent::wifi_sta_connect_status_() const { // Use state machine instead of querying WiFi.status() directly // State is updated in main loop from queued events, ensuring thread safety - switch (s_sta_state) { + switch (static_cast(this->sta_state_)) { case LTWiFiSTAState::CONNECTED: return WiFiSTAConnectStatus::CONNECTED; case LTWiFiSTAState::ERROR_NOT_FOUND: @@ -758,7 +757,7 @@ network::IPAddress WiFiComponent::wifi_soft_ap_ip() { return {WiFi.softAPIP()}; bool WiFiComponent::wifi_disconnect_() { // Reset state first so disconnect events aren't ignored // and wifi_sta_connect_status_() returns IDLE instead of CONNECTING - s_sta_state = LTWiFiSTAState::IDLE; + this->sta_state_ = static_cast(LTWiFiSTAState::IDLE); return WiFi.disconnect(); } diff --git a/esphome/components/xiaomi_cgpr1/binary_sensor.py b/esphome/components/xiaomi_cgpr1/binary_sensor.py index 2f71a11b28..0606c93dbe 100644 --- a/esphome/components/xiaomi_cgpr1/binary_sensor.py +++ b/esphome/components/xiaomi_cgpr1/binary_sensor.py @@ -12,6 +12,7 @@ from esphome.const import ( DEVICE_CLASS_MOTION, ENTITY_CATEGORY_DIAGNOSTIC, ICON_TIMELAPSE, + STATE_CLASS_MEASUREMENT, UNIT_LUX, UNIT_MINUTE, UNIT_PERCENT, @@ -38,6 +39,7 @@ CONFIG_SCHEMA = cv.All( unit_of_measurement=UNIT_PERCENT, accuracy_decimals=0, device_class=DEVICE_CLASS_BATTERY, + state_class=STATE_CLASS_MEASUREMENT, entity_category=ENTITY_CATEGORY_DIAGNOSTIC, ), cv.Optional(CONF_IDLE_TIME): sensor.sensor_schema( @@ -45,11 +47,13 @@ CONFIG_SCHEMA = cv.All( icon=ICON_TIMELAPSE, accuracy_decimals=0, entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional(CONF_ILLUMINANCE): sensor.sensor_schema( unit_of_measurement=UNIT_LUX, accuracy_decimals=0, device_class=DEVICE_CLASS_ILLUMINANCE, + state_class=STATE_CLASS_MEASUREMENT, ), } ) diff --git a/esphome/components/xiaomi_miscale/sensor.py b/esphome/components/xiaomi_miscale/sensor.py index 4aa8d029f8..14e5c1d376 100644 --- a/esphome/components/xiaomi_miscale/sensor.py +++ b/esphome/components/xiaomi_miscale/sensor.py @@ -7,6 +7,7 @@ from esphome.const import ( CONF_IMPEDANCE, CONF_MAC_ADDRESS, CONF_WEIGHT, + DEVICE_CLASS_WEIGHT, ICON_OMEGA, ICON_SCALE_BATHROOM, STATE_CLASS_MEASUREMENT, @@ -31,6 +32,7 @@ CONFIG_SCHEMA = ( unit_of_measurement=UNIT_KILOGRAM, icon=ICON_SCALE_BATHROOM, accuracy_decimals=2, + device_class=DEVICE_CLASS_WEIGHT, state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional(CONF_IMPEDANCE): sensor.sensor_schema( diff --git a/esphome/components/xiaomi_mjyd02yla/binary_sensor.py b/esphome/components/xiaomi_mjyd02yla/binary_sensor.py index 312f8b43b1..312abc82cb 100644 --- a/esphome/components/xiaomi_mjyd02yla/binary_sensor.py +++ b/esphome/components/xiaomi_mjyd02yla/binary_sensor.py @@ -43,6 +43,7 @@ CONFIG_SCHEMA = cv.All( unit_of_measurement=UNIT_MINUTE, icon=ICON_TIMELAPSE, accuracy_decimals=0, + state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional(CONF_BATTERY_LEVEL): sensor.sensor_schema( unit_of_measurement=UNIT_PERCENT, diff --git a/esphome/components/zigbee/zigbee_zephyr.cpp b/esphome/components/zigbee/zigbee_zephyr.cpp index c103363b4a..047c30300e 100644 --- a/esphome/components/zigbee/zigbee_zephyr.cpp +++ b/esphome/components/zigbee/zigbee_zephyr.cpp @@ -255,26 +255,25 @@ void ZigbeeComponent::factory_reset() { ZB_SCHEDULE_APP_CALLBACK(zb_bdb_reset_via_local_action, 0); } +static void log_reporting_info(zb_zcl_reporting_info_t *rep_info) { + auto now = millis(); + ESP_LOGD(TAG, "Reporting: endpoint %d, cluster_id 0x%04X, attr_id 0x%04X, flags 0x%02X, report in %ums", rep_info->ep, + rep_info->cluster_id, rep_info->attr_id, rep_info->flags, + ZB_ZCL_GET_REPORTING_FLAG(rep_info, ZB_ZCL_REPORT_TIMER_STARTED) + ? ZB_TIME_BEACON_INTERVAL_TO_MSEC(rep_info->run_time) - now + : 0); + ESP_LOGD(TAG, " min_interval %ds, max_interval %ds, def_min_interval %ds, def_max_interval %ds", + rep_info->u.send_info.min_interval, rep_info->u.send_info.max_interval, + rep_info->u.send_info.def_min_interval, rep_info->u.send_info.def_max_interval); +} + void ZigbeeComponent::dump_reporting_() { #ifdef ESPHOME_LOG_HAS_VERBOSE - auto now = millis(); - bool first = true; for (zb_uint8_t j = 0; j < ZCL_CTX().device_ctx->ep_count; j++) { if (ZCL_CTX().device_ctx->ep_desc_list[j]->reporting_info) { zb_zcl_reporting_info_t *rep_info = ZCL_CTX().device_ctx->ep_desc_list[j]->reporting_info; for (zb_uint8_t i = 0; i < ZCL_CTX().device_ctx->ep_desc_list[j]->rep_info_count; i++) { - if (!first) { - ESP_LOGV(TAG, ""); - } - first = false; - ESP_LOGV(TAG, "Endpoint: %d, cluster_id %d, attr_id %d, flags %d, report in %ums", rep_info->ep, - rep_info->cluster_id, rep_info->attr_id, rep_info->flags, - ZB_ZCL_GET_REPORTING_FLAG(rep_info, ZB_ZCL_REPORT_TIMER_STARTED) - ? ZB_TIME_BEACON_INTERVAL_TO_MSEC(rep_info->run_time) - now - : 0); - ESP_LOGV(TAG, "Min_interval %ds, max_interval %ds, def_min_interval %ds, def_max_interval %ds", - rep_info->u.send_info.min_interval, rep_info->u.send_info.max_interval, - rep_info->u.send_info.def_min_interval, rep_info->u.send_info.def_max_interval); + log_reporting_info(rep_info); rep_info++; } } @@ -282,9 +281,38 @@ void ZigbeeComponent::dump_reporting_() { #endif } +void ZigbeeComponent::after_reporting_info(zb_zcl_configure_reporting_req_t *config_rep_req, + zb_zcl_attr_addr_info_t *attr_addr_info) { +#ifdef ESPHOME_LOG_HAS_DEBUG + auto *rep_info = + zb_zcl_find_reporting_info_manuf(attr_addr_info->src_ep, attr_addr_info->cluster_id, attr_addr_info->cluster_role, + config_rep_req->attr_id, attr_addr_info->manuf_code); + if (rep_info == nullptr) { + ESP_LOGE(TAG, + "Failed to resolve reporting info (src_ep=%u cluster_id=0x%04x role=%u attr_id=0x%04x manuf_code=0x%04x)", + attr_addr_info->src_ep, attr_addr_info->cluster_id, attr_addr_info->cluster_role, config_rep_req->attr_id, + attr_addr_info->manuf_code); + return; + } + log_reporting_info(rep_info); +#endif +} + } // namespace esphome::zigbee -extern "C" void zboss_signal_handler(zb_uint8_t param) { - esphome::zigbee::global_zigbee->zboss_signal_handler_esphome(param); +extern "C" { +void zboss_signal_handler(zb_uint8_t param) { esphome::zigbee::global_zigbee->zboss_signal_handler_esphome(param); } + +// NOLINTBEGIN(readability-identifier-naming,bugprone-reserved-identifier,cert-dcl37-c,cert-dcl51-cpp) +extern zb_ret_t __real_zb_zcl_put_reporting_info_from_req(zb_zcl_configure_reporting_req_t *config_rep_req, + zb_zcl_attr_addr_info_t *attr_addr_info); + +zb_ret_t __wrap_zb_zcl_put_reporting_info_from_req(zb_zcl_configure_reporting_req_t *config_rep_req, + zb_zcl_attr_addr_info_t *attr_addr_info) { + zb_ret_t ret = __real_zb_zcl_put_reporting_info_from_req(config_rep_req, attr_addr_info); + esphome::zigbee::global_zigbee->after_reporting_info(config_rep_req, attr_addr_info); + return ret; +} +// NOLINTEND(readability-identifier-naming,bugprone-reserved-identifier,cert-dcl37-c,cert-dcl51-cpp) } #endif diff --git a/esphome/components/zigbee/zigbee_zephyr.h b/esphome/components/zigbee/zigbee_zephyr.h index 3fa5818ec5..eeb142eff1 100644 --- a/esphome/components/zigbee/zigbee_zephyr.h +++ b/esphome/components/zigbee/zigbee_zephyr.h @@ -76,6 +76,7 @@ class ZigbeeComponent : public Component { } template void add_join_callback(F &&cb) { this->join_cb_.add(std::forward(cb)); } void zboss_signal_handler_esphome(zb_bufid_t bufid); + void after_reporting_info(zb_zcl_configure_reporting_req_t *config_rep_req, zb_zcl_attr_addr_info_t *attr_addr_info); void factory_reset(); Trigger<> *get_join_trigger() { return &this->join_trigger_; }; void force_report(); diff --git a/esphome/components/zigbee/zigbee_zephyr.py b/esphome/components/zigbee/zigbee_zephyr.py index a1e6ad3097..3288d92483 100644 --- a/esphome/components/zigbee/zigbee_zephyr.py +++ b/esphome/components/zigbee/zigbee_zephyr.py @@ -166,6 +166,8 @@ async def zephyr_to_code(config: ConfigType) -> None: zephyr_add_prj_conf("NET_IP_ADDR_CHECK", False) zephyr_add_prj_conf("NET_UDP", False) + cg.add_build_flag("-Wl,--wrap=zb_zcl_put_reporting_info_from_req") + if CONF_IEEE802154_VENDOR_OUI in config: zephyr_add_prj_conf("IEEE802154_VENDOR_OUI_ENABLE", True) random_number = config[CONF_IEEE802154_VENDOR_OUI] diff --git a/esphome/components/zwave_proxy/zwave_proxy.cpp b/esphome/components/zwave_proxy/zwave_proxy.cpp index ad4357663f..ecb38b25e7 100644 --- a/esphome/components/zwave_proxy/zwave_proxy.cpp +++ b/esphome/components/zwave_proxy/zwave_proxy.cpp @@ -3,6 +3,8 @@ #ifdef USE_API #include "esphome/components/api/api_server.h" + +#include #include "esphome/core/application.h" #include "esphome/core/helpers.h" #include "esphome/core/log.h" @@ -20,6 +22,8 @@ static constexpr uint8_t ZWAVE_COMMAND_GET_NETWORK_IDS = 0x20; static constexpr uint8_t ZWAVE_COMMAND_TYPE_RESPONSE = 0x01; // Response type field value static constexpr uint8_t ZWAVE_MIN_GET_NETWORK_IDS_LENGTH = 9; // TYPE + CMD + HOME_ID(4) + NODE_ID + checksum static constexpr uint32_t HOME_ID_TIMEOUT_MS = 100; // Timeout for waiting for home ID during setup +static constexpr uint32_t RECONNECT_DELAY_MS = 500; // Delay between home ID query attempts after reconnect +static constexpr uint8_t MAX_QUERY_RETRIES = 5; // Max attempts to query home ID after reconnect static uint8_t calculate_frame_checksum(const uint8_t *data, uint8_t length) { // Calculate Z-Wave frame checksum @@ -36,7 +40,10 @@ ZWaveProxy::ZWaveProxy() { global_zwave_proxy = this; } void ZWaveProxy::setup() { this->setup_time_ = App.get_loop_component_start_time(); - this->send_simple_command_(ZWAVE_COMMAND_GET_NETWORK_IDS); + this->was_connected_ = this->parent_->is_connected(); + if (this->was_connected_) { + this->send_simple_command_(ZWAVE_COMMAND_GET_NETWORK_IDS); + } } float ZWaveProxy::get_setup_priority() const { @@ -82,6 +89,14 @@ void ZWaveProxy::loop() { this->api_connection_ = nullptr; // Unsubscribe if disconnected } + const bool connected = this->parent_->is_connected(); + if (this->was_connected_ != connected) { + this->on_connection_changed_(connected); + } + if (this->reconnect_time_ != 0) { + this->retry_home_id_query_(); + } + this->process_uart_(); this->status_clear_warning(); } @@ -160,11 +175,60 @@ void ZWaveProxy::zwave_proxy_request(api::APIConnection *api_connection, api::en break; default: - ESP_LOGW(TAG, "Unknown request type: %d", type); + ESP_LOGW(TAG, "Unknown request type: %" PRIu32, static_cast(type)); break; } } +void ZWaveProxy::on_connection_changed_(bool connected) { + this->was_connected_ = connected; + if (connected) { + ESP_LOGD(TAG, "Modem reconnected"); + this->parsing_state_ = ZWAVE_PARSING_STATE_WAIT_START; + this->buffer_index_ = 0; + this->last_response_ = 0; + this->in_bootloader_ = false; + // Defer the query — the modem needs time to initialize after power is applied + this->reconnect_time_ = App.get_loop_component_start_time(); + this->query_retries_ = 0; + } else { + ESP_LOGW(TAG, "Modem disconnected"); + this->clear_home_id_(); + } +} + +void ZWaveProxy::retry_home_id_query_() { + if (this->home_id_ready_) { + // Got the home ID, cancel remaining retries + this->reconnect_time_ = 0; + return; + } + if (App.get_loop_component_start_time() - this->reconnect_time_ <= RECONNECT_DELAY_MS) { + return; // Not yet time for next attempt + } + this->reconnect_time_ = App.get_loop_component_start_time(); // Reset timer for next retry + this->query_retries_++; + if (this->query_retries_ <= MAX_QUERY_RETRIES) { + ESP_LOGD(TAG, "Querying Home ID (attempt %u)", this->query_retries_); + this->send_simple_command_(ZWAVE_COMMAND_GET_NETWORK_IDS); + } else { + ESP_LOGW(TAG, "Failed to read Home ID after %u attempts", MAX_QUERY_RETRIES); + this->reconnect_time_ = 0; + } +} + +void ZWaveProxy::clear_home_id_() { + static constexpr uint8_t ZERO_HOME_ID[ZWAVE_HOME_ID_SIZE] = {}; + if (this->set_home_id_(ZERO_HOME_ID)) { + this->send_homeid_changed_msg_(); + } + this->home_id_ready_ = false; + this->parsing_state_ = ZWAVE_PARSING_STATE_WAIT_START; + this->buffer_index_ = 0; + this->last_response_ = 0; + this->in_bootloader_ = false; +} + bool ZWaveProxy::set_home_id_(const uint8_t *new_home_id) { if (std::memcmp(this->home_id_.data(), new_home_id, this->home_id_.size()) == 0) { ESP_LOGV(TAG, "Home ID unchanged"); @@ -307,7 +371,7 @@ void ZWaveProxy::parse_start_(uint8_t byte) { this->parsing_state_ = ZWAVE_PARSING_STATE_WAIT_START; switch (byte) { case ZWAVE_FRAME_TYPE_START: - ESP_LOGVV(TAG, "Received START"); + ESP_LOGV(TAG, "Received START"); if (this->in_bootloader_) { ESP_LOGD(TAG, "Exited bootloader mode"); this->in_bootloader_ = false; @@ -316,7 +380,7 @@ void ZWaveProxy::parse_start_(uint8_t byte) { this->parsing_state_ = ZWAVE_PARSING_STATE_WAIT_LENGTH; return; case ZWAVE_FRAME_TYPE_BL_MENU: - ESP_LOGVV(TAG, "Received BL_MENU"); + ESP_LOGV(TAG, "Received BL_MENU"); if (!this->in_bootloader_) { ESP_LOGD(TAG, "Entered bootloader mode"); this->in_bootloader_ = true; @@ -325,16 +389,16 @@ void ZWaveProxy::parse_start_(uint8_t byte) { this->parsing_state_ = ZWAVE_PARSING_STATE_READ_BL_MENU; return; case ZWAVE_FRAME_TYPE_BL_BEGIN_UPLOAD: - ESP_LOGVV(TAG, "Received BL_BEGIN_UPLOAD"); + ESP_LOGV(TAG, "Received BL_BEGIN_UPLOAD"); break; case ZWAVE_FRAME_TYPE_ACK: - ESP_LOGVV(TAG, "Received ACK"); + ESP_LOGV(TAG, "Received ACK"); break; case ZWAVE_FRAME_TYPE_NAK: - ESP_LOGW(TAG, "Received NAK"); + ESP_LOGV(TAG, "Received NAK"); break; case ZWAVE_FRAME_TYPE_CAN: - ESP_LOGW(TAG, "Received CAN"); + ESP_LOGV(TAG, "Received CAN"); break; default: ESP_LOGW(TAG, "Unrecognized START: 0x%02X", byte); diff --git a/esphome/components/zwave_proxy/zwave_proxy.h b/esphome/components/zwave_proxy/zwave_proxy.h index 12cb9a90a1..0b810de29f 100644 --- a/esphome/components/zwave_proxy/zwave_proxy.h +++ b/esphome/components/zwave_proxy/zwave_proxy.h @@ -65,6 +65,9 @@ class ZWaveProxy : public uart::UARTDevice, public Component { protected: bool set_home_id_(const uint8_t *new_home_id); // Store a new home ID. Returns true if it changed. + void clear_home_id_(); // Clear home ID and notify API clients + void on_connection_changed_(bool connected); // Handle modem connect/disconnect transitions + void retry_home_id_query_(); // Retry home ID query after reconnect void send_homeid_changed_msg_(api::APIConnection *conn = nullptr); void send_simple_command_(uint8_t command_id); bool parse_byte_(uint8_t byte); // Returns true if frame parsing was completed (a frame is ready in the buffer) @@ -80,14 +83,17 @@ class ZWaveProxy : public uart::UARTDevice, public Component { // Pointers and 32-bit values (aligned together) api::APIConnection *api_connection_{nullptr}; // Current subscribed client uint32_t setup_time_{0}; // Time when setup() was called + uint32_t reconnect_time_{0}; // Timestamp of reconnect detection (0 = no pending query) // Small values (grouped by size to minimize padding) uint16_t buffer_index_{0}; // Index for populating the data buffer uint16_t end_frame_after_{0}; // Payload reception ends after this index uint8_t last_response_{0}; // Last response type sent + uint8_t query_retries_{0}; // Number of home ID query attempts after reconnect ZWaveParsingState parsing_state_{ZWAVE_PARSING_STATE_WAIT_START}; bool in_bootloader_{false}; // True if the device is detected to be in bootloader mode bool home_id_ready_{false}; // True when home ID has been received from Z-Wave module + bool was_connected_{false}; // Previous UART connection state for edge detection }; extern ZWaveProxy *global_zwave_proxy; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) diff --git a/esphome/config.py b/esphome/config.py index 7a6feea3d3..641b6ec1b4 100644 --- a/esphome/config.py +++ b/esphome/config.py @@ -958,6 +958,23 @@ class FinalValidateValidationStep(ConfigValidationStep): fv.full_config.reset(token) +class CoreFinalValidateStep(ConfigValidationStep): + """Run final validation on core esphome config (area/device hash collisions).""" + + # Same priority as component final validate steps + priority = -20.0 + + def run(self, result: Config) -> None: + if result.errors: + return + + token = fv.full_config.set(result) + with result.catch_error([CONF_ESPHOME]): + if CONF_ESPHOME in result: + core_config.validate_ids_and_references(result[CONF_ESPHOME]) + fv.full_config.reset(token) + + class PinUseValidationCheck(ConfigValidationStep): """Check for pin reuse""" @@ -1085,6 +1102,7 @@ def validate_config( for domain, conf in config.items(): result.add_validation_step(LoadValidationStep(domain, conf)) result.add_validation_step(IDPassValidationStep()) + result.add_validation_step(CoreFinalValidateStep()) result.add_validation_step(PinUseValidationCheck()) result.add_validation_step(RemoveReferenceValidationStep()) diff --git a/esphome/config_validation.py b/esphome/config_validation.py index 56f255a076..c6b67e9f35 100644 --- a/esphome/config_validation.py +++ b/esphome/config_validation.py @@ -244,6 +244,8 @@ RESERVED_IDS = [ "open", "setup", "loop", + "spi0", + "spi1", "uart0", "uart1", "uart2", @@ -417,10 +419,14 @@ def icon(value): return value +@schema_extractor("use_id") def sub_device_id(value: str | None) -> core.ID | None: # Lazy import to avoid circular imports from esphome.core.config import Device + if value == SCHEMA_EXTRACT: + return Device + if not value: return None @@ -1661,7 +1667,7 @@ def dimensions(value): match = re.match(r"\s*([0-9]+)\s*[xX]\s*([0-9]+)\s*", value) if not match: raise Invalid( - "Invalid value '{}' for dimensions. Only WIDTHxHEIGHT is allowed." + f"Invalid value '{value}' for dimensions. Only WIDTHxHEIGHT is allowed." ) return dimensions([match.group(1), match.group(2)]) diff --git a/esphome/core/application.cpp b/esphome/core/application.cpp index ce15aed1e2..cd75859880 100644 --- a/esphome/core/application.cpp +++ b/esphome/core/application.cpp @@ -28,22 +28,8 @@ #include "esphome/components/socket/socket.h" #endif -#ifdef USE_SOCKET_SELECT_SUPPORT +#ifdef USE_HOST #include - -#ifdef USE_SOCKET_IMPL_LWIP_SOCKETS -// LWIP sockets implementation -#include -#elif defined(USE_SOCKET_IMPL_BSD_SOCKETS) -// BSD sockets implementation -#ifdef USE_ESP32 -// ESP32 "BSD sockets" are actually LWIP under the hood -#include -#else -// True BSD sockets (e.g., host platform) -#include -#endif -#endif #endif namespace esphome { @@ -99,12 +85,6 @@ void Application::setup() { if (component->can_proceed()) continue; -#ifdef USE_LOOP_PRIORITY - // Sort components 0 through i by loop priority - insertion_sort_by_prioritycomponents_.begin()), &Component::get_loop_priority>( - this->components_.begin(), this->components_.begin() + i + 1); -#endif - do { uint8_t new_app_state = STATUS_LED_WARNING; uint32_t now = millis(); @@ -134,13 +114,11 @@ void Application::setup() { clear_setup_priority_overrides(); #endif -#if defined(USE_SOCKET_SELECT_SUPPORT) && defined(USE_LWIP_FAST_SELECT) - // Initialize fast select: saves main loop task handle for xTaskNotifyGive wake. - // The fast path (rcvevent reads + ulTaskNotifyTake) is used unconditionally - // when USE_LWIP_FAST_SELECT is enabled (ESP32 and LibreTiny). - esphome_lwip_fast_select_init(); +#if defined(USE_ESP32) || defined(USE_LIBRETINY) + // Save main loop task handle for wake_loop_*() / fast select FreeRTOS notifications. + esphome_main_task_handle = xTaskGetCurrentTaskHandle(); #endif -#if defined(USE_SOCKET_SELECT_SUPPORT) && defined(USE_WAKE_LOOP_THREADSAFE) && !defined(USE_LWIP_FAST_SELECT) +#ifdef USE_HOST // Set up wake socket for waking main loop from tasks (platforms without fast select only) this->setup_wake_loop_threadsafe_(); #endif @@ -496,23 +474,17 @@ void Application::unregister_socket(struct lwip_sock *sock) { return; } } -#elif defined(USE_SOCKET_SELECT_SUPPORT) +#elif defined(USE_HOST) bool Application::register_socket_fd(int fd) { // WARNING: This function is NOT thread-safe and must only be called from the main loop // It modifies socket_fds_ and related variables without locking if (fd < 0) return false; -#ifndef USE_ESP32 - // Only check on non-ESP32 platforms - // On ESP32 (both Arduino and ESP-IDF), CONFIG_LWIP_MAX_SOCKETS is always <= FD_SETSIZE by design - // (LWIP_SOCKET_OFFSET = FD_SETSIZE - CONFIG_LWIP_MAX_SOCKETS per lwipopts.h) - // Other platforms may not have this guarantee if (fd >= FD_SETSIZE) { ESP_LOGE(TAG, "fd %d exceeds FD_SETSIZE %d", fd, FD_SETSIZE); return false; } -#endif this->socket_fds_.push_back(fd); this->socket_fds_changed_ = true; @@ -553,7 +525,7 @@ void Application::unregister_socket_fd(int fd) { #endif // Only the select() fallback path remains in the .cpp — all other paths are inlined in application.h -#if defined(USE_SOCKET_SELECT_SUPPORT) && !defined(USE_LWIP_FAST_SELECT) +#ifdef USE_HOST void Application::yield_with_select_(uint32_t delay_ms) { // Fallback select() path (host platform and any future platforms without fast select). if (!this->socket_fds_.empty()) [[likely]] { @@ -576,11 +548,7 @@ void Application::yield_with_select_(uint32_t delay_ms) { tv.tv_usec = (delay_ms - tv.tv_sec * 1000) * 1000; // Call select with timeout -#ifdef USE_SOCKET_IMPL_LWIP_SOCKETS - int ret = lwip_select(this->max_fd_ + 1, &this->read_fds_, nullptr, nullptr, &tv); -#else int ret = ::select(this->max_fd_ + 1, &this->read_fds_, nullptr, nullptr, &tv); -#endif // Process select() result: // ret > 0: socket(s) have data ready - normal and expected @@ -603,7 +571,7 @@ void Application::yield_with_select_(uint32_t delay_ms) { // No sockets registered or select() failed - use regular delay delay(delay_ms); } -#endif // defined(USE_SOCKET_SELECT_SUPPORT) && !defined(USE_LWIP_FAST_SELECT) +#endif // USE_HOST // App storage — asm label shares the linker symbol with "extern Application App". // char[] is trivially destructible, so no __cxa_atexit or destructor chain is emitted. @@ -624,18 +592,13 @@ alignas(Application) char app_storage[sizeof(Application)] asm( #undef ESPHOME_STRINGIFY_ #undef ESPHOME_STRINGIFY_IMPL_ -#if defined(USE_SOCKET_SELECT_SUPPORT) && defined(USE_WAKE_LOOP_THREADSAFE) - -#ifdef USE_LWIP_FAST_SELECT -void Application::wake_loop_threadsafe() { - // Direct FreeRTOS task notification — <1 us, task context only (NOT ISR-safe) - esphome_lwip_wake_main_loop(); -} -#else // !USE_LWIP_FAST_SELECT +// Host platform wake_loop_threadsafe() and setup — needs wake_socket_fd_ +// ESP32/LibreTiny/ESP8266/RP2040 implementations are in wake.cpp +#ifdef USE_HOST void Application::setup_wake_loop_threadsafe_() { // Create UDP socket for wake notifications - this->wake_socket_fd_ = lwip_socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP); + this->wake_socket_fd_ = ::socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP); if (this->wake_socket_fd_ < 0) { ESP_LOGW(TAG, "Wake socket create failed: %d", errno); return; @@ -644,12 +607,12 @@ void Application::setup_wake_loop_threadsafe_() { // Bind to loopback with auto-assigned port struct sockaddr_in addr = {}; addr.sin_family = AF_INET; - addr.sin_addr.s_addr = lwip_htonl(INADDR_LOOPBACK); + addr.sin_addr.s_addr = htonl(INADDR_LOOPBACK); addr.sin_port = 0; // Auto-assign port - if (lwip_bind(this->wake_socket_fd_, (struct sockaddr *) &addr, sizeof(addr)) < 0) { + if (::bind(this->wake_socket_fd_, (struct sockaddr *) &addr, sizeof(addr)) < 0) { ESP_LOGW(TAG, "Wake socket bind failed: %d", errno); - lwip_close(this->wake_socket_fd_); + ::close(this->wake_socket_fd_); this->wake_socket_fd_ = -1; return; } @@ -658,50 +621,36 @@ void Application::setup_wake_loop_threadsafe_() { // Connecting a UDP socket allows using send() instead of sendto() for better performance struct sockaddr_in wake_addr; socklen_t len = sizeof(wake_addr); - if (lwip_getsockname(this->wake_socket_fd_, (struct sockaddr *) &wake_addr, &len) < 0) { + if (::getsockname(this->wake_socket_fd_, (struct sockaddr *) &wake_addr, &len) < 0) { ESP_LOGW(TAG, "Wake socket address failed: %d", errno); - lwip_close(this->wake_socket_fd_); + ::close(this->wake_socket_fd_); this->wake_socket_fd_ = -1; return; } // Connect to self (loopback) - allows using send() instead of sendto() // After connect(), no need to store wake_addr - the socket remembers it - if (lwip_connect(this->wake_socket_fd_, (struct sockaddr *) &wake_addr, sizeof(wake_addr)) < 0) { + if (::connect(this->wake_socket_fd_, (struct sockaddr *) &wake_addr, sizeof(wake_addr)) < 0) { ESP_LOGW(TAG, "Wake socket connect failed: %d", errno); - lwip_close(this->wake_socket_fd_); + ::close(this->wake_socket_fd_); this->wake_socket_fd_ = -1; return; } // Set non-blocking mode - int flags = lwip_fcntl(this->wake_socket_fd_, F_GETFL, 0); - lwip_fcntl(this->wake_socket_fd_, F_SETFL, flags | O_NONBLOCK); + int flags = ::fcntl(this->wake_socket_fd_, F_GETFL, 0); + ::fcntl(this->wake_socket_fd_, F_SETFL, flags | O_NONBLOCK); // Register with application's select() loop if (!this->register_socket_fd(this->wake_socket_fd_)) { ESP_LOGW(TAG, "Wake socket register failed"); - lwip_close(this->wake_socket_fd_); + ::close(this->wake_socket_fd_); this->wake_socket_fd_ = -1; return; } } -void Application::wake_loop_threadsafe() { - // Called from FreeRTOS task context when events need immediate processing - // Wakes up lwip_select() in main loop by writing to connected loopback socket - if (this->wake_socket_fd_ >= 0) { - const char dummy = 1; - // Non-blocking send - if it fails (unlikely), select() will wake on timeout anyway - // No error checking needed: we control both ends of this loopback socket. - // This is safe to call from FreeRTOS tasks - send() is thread-safe in lwip - // Socket is already connected to loopback address, so send() is faster than sendto() - lwip_send(this->wake_socket_fd_, &dummy, 1, 0); - } -} -#endif // USE_LWIP_FAST_SELECT - -#endif // defined(USE_SOCKET_SELECT_SUPPORT) && defined(USE_WAKE_LOOP_THREADSAFE) +#endif // USE_HOST void Application::get_build_time_string(std::span buffer) { ESPHOME_strncpy_P(buffer.data(), ESPHOME_BUILD_TIME_STR, buffer.size()); diff --git a/esphome/core/application.h b/esphome/core/application.h index 06ff30e81f..6b2969b490 100644 --- a/esphome/core/application.h +++ b/esphome/core/application.h @@ -24,32 +24,21 @@ #include "esphome/core/area.h" #endif -#ifdef USE_SOCKET_SELECT_SUPPORT #ifdef USE_LWIP_FAST_SELECT #include "esphome/core/lwip_fast_select.h" -#ifdef USE_ESP32 -#include -#include -#else -#include -#include #endif -#else +#ifdef USE_HOST #include -#ifdef USE_WAKE_LOOP_THREADSAFE -#include +#include +#include +#include +#include +#include #endif -#endif -#endif // USE_SOCKET_SELECT_SUPPORT #ifdef USE_RUNTIME_STATS #include "esphome/components/runtime_stats/runtime_stats.h" #endif -#if (defined(USE_ESP8266) || defined(USE_RP2040)) && defined(USE_SOCKET_IMPL_LWIP_TCP) -namespace esphome::socket { -void socket_wake(); // NOLINT(readability-redundant-declaration) -void socket_delay(uint32_t ms); // NOLINT(readability-redundant-declaration) -} // namespace esphome::socket -#endif +#include "esphome/core/wake.h" #ifdef USE_BINARY_SENSOR #include "esphome/components/binary_sensor/binary_sensor.h" #endif @@ -124,12 +113,18 @@ void socket_delay(uint32_t ms); // NOLINT(readability-redundant-declaration) #endif namespace esphome::socket { -#ifdef USE_SOCKET_SELECT_SUPPORT +#ifdef USE_HOST /// Shared ready() helper for fd-based socket implementations. bool socket_ready_fd(int fd, bool loop_monitored); // NOLINT(readability-redundant-declaration) #endif } // namespace esphome::socket +#ifdef USE_RUNTIME_STATS +namespace esphome::runtime_stats { +class RuntimeStatsCollector; +} // namespace esphome::runtime_stats +#endif + // Forward declarations for friend access from codegen-generated setup() void setup(); // NOLINT(readability-redundant-declaration) - may be declared in Arduino.h void original_setup(); // NOLINT(readability-redundant-declaration) - used by cpp unit tests @@ -544,7 +539,7 @@ class Application { /// @return true if registration was successful, false if sock is null bool register_socket(struct lwip_sock *sock); void unregister_socket(struct lwip_sock *sock); -#elif defined(USE_SOCKET_SELECT_SUPPORT) +#elif defined(USE_HOST) /// Fallback select() path: monitors file descriptors. /// NOTE: File descriptors >= FD_SETSIZE (typically 10 on ESP) will be rejected with an error. /// @return true if registration was successful, false if fd exceeds limits @@ -552,49 +547,33 @@ class Application { void unregister_socket_fd(int fd); #endif -#ifdef USE_WAKE_LOOP_THREADSAFE - /// Wake the main event loop from another FreeRTOS task. - /// Thread-safe, but must only be called from task context (NOT ISR-safe). - /// On ESP32: uses xTaskNotifyGive (<1 us) - /// On other platforms: uses UDP loopback socket - void wake_loop_threadsafe(); -#endif - -#ifdef USE_LWIP_FAST_SELECT - /// Wake the main event loop from an ISR. - /// Uses vTaskNotifyGiveFromISR() — <1 us, ISR-safe. - /// Only available on platforms with fast select (ESP32, LibreTiny). - /// @param px_higher_priority_task_woken Set to pdTRUE if a context switch is needed. - static void IRAM_ATTR wake_loop_isrsafe(int *px_higher_priority_task_woken) { - esphome_lwip_wake_main_loop_from_isr(px_higher_priority_task_woken); - } + /// Wake the main event loop from another thread or callback. + /// @see esphome::wake_loop_threadsafe() in wake.h for platform details. + void wake_loop_threadsafe() { esphome::wake_loop_threadsafe(); } #ifdef USE_ESP32 - /// Wake the main event loop from any context (ISR, thread, or main loop). - /// Detects the calling context and uses the appropriate FreeRTOS API. - static void IRAM_ATTR wake_loop_any_context() { esphome_lwip_wake_main_loop_any_context(); } -#endif + /// Wake from ISR (ESP32 only). + static void IRAM_ATTR wake_loop_isrsafe(BaseType_t *px) { esphome::wake_loop_isrsafe(px); } #endif -#if defined(USE_ESP8266) && defined(USE_SOCKET_IMPL_LWIP_TCP) - /// Wake the main event loop from any context (ISR, thread, or main loop). - /// Sets the socket wake flag and calls esp_schedule() to exit esp_delay() early. - static void IRAM_ATTR wake_loop_any_context() { socket::socket_wake(); } -#elif defined(USE_RP2040) && defined(USE_SOCKET_IMPL_LWIP_TCP) - /// Wake the main event loop from any context. - /// Sets the socket wake flag and calls __sev() to exit __wfe() early. - static void wake_loop_any_context() { socket::socket_wake(); } -#endif + /// Wake from any context (ISR, thread, callback). + static void IRAM_ATTR wake_loop_any_context() { esphome::wake_loop_any_context(); } protected: friend Component; -#if defined(USE_SOCKET_SELECT_SUPPORT) && !defined(USE_LWIP_FAST_SELECT) +#ifdef USE_HOST friend bool socket::socket_ready_fd(int fd, bool loop_monitored); +#endif +#ifdef USE_RUNTIME_STATS + friend class runtime_stats::RuntimeStatsCollector; #endif friend void ::setup(); friend void ::original_setup(); +#ifdef USE_HOST + friend void wake_loop_threadsafe(); // Host platform accesses wake_socket_fd_ +#endif -#if defined(USE_SOCKET_SELECT_SUPPORT) && !defined(USE_LWIP_FAST_SELECT) +#ifdef USE_HOST bool is_socket_ready_(int fd) const { return FD_ISSET(fd, &this->read_fds_); } #endif @@ -639,14 +618,14 @@ class Application { void feed_wdt_arch_(); /// Perform a delay while also monitoring socket file descriptors for readiness -#if defined(USE_SOCKET_SELECT_SUPPORT) && !defined(USE_LWIP_FAST_SELECT) +#ifdef USE_HOST // select() fallback path is too complex to inline (host platform) void yield_with_select_(uint32_t delay_ms); #else inline void ESPHOME_ALWAYS_INLINE yield_with_select_(uint32_t delay_ms); #endif -#if defined(USE_SOCKET_SELECT_SUPPORT) && defined(USE_WAKE_LOOP_THREADSAFE) && !defined(USE_LWIP_FAST_SELECT) +#ifdef USE_HOST void setup_wake_loop_threadsafe_(); // Create wake notification socket inline void drain_wake_notifications_(); // Read pending wake notifications in main loop (hot path - inlined) #endif @@ -676,13 +655,11 @@ class Application { FixedVector looping_components_{}; #ifdef USE_LWIP_FAST_SELECT std::vector monitored_sockets_; // Cached lwip_sock pointers for direct rcvevent read -#elif defined(USE_SOCKET_SELECT_SUPPORT) +#elif defined(USE_HOST) std::vector socket_fds_; // Vector of all monitored socket file descriptors #endif -#ifdef USE_SOCKET_SELECT_SUPPORT -#if defined(USE_WAKE_LOOP_THREADSAFE) && !defined(USE_LWIP_FAST_SELECT) +#ifdef USE_HOST int wake_socket_fd_{-1}; // Shared wake notification socket for waking main loop from tasks -#endif #endif // StringRef members (8 bytes each: pointer + size) @@ -693,7 +670,7 @@ class Application { uint32_t last_loop_{0}; uint32_t loop_component_start_time_{0}; -#if defined(USE_SOCKET_SELECT_SUPPORT) && !defined(USE_LWIP_FAST_SELECT) +#ifdef USE_HOST int max_fd_{-1}; // Highest file descriptor number for select() #endif @@ -709,11 +686,11 @@ class Application { bool in_loop_{false}; volatile bool has_pending_enable_loop_requests_{false}; -#if defined(USE_SOCKET_SELECT_SUPPORT) && !defined(USE_LWIP_FAST_SELECT) +#ifdef USE_HOST bool socket_fds_changed_{false}; // Flag to rebuild base_read_fds_ when socket_fds_ changes #endif -#if defined(USE_SOCKET_SELECT_SUPPORT) && !defined(USE_LWIP_FAST_SELECT) +#ifdef USE_HOST // Variable-sized members (not needed with fast select — is_socket_ready_ reads rcvevent directly) fd_set read_fds_{}; // Working fd_set: populated by select() fd_set base_read_fds_{}; // Cached fd_set rebuilt only when socket_fds_ changes @@ -806,7 +783,7 @@ class Application { /// Global storage of Application pointer - only one Application can exist. extern Application App; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) -#if defined(USE_SOCKET_SELECT_SUPPORT) && defined(USE_WAKE_LOOP_THREADSAFE) && !defined(USE_LWIP_FAST_SELECT) +#ifdef USE_HOST // Inline implementations for hot-path functions // drain_wake_notifications_() is called on every loop iteration @@ -823,15 +800,15 @@ inline void Application::drain_wake_notifications_() { // Multiple wake events may have triggered multiple writes, so drain until EWOULDBLOCK // We control both ends of this loopback socket (always write 1 byte per wake), // so no error checking needed - any errors indicate catastrophic system failure - while (lwip_recvfrom(this->wake_socket_fd_, buffer, sizeof(buffer), 0, nullptr, nullptr) > 0) { + while (::recvfrom(this->wake_socket_fd_, buffer, sizeof(buffer), 0, nullptr, nullptr) > 0) { // Just draining, no action needed - wake has already occurred } } } -#endif // defined(USE_SOCKET_SELECT_SUPPORT) && defined(USE_WAKE_LOOP_THREADSAFE) && !defined(USE_LWIP_FAST_SELECT) +#endif // USE_HOST inline void ESPHOME_ALWAYS_INLINE Application::before_loop_tasks_(uint32_t loop_start_time) { -#if defined(USE_SOCKET_SELECT_SUPPORT) && defined(USE_WAKE_LOOP_THREADSAFE) && !defined(USE_LWIP_FAST_SELECT) +#ifdef USE_HOST // Drain wake notifications first to clear socket for next wake this->drain_wake_notifications_(); #endif @@ -899,21 +876,17 @@ inline void ESPHOME_ALWAYS_INLINE Application::loop() { #endif // Use the last component's end time instead of calling millis() again + uint32_t delay_time = 0; auto elapsed = last_op_end_time - this->last_loop_; - if (elapsed >= this->loop_interval_ || HighFrequencyLoopRequester::is_high_frequency()) { - // Even if we overran the loop interval, we still need to select() - // to know if any sockets have data ready - this->yield_with_select_(0); - } else { - uint32_t delay_time = this->loop_interval_ - elapsed; + if (elapsed < this->loop_interval_ && !HighFrequencyLoopRequester::is_high_frequency()) { + delay_time = this->loop_interval_ - elapsed; uint32_t next_schedule = this->scheduler.next_schedule_in(last_op_end_time).value_or(delay_time); // next_schedule is max 0.5*delay_time // otherwise interval=0 schedules result in constant looping with almost no sleep next_schedule = std::max(next_schedule, delay_time / 2); delay_time = std::min(next_schedule, delay_time); - - this->yield_with_select_(delay_time); } + this->yield_with_select_(delay_time); this->last_loop_ = last_op_end_time; if (this->dump_config_at_ < this->components_.size()) { @@ -922,9 +895,9 @@ inline void ESPHOME_ALWAYS_INLINE Application::loop() { } // Inline yield_with_select_ for all paths except the select() fallback -#if !defined(USE_SOCKET_SELECT_SUPPORT) || defined(USE_LWIP_FAST_SELECT) +#ifndef USE_HOST inline void ESPHOME_ALWAYS_INLINE Application::yield_with_select_(uint32_t delay_ms) { -#if defined(USE_SOCKET_SELECT_SUPPORT) && defined(USE_LWIP_FAST_SELECT) +#ifdef USE_LWIP_FAST_SELECT // Fast path (ESP32/LibreTiny): reads rcvevent directly from cached lwip_sock pointers. // Safe because this runs on the main loop which owns socket lifetime (create, read, close). if (delay_ms == 0) [[unlikely]] { @@ -944,20 +917,10 @@ inline void ESPHOME_ALWAYS_INLINE Application::yield_with_select_(uint32_t delay } // Sleep with instant wake via FreeRTOS task notification. - // Woken by: callback wrapper (socket data arrives), wake_loop_threadsafe() (other tasks), or timeout. - // Without USE_WAKE_LOOP_THREADSAFE, only hooked socket callbacks wake the task — - // background tasks won't call wake, so this degrades to a pure timeout (same as old select path). - ulTaskNotifyTake(pdTRUE, pdMS_TO_TICKS(delay_ms)); -#elif (defined(USE_ESP8266) || defined(USE_RP2040)) && defined(USE_SOCKET_IMPL_LWIP_TCP) - // No select support but can wake on socket activity - // ESP8266: via esp_schedule() - // RP2040: via __sev()/__wfe() hardware sleep/wake - socket::socket_delay(delay_ms); -#else - // No select support, use regular delay - delay(delay_ms); + // Woken by: callback wrapper (socket data), wake_loop_threadsafe() (background tasks), or timeout. #endif + esphome::internal::wakeable_delay(delay_ms); } -#endif // !defined(USE_SOCKET_SELECT_SUPPORT) || defined(USE_LWIP_FAST_SELECT) +#endif // !USE_HOST } // namespace esphome diff --git a/esphome/core/automation.h b/esphome/core/automation.h index fc2cad99be..05c7f19588 100644 --- a/esphome/core/automation.h +++ b/esphome/core/automation.h @@ -419,44 +419,48 @@ template class Action { template class ActionList { public: void add_action(Action *action) { - if (this->actions_end_ == nullptr) { - this->actions_begin_ = action; - } else { - this->actions_end_->next_ = action; - } - this->actions_end_ = action; + // Walk to end of chain - action lists are short and only built during setup() + Action **tail = &this->actions_; + while (*tail != nullptr) + tail = &(*tail)->next_; + *tail = action; } void add_actions(const std::initializer_list *> &actions) { + // Find tail once, then append all actions in a single pass + Action **tail = &this->actions_; + while (*tail != nullptr) + tail = &(*tail)->next_; for (auto *action : actions) { - this->add_action(action); + *tail = action; + tail = &action->next_; } } // Force-inline: part of the Trigger→Automation→ActionList forwarding // chain collapsed to reduce automation call stack depth. inline void play(const Ts &...x) ESPHOME_ALWAYS_INLINE { - if (this->actions_begin_ != nullptr) - this->actions_begin_->play_complex(x...); + if (this->actions_ != nullptr) + this->actions_->play_complex(x...); } void play_tuple(const std::tuple &tuple) { this->play_tuple_(tuple, std::make_index_sequence{}); } void stop() { - if (this->actions_begin_ != nullptr) - this->actions_begin_->stop_complex(); + if (this->actions_ != nullptr) + this->actions_->stop_complex(); } - bool empty() const { return this->actions_begin_ == nullptr; } + bool empty() const { return this->actions_ == nullptr; } /// Check if any action in this action list is currently running. bool is_running() { - if (this->actions_begin_ == nullptr) + if (this->actions_ == nullptr) return false; - return this->actions_begin_->is_running(); + return this->actions_->is_running(); } /// Return the number of actions in this action list that are currently running. int num_running() { - if (this->actions_begin_ == nullptr) + if (this->actions_ == nullptr) return 0; - return this->actions_begin_->num_running_total(); + return this->actions_->num_running_total(); } protected: @@ -464,8 +468,7 @@ template class ActionList { this->play(std::get(tuple)...); } - Action *actions_begin_{nullptr}; - Action *actions_end_{nullptr}; + Action *actions_{nullptr}; }; template class Automation { diff --git a/esphome/core/base_automation.h b/esphome/core/base_automation.h index efcffa8824..11133d3973 100644 --- a/esphome/core/base_automation.h +++ b/esphome/core/base_automation.h @@ -9,14 +9,17 @@ #include "esphome/core/application.h" #include "esphome/core/helpers.h" +#include #include #include namespace esphome { -template class AndCondition : public Condition { +template class AndCondition : public Condition { public: - explicit AndCondition(std::initializer_list *> conditions) : conditions_(conditions) {} + explicit AndCondition(std::initializer_list *> conditions) { + init_array_from(this->conditions_, conditions); + } bool check(const Ts &...x) override { for (auto *condition : this->conditions_) { if (!condition->check(x...)) @@ -27,12 +30,14 @@ template class AndCondition : public Condition { } protected: - FixedVector *> conditions_; + std::array *, N> conditions_{}; }; -template class OrCondition : public Condition { +template class OrCondition : public Condition { public: - explicit OrCondition(std::initializer_list *> conditions) : conditions_(conditions) {} + explicit OrCondition(std::initializer_list *> conditions) { + init_array_from(this->conditions_, conditions); + } bool check(const Ts &...x) override { for (auto *condition : this->conditions_) { if (condition->check(x...)) @@ -43,7 +48,7 @@ template class OrCondition : public Condition { } protected: - FixedVector *> conditions_; + std::array *, N> conditions_{}; }; template class NotCondition : public Condition { @@ -55,9 +60,11 @@ template class NotCondition : public Condition { Condition *condition_; }; -template class XorCondition : public Condition { +template class XorCondition : public Condition { public: - explicit XorCondition(std::initializer_list *> conditions) : conditions_(conditions) {} + explicit XorCondition(std::initializer_list *> conditions) { + init_array_from(this->conditions_, conditions); + } bool check(const Ts &...x) override { size_t result = 0; for (auto *condition : this->conditions_) { @@ -68,7 +75,7 @@ template class XorCondition : public Condition { } protected: - FixedVector *> conditions_; + std::array *, N> conditions_{}; }; template class LambdaCondition : public Condition { diff --git a/esphome/core/component.cpp b/esphome/core/component.cpp index caaea89143..deda42b0a7 100644 --- a/esphome/core/component.cpp +++ b/esphome/core/component.cpp @@ -9,9 +9,6 @@ #include "esphome/core/hal.h" #include "esphome/core/helpers.h" #include "esphome/core/log.h" -#ifdef USE_RUNTIME_STATS -#include "esphome/components/runtime_stats/runtime_stats.h" -#endif namespace esphome { @@ -84,10 +81,8 @@ void store_component_error_message(const Component *component, const char *messa static constexpr uint16_t WARN_IF_BLOCKING_INCREMENT_MS = 10U; ///< How long the blocking time must be larger to warn again - -#ifdef USE_LOOP_PRIORITY -float Component::get_loop_priority() const { return 0.0f; } -#endif +// Threshold in ms (computed from centiseconds constant in component.h) +static constexpr uint32_t WARN_IF_BLOCKING_OVER_MS = static_cast(WARN_IF_BLOCKING_OVER_CS) * 10U; float Component::get_setup_priority() const { return setup_priority::DATA; } @@ -273,14 +268,14 @@ void Component::call() { } } bool Component::should_warn_of_blocking(uint32_t blocking_time) { - if (blocking_time > this->warn_if_blocking_over_) { - // Prevent overflow when adding increment - if we're about to overflow, just max out - if (blocking_time + WARN_IF_BLOCKING_INCREMENT_MS < blocking_time || - blocking_time + WARN_IF_BLOCKING_INCREMENT_MS > std::numeric_limits::max()) { - this->warn_if_blocking_over_ = std::numeric_limits::max(); - } else { - this->warn_if_blocking_over_ = static_cast(blocking_time + WARN_IF_BLOCKING_INCREMENT_MS); - } + // Convert centisecond threshold to milliseconds for comparison + uint32_t threshold_ms = static_cast(this->warn_if_blocking_over_) * 10U; + if (blocking_time > threshold_ms) { + // Set new threshold: blocking_time + increment, converted back to centiseconds + uint32_t new_threshold_ms = blocking_time + WARN_IF_BLOCKING_INCREMENT_MS; + uint32_t new_cs = new_threshold_ms / 10U; + // Saturate at uint8_t max (255 = 2550ms) + this->warn_if_blocking_over_ = static_cast(new_cs > 255U ? 255U : new_cs); return true; } return false; @@ -299,14 +294,12 @@ void Component::disable_loop() { App.disable_component_loop_(this); } } -void Component::enable_loop() { - if ((this->component_state_ & COMPONENT_STATE_MASK) == COMPONENT_STATE_LOOP_DONE) { - ESP_LOGVV(TAG, "%s loop enabled", LOG_STR_ARG(this->get_component_log_str())); - this->set_component_state_(COMPONENT_STATE_LOOP); - App.enable_component_loop_(this); - } +void Component::enable_loop_slow_path_() { + ESP_LOGVV(TAG, "%s loop enabled", LOG_STR_ARG(this->get_component_log_str())); + this->set_component_state_(COMPONENT_STATE_LOOP); + App.enable_component_loop_(this); } -void IRAM_ATTR HOT Component::enable_loop_soon_any_context() { +void IRAM_ATTR Component::enable_loop_soon_any_context() { // This method is thread and ISR-safe because: // 1. Only performs simple assignments to volatile variables (atomic on all platforms) // 2. No read-modify-write operations that could be interrupted @@ -318,15 +311,9 @@ void IRAM_ATTR HOT Component::enable_loop_soon_any_context() { // 8. Race condition with main loop is handled by clearing flag before processing this->pending_enable_loop_ = true; App.has_pending_enable_loop_requests_ = true; -#if (defined(USE_LWIP_FAST_SELECT) && defined(USE_ESP32)) || \ - ((defined(USE_ESP8266) || defined(USE_RP2040)) && defined(USE_SOCKET_IMPL_LWIP_TCP)) // Wake the main loop from sleep. Without this, the main loop would not // wake until the select/delay timeout expires (~16ms). - // ESP32: uses xPortInIsrContext() to choose the correct FreeRTOS notify API. - // ESP8266: sets socket wake flag and calls esp_schedule() to exit esp_delay() early. - // RP2040: sets socket wake flag and calls __sev() to exit __wfe() early. - Application::wake_loop_any_context(); -#endif + wake_loop_any_context(); } void Component::reset_to_construction_state() { if ((this->component_state_ & COMPONENT_STATE_MASK) == COMPONENT_STATE_FAILED) { @@ -521,18 +508,6 @@ WarnIfComponentBlockingGuard::warn_blocking(Component *component, uint32_t block } } -#ifdef USE_RUNTIME_STATS -void WarnIfComponentBlockingGuard::record_runtime_stats_() { - // Use micros() for accurate sub-millisecond timing. millis() has insufficient - // resolution — most components complete in microseconds but millis() only has - // 1ms granularity, so results were essentially random noise. - if (global_runtime_stats != nullptr) { - uint32_t duration_us = micros() - this->started_us_; - global_runtime_stats->record_component_time(this->component_, duration_us); - } -} -#endif - #ifdef USE_SETUP_PRIORITY_OVERRIDE void clear_setup_priority_overrides() { // Free the setup priority map completely @@ -541,4 +516,7 @@ void clear_setup_priority_overrides() { } #endif +// Weak default for component_source_lookup - overridden by generated code +__attribute__((weak)) const LogString *component_source_lookup(uint8_t) { return LOG_STR(""); } + } // namespace esphome diff --git a/esphome/core/component.h b/esphome/core/component.h index 46cd77b034..e2b7aa85d3 100644 --- a/esphome/core/component.h +++ b/esphome/core/component.h @@ -11,11 +11,21 @@ #include "esphome/core/log.h" #include "esphome/core/optional.h" +// Forward declarations for friend access from codegen-generated setup() +void setup(); // NOLINT(readability-redundant-declaration) - may be declared in Arduino.h +void original_setup(); // NOLINT(readability-redundant-declaration) + namespace esphome { // Forward declaration for LogString struct LogString; +#ifdef USE_RUNTIME_STATS +namespace runtime_stats { +class RuntimeStatsCollector; +} // namespace runtime_stats +#endif + /** Default setup priorities for components of different types. * * Components should return one of these setup priorities in get_setup_priority. @@ -79,11 +89,45 @@ inline constexpr uint8_t STATUS_LED_WARNING = 0x08; inline constexpr uint8_t STATUS_LED_ERROR = 0x10; // Component loop override flag uses bit 5 (set at registration time) inline constexpr uint8_t COMPONENT_HAS_LOOP = 0x20; - // Remove before 2026.8.0 enum class RetryResult { DONE, RETRY }; -inline constexpr uint16_t WARN_IF_BLOCKING_OVER_MS = 50U; +inline constexpr uint8_t WARN_IF_BLOCKING_OVER_CS = 5U; // 50ms in centiseconds (1cs = 10ms) + +/// Lookup component source name by index (1-based). Generated by Python codegen. +/// Weak default returns "" so builds without codegen still link. +const LogString *component_source_lookup(uint8_t index); + +#ifdef USE_RUNTIME_STATS +/// Inline runtime statistics — eliminates std::map lookup on every loop iteration. +/// Only present when USE_RUNTIME_STATS is defined (profiling builds). +struct ComponentRuntimeStats { + // Period stats (reset each logging interval) + uint32_t period_count{0}; + uint32_t period_time_us{0}; + uint32_t period_max_time_us{0}; + // Total stats (persistent until reboot, uint64_t to avoid overflow) + uint32_t total_count{0}; + uint64_t total_time_us{0}; + uint32_t total_max_time_us{0}; + + void record_time(uint32_t duration_us) { + this->period_count++; + this->period_time_us += duration_us; + if (duration_us > this->period_max_time_us) + this->period_max_time_us = duration_us; + this->total_count++; + this->total_time_us += duration_us; + if (duration_us > this->total_max_time_us) + this->total_max_time_us = duration_us; + } + void reset_period() { + this->period_count = 0; + this->period_time_us = 0; + this->period_max_time_us = 0; + } +}; +#endif class Component { public: @@ -115,16 +159,6 @@ class Component { void set_setup_priority(float priority); - /** priority of loop(). higher -> executed earlier - * - * Defaults to 0. - * - * @return The loop priority of this component - */ -#ifdef USE_LOOP_PRIORITY - virtual float get_loop_priority() const; -#endif - void call(); virtual void on_shutdown() {} @@ -208,7 +242,10 @@ class Component { * @note Components should call this->enable_loop() on themselves, not on other components. * This ensures the component's state is properly updated along with the loop partition. */ - void enable_loop(); + void enable_loop() { + if ((this->component_state_ & COMPONENT_STATE_MASK) == COMPONENT_STATE_LOOP_DONE) + this->enable_loop_slow_path_(); + } /** Thread and ISR-safe version of enable_loop() that can be called from any context. * @@ -285,27 +322,33 @@ class Component { bool has_overridden_loop() const { return (this->component_state_ & COMPONENT_HAS_LOOP) != 0; } - /** Set where this component was loaded from for some debug messages. - * - * This is set by the ESPHome core, and should not be called manually. - */ - void set_component_source(const LogString *source) { component_source_ = source; } /** Get the integration where this component was declared as a LogString for logging. * * Returns LOG_STR("") if source not set */ - const LogString *get_component_log_str() const { - return this->component_source_ == nullptr ? LOG_STR("") : this->component_source_; + inline const LogString *get_component_log_str() const ESPHOME_ALWAYS_INLINE { + return component_source_lookup(this->component_source_index_); } bool should_warn_of_blocking(uint32_t blocking_time); protected: friend class Application; + friend void ::setup(); + friend void ::original_setup(); + + /** Set where this component was loaded from for some debug messages. + * + * This is set by the ESPHome core during setup, and should not be called manually. + * @param index 1-based index into the component source lookup table (0 = not set) + */ + void set_component_source_(uint8_t index) { this->component_source_index_ = index; } virtual void call_setup(); void call_dump_config_(); + void enable_loop_slow_path_(); + /// Helper to set component state (clears state bits and sets new state) inline void set_component_state_(uint8_t state) { this->component_state_ &= ~COMPONENT_STATE_MASK; @@ -519,9 +562,9 @@ class Component { void status_clear_warning_slow_path_(); void status_clear_error_slow_path_(); - // Ordered for optimal packing on 32-bit systems - const LogString *component_source_{nullptr}; - uint16_t warn_if_blocking_over_{WARN_IF_BLOCKING_OVER_MS}; ///< Warn if blocked for this many ms (max 65.5s) + // Ordered for optimal packing on 32-bit systems (8 bytes total with vtable) + uint8_t component_source_index_{0}; ///< Index into component source PROGMEM lookup table (0 = not set) + uint8_t warn_if_blocking_over_{WARN_IF_BLOCKING_OVER_CS}; ///< Warn threshold in centiseconds (max 2550ms) /// State of this component - each bit has a purpose: /// Bits 0-2: Component state (0x00=CONSTRUCTION, 0x01=SETUP, 0x02=LOOP, 0x03=FAILED, 0x04=LOOP_DONE) /// Bit 3: STATUS_LED_WARNING @@ -530,6 +573,11 @@ class Component { /// Bits 6-7: Unused - reserved for future expansion uint8_t component_state_{0x00}; volatile bool pending_enable_loop_{false}; ///< ISR-safe flag for enable_loop_soon_any_context +#ifdef USE_RUNTIME_STATS + friend class runtime_stats::RuntimeStatsCollector; + friend class WarnIfComponentBlockingGuard; + ComponentRuntimeStats runtime_stats_; +#endif }; /** This class simplifies creating components that periodically check a state. @@ -589,15 +637,17 @@ class WarnIfComponentBlockingGuard { { } - // Finish the timing operation and return the current time + // Finish the timing operation and return the current time (millis) // Inlined: the fast path is just millis() + subtract + compare inline uint32_t HOT finish() { - uint32_t curr_time = millis(); - uint32_t blocking_time = curr_time - this->started_; #ifdef USE_RUNTIME_STATS - this->record_runtime_stats_(); + this->component_->runtime_stats_.record_time(micros() - this->started_us_); #endif + uint32_t curr_time = millis(); #ifndef USE_BENCHMARK + // Fast path: compare against constant threshold in ms (computed at compile time from centiseconds) + static constexpr uint32_t WARN_IF_BLOCKING_OVER_MS = static_cast(WARN_IF_BLOCKING_OVER_CS) * 10U; + uint32_t blocking_time = curr_time - this->started_; if (blocking_time > WARN_IF_BLOCKING_OVER_MS) [[unlikely]] { warn_blocking(this->component_, blocking_time); } @@ -612,7 +662,6 @@ class WarnIfComponentBlockingGuard { Component *component_; #ifdef USE_RUNTIME_STATS uint32_t started_us_; - void record_runtime_stats_(); #endif private: diff --git a/esphome/core/config.py b/esphome/core/config.py index e02c6ec75f..31cfd00ef7 100644 --- a/esphome/core/config.py +++ b/esphome/core/config.py @@ -156,22 +156,22 @@ def validate_ids_and_references(config: ConfigType) -> ConfigType: hash_dict[hash_val] = id_obj.id # Collect all areas - all_areas: list[dict[str, str | core.ID]] = [] + all_areas: list[tuple[dict[str, str | core.ID], str]] = [] if CONF_AREA in config: - all_areas.append(config[CONF_AREA]) - all_areas.extend(config[CONF_AREAS]) + all_areas.append((config[CONF_AREA], CONF_AREA)) + all_areas.extend((area, CONF_AREAS) for area in config.get(CONF_AREAS, [])) # Validate area hash collisions and collect IDs area_hashes: dict[int, str] = {} area_ids: set[str] = set() - for area in all_areas: + for area, key in all_areas: area_id: core.ID = area[CONF_ID] - check_hash_collision(area_id, area_hashes, "Area", [CONF_AREAS, area_id.id]) + check_hash_collision(area_id, area_hashes, "Area", [key, area_id.id]) area_ids.add(area_id.id) # Validate device hash collisions and area references device_hashes: dict[int, str] = {} - for device in config[CONF_DEVICES]: + for device in config.get(CONF_DEVICES, []): device_id: core.ID = device[CONF_ID] check_hash_collision( device_id, device_hashes, "Device", [CONF_DEVICES, device_id.id] @@ -233,6 +233,9 @@ DEVICE_CLASS_MAX_LENGTH = 47 # Keep in sync with MAX_ICON_LENGTH in esphome/core/entity_base.h ICON_MAX_LENGTH = 63 +# Max unit of measurement string length +UNIT_OF_MEASUREMENT_MAX_LENGTH = 63 + AREA_SCHEMA = cv.Schema( { cv.GenerateID(CONF_ID): cv.declare_id(Area), @@ -329,9 +332,6 @@ CONFIG_SCHEMA = cv.All( ) -FINAL_VALIDATE_SCHEMA = cv.All(validate_ids_and_references) - - PRELOAD_CONFIG_SCHEMA = cv.Schema( { cv.Required(CONF_NAME): cv.valid_name, @@ -756,6 +756,20 @@ FILTER_SOURCE_FILES = filter_source_files_from_platform( PlatformFramework.ESP32_ARDUINO, PlatformFramework.ESP32_IDF, }, + "main_task.c": { + PlatformFramework.ESP32_ARDUINO, + PlatformFramework.ESP32_IDF, + PlatformFramework.BK72XX_ARDUINO, + PlatformFramework.RTL87XX_ARDUINO, + PlatformFramework.LN882X_ARDUINO, + }, + "lwip_fast_select.c": { + PlatformFramework.ESP32_ARDUINO, + PlatformFramework.ESP32_IDF, + PlatformFramework.BK72XX_ARDUINO, + PlatformFramework.RTL87XX_ARDUINO, + PlatformFramework.LN882X_ARDUINO, + }, "time_64.cpp": { PlatformFramework.ESP8266_ARDUINO, PlatformFramework.BK72XX_ARDUINO, diff --git a/esphome/core/controller_registry.cpp b/esphome/core/controller_registry.cpp index dd69de47d4..92f23f5642 100644 --- a/esphome/core/controller_registry.cpp +++ b/esphome/core/controller_registry.cpp @@ -2,122 +2,12 @@ #ifdef USE_CONTROLLER_REGISTRY -#include "esphome/core/controller.h" - namespace esphome { StaticVector ControllerRegistry::controllers; void ControllerRegistry::register_controller(Controller *controller) { controllers.push_back(controller); } -// Each notify method directly iterates controllers and calls the virtual method. -// This avoids the overhead of a shared noinline dispatch loop with function pointer -// indirection. The loop is tiny (~20 bytes per entity type) so the flash cost of -// duplicating it is negligible compared to eliminating two levels of indirection -// (noinline call + function pointer) from every state publish. -// NOLINTBEGIN(bugprone-macro-parentheses) -#define CONTROLLER_REGISTRY_NOTIFY(entity_type, entity_name) \ - void ControllerRegistry::notify_##entity_name##_update(entity_type *obj) { \ - for (auto *controller : controllers) { \ - controller->on_##entity_name##_update(obj); \ - } \ - } - -#define CONTROLLER_REGISTRY_NOTIFY_NO_UPDATE_SUFFIX(entity_type, entity_name) \ - void ControllerRegistry::notify_##entity_name(entity_type *obj) { \ - for (auto *controller : controllers) { \ - controller->on_##entity_name(obj); \ - } \ - } -// NOLINTEND(bugprone-macro-parentheses) - -#ifdef USE_BINARY_SENSOR -CONTROLLER_REGISTRY_NOTIFY(binary_sensor::BinarySensor, binary_sensor) -#endif - -#ifdef USE_FAN -CONTROLLER_REGISTRY_NOTIFY(fan::Fan, fan) -#endif - -#ifdef USE_LIGHT -CONTROLLER_REGISTRY_NOTIFY(light::LightState, light) -#endif - -#ifdef USE_SENSOR -CONTROLLER_REGISTRY_NOTIFY(sensor::Sensor, sensor) -#endif - -#ifdef USE_SWITCH -CONTROLLER_REGISTRY_NOTIFY(switch_::Switch, switch) -#endif - -#ifdef USE_COVER -CONTROLLER_REGISTRY_NOTIFY(cover::Cover, cover) -#endif - -#ifdef USE_TEXT_SENSOR -CONTROLLER_REGISTRY_NOTIFY(text_sensor::TextSensor, text_sensor) -#endif - -#ifdef USE_CLIMATE -CONTROLLER_REGISTRY_NOTIFY(climate::Climate, climate) -#endif - -#ifdef USE_NUMBER -CONTROLLER_REGISTRY_NOTIFY(number::Number, number) -#endif - -#ifdef USE_DATETIME_DATE -CONTROLLER_REGISTRY_NOTIFY(datetime::DateEntity, date) -#endif - -#ifdef USE_DATETIME_TIME -CONTROLLER_REGISTRY_NOTIFY(datetime::TimeEntity, time) -#endif - -#ifdef USE_DATETIME_DATETIME -CONTROLLER_REGISTRY_NOTIFY(datetime::DateTimeEntity, datetime) -#endif - -#ifdef USE_TEXT -CONTROLLER_REGISTRY_NOTIFY(text::Text, text) -#endif - -#ifdef USE_SELECT -CONTROLLER_REGISTRY_NOTIFY(select::Select, select) -#endif - -#ifdef USE_LOCK -CONTROLLER_REGISTRY_NOTIFY(lock::Lock, lock) -#endif - -#ifdef USE_VALVE -CONTROLLER_REGISTRY_NOTIFY(valve::Valve, valve) -#endif - -#ifdef USE_MEDIA_PLAYER -CONTROLLER_REGISTRY_NOTIFY(media_player::MediaPlayer, media_player) -#endif - -#ifdef USE_ALARM_CONTROL_PANEL -CONTROLLER_REGISTRY_NOTIFY(alarm_control_panel::AlarmControlPanel, alarm_control_panel) -#endif - -#ifdef USE_WATER_HEATER -CONTROLLER_REGISTRY_NOTIFY(water_heater::WaterHeater, water_heater) -#endif - -#ifdef USE_EVENT -CONTROLLER_REGISTRY_NOTIFY_NO_UPDATE_SUFFIX(event::Event, event) -#endif - -#ifdef USE_UPDATE -CONTROLLER_REGISTRY_NOTIFY_NO_UPDATE_SUFFIX(update::UpdateEntity, update) -#endif - -#undef CONTROLLER_REGISTRY_NOTIFY -#undef CONTROLLER_REGISTRY_NOTIFY_NO_UPDATE_SUFFIX - } // namespace esphome #endif // USE_CONTROLLER_REGISTRY diff --git a/esphome/core/controller_registry.h b/esphome/core/controller_registry.h index 89b3069bcb..846642da29 100644 --- a/esphome/core/controller_registry.h +++ b/esphome/core/controller_registry.h @@ -252,4 +252,121 @@ class ControllerRegistry { } // namespace esphome +// Include controller.h AFTER the class definition so notify methods can be +// defined inline. This is safe because controller_registry.h is only ever +// included from .cpp files, never from other headers. +#include "esphome/core/controller.h" + +namespace esphome { + +// Inline notify methods — each is a tiny loop over 1-2 controllers. +// Defining them here (rather than in controller_registry.cpp) allows the +// compiler to inline them into the single call site in each entity's +// notify_frontend_(), eliminating an unnecessary function-call frame. + +// NOLINTBEGIN(bugprone-macro-parentheses) +#define CONTROLLER_REGISTRY_NOTIFY(entity_type, entity_name) \ + inline void ControllerRegistry::notify_##entity_name##_update(entity_type *obj) { \ + for (auto *controller : controllers) { \ + controller->on_##entity_name##_update(obj); \ + } \ + } + +#define CONTROLLER_REGISTRY_NOTIFY_NO_UPDATE_SUFFIX(entity_type, entity_name) \ + inline void ControllerRegistry::notify_##entity_name(entity_type *obj) { \ + for (auto *controller : controllers) { \ + controller->on_##entity_name(obj); \ + } \ + } +// NOLINTEND(bugprone-macro-parentheses) + +#ifdef USE_BINARY_SENSOR +CONTROLLER_REGISTRY_NOTIFY(binary_sensor::BinarySensor, binary_sensor) +#endif + +#ifdef USE_FAN +CONTROLLER_REGISTRY_NOTIFY(fan::Fan, fan) +#endif + +#ifdef USE_LIGHT +CONTROLLER_REGISTRY_NOTIFY(light::LightState, light) +#endif + +#ifdef USE_SENSOR +CONTROLLER_REGISTRY_NOTIFY(sensor::Sensor, sensor) +#endif + +#ifdef USE_SWITCH +CONTROLLER_REGISTRY_NOTIFY(switch_::Switch, switch) +#endif + +#ifdef USE_COVER +CONTROLLER_REGISTRY_NOTIFY(cover::Cover, cover) +#endif + +#ifdef USE_TEXT_SENSOR +CONTROLLER_REGISTRY_NOTIFY(text_sensor::TextSensor, text_sensor) +#endif + +#ifdef USE_CLIMATE +CONTROLLER_REGISTRY_NOTIFY(climate::Climate, climate) +#endif + +#ifdef USE_NUMBER +CONTROLLER_REGISTRY_NOTIFY(number::Number, number) +#endif + +#ifdef USE_DATETIME_DATE +CONTROLLER_REGISTRY_NOTIFY(datetime::DateEntity, date) +#endif + +#ifdef USE_DATETIME_TIME +CONTROLLER_REGISTRY_NOTIFY(datetime::TimeEntity, time) +#endif + +#ifdef USE_DATETIME_DATETIME +CONTROLLER_REGISTRY_NOTIFY(datetime::DateTimeEntity, datetime) +#endif + +#ifdef USE_TEXT +CONTROLLER_REGISTRY_NOTIFY(text::Text, text) +#endif + +#ifdef USE_SELECT +CONTROLLER_REGISTRY_NOTIFY(select::Select, select) +#endif + +#ifdef USE_LOCK +CONTROLLER_REGISTRY_NOTIFY(lock::Lock, lock) +#endif + +#ifdef USE_VALVE +CONTROLLER_REGISTRY_NOTIFY(valve::Valve, valve) +#endif + +#ifdef USE_MEDIA_PLAYER +CONTROLLER_REGISTRY_NOTIFY(media_player::MediaPlayer, media_player) +#endif + +#ifdef USE_ALARM_CONTROL_PANEL +CONTROLLER_REGISTRY_NOTIFY(alarm_control_panel::AlarmControlPanel, alarm_control_panel) +#endif + +#ifdef USE_WATER_HEATER +CONTROLLER_REGISTRY_NOTIFY(water_heater::WaterHeater, water_heater) +#endif + +#ifdef USE_EVENT +CONTROLLER_REGISTRY_NOTIFY_NO_UPDATE_SUFFIX(event::Event, event) +#endif + +#ifdef USE_UPDATE +CONTROLLER_REGISTRY_NOTIFY_NO_UPDATE_SUFFIX(update::UpdateEntity, update) +#endif + +#undef CONTROLLER_REGISTRY_NOTIFY +#undef CONTROLLER_REGISTRY_NOTIFY_NO_UPDATE_SUFFIX + +} // namespace esphome + #endif // USE_CONTROLLER_REGISTRY diff --git a/esphome/core/defines.h b/esphome/core/defines.h index 8cf331c4d6..9c90790f3a 100644 --- a/esphome/core/defines.h +++ b/esphome/core/defines.h @@ -123,6 +123,7 @@ #define USE_NEXTION_MAX_COMMANDS_PER_LOOP #define USE_NEXTION_MAX_QUEUE_SIZE #define USE_NEXTION_TFT_UPLOAD +#define USE_NEXTION_WAVEFORM #define USE_NUMBER #define USE_OUTPUT #define USE_POWER_SUPPLY @@ -180,6 +181,7 @@ #define USE_RUNTIME_IMAGE_BMP #define USE_RUNTIME_IMAGE_PNG #define USE_RUNTIME_IMAGE_JPEG +#define USE_RUNTIME_STATS #define USE_OTA #define USE_OTA_PASSWORD #define USE_OTA_STATE_LISTENER @@ -209,6 +211,7 @@ #define USE_ESPHOME_TASK_LOG_BUFFER #define ESPHOME_TASK_LOG_BUFFER_SIZE 768 #define USE_OTA_ROLLBACK +#define USE_OTA_SIGNED_VERIFICATION #define USE_ESP32_MIN_CHIP_REVISION_SET #define USE_ESP32_SRAM1_AS_IRAM @@ -249,9 +252,8 @@ #define USE_SENDSPIN #define USE_SENDSPIN_PORT 8928 // NOLINT #define USE_SOCKET_IMPL_BSD_SOCKETS -#define USE_SOCKET_SELECT_SUPPORT #define USE_LWIP_FAST_SELECT -#define USE_WAKE_LOOP_THREADSAFE + #define USE_SPEAKER #define USE_SPEAKER_MEDIA_PLAYER_ON_OFF #define USE_SPI @@ -328,6 +330,7 @@ // ESP8266-specific feature flags #ifdef USE_ESP8266 #define USE_ADC_SENSOR_VCC +#define USE_ESP8266_CRASH_HANDLER #define USE_ARDUINO_VERSION_CODE VERSION_CODE(3, 1, 2) #define USE_CAPTIVE_PORTAL #define USE_ESP8266_LOGGER_SERIAL @@ -357,7 +360,6 @@ #ifdef USE_RP2040 #define USE_ARDUINO_VERSION_CODE VERSION_CODE(3, 3, 0) -#define USE_LOOP_PRIORITY #define USE_RP2040_CRASH_HANDLER #define USE_HTTP_REQUEST_RESPONSE #define USE_I2C @@ -376,7 +378,6 @@ #ifdef USE_LIBRETINY #define USE_CAPTIVE_PORTAL #define USE_SOCKET_IMPL_LWIP_SOCKETS -#define USE_SOCKET_SELECT_SUPPORT #define USE_LWIP_FAST_SELECT #define USE_WEBSERVER #define USE_WEBSERVER_AUTH @@ -388,7 +389,6 @@ #ifdef USE_HOST #define USE_HTTP_REQUEST_RESPONSE #define USE_SOCKET_IMPL_BSD_SOCKETS -#define USE_SOCKET_SELECT_SUPPORT #define USE_ESPHOME_TASK_LOG_BUFFER #define ESPHOME_TASK_LOG_BUFFER_SIZE 64 #endif diff --git a/esphome/core/entity_base.h b/esphome/core/entity_base.h index 8c1f1a213e..5a69c9dd09 100644 --- a/esphome/core/entity_base.h +++ b/esphome/core/entity_base.h @@ -296,15 +296,36 @@ void log_entity_device_class(const char *tag, const char *prefix, const EntityBa #define LOG_ENTITY_UNIT_OF_MEASUREMENT(tag, prefix, obj) log_entity_unit_of_measurement(tag, prefix, obj) void log_entity_unit_of_measurement(const char *tag, const char *prefix, const EntityBase &obj); -/** - * An entity that has a state. - * @tparam T The type of the state +/** Base class for entities that track a typed state value with change-detection and callbacks. + * + * This class does not store the state value — subclasses own their storage. Whether a state + * has been set is tracked by EntityBase::has_state(). + * + * Subclasses must implement: + * - get_state(): return a const reference to the current value + * - set_state_value(): store a new value (called only when the state actually changes) + * - get_trigger_on_initial_state(): return whether callbacks should fire on the first state + * + * Subclasses may override set_new_state() to add behavior (logging, notifications) after calling + * the base implementation. Since set_new_state() is virtual, callers like invalidate_state() + * dispatch through the vtable to the subclass override in the .cpp, avoiding template code + * bloat at inline call sites. Subclasses may also add a fast-path dedup check before calling + * set_new_state() to skip virtual dispatch entirely when the state hasn't changed. + * + * Callback behavior: + * - full_state_callbacks_: fired on every change, receives optional previous and current + * - state_callbacks_: fired only when the new state has a value, and either this is not the + * first state (had_state) or trigger_on_initial_state is set + * + * @tparam T The type of the state value */ template class StatefulEntityBase : public EntityBase { public: - virtual bool has_state() const { return this->state_.has_value(); } - virtual const T &get_state() const { return this->state_.value(); } // NOLINT(bugprone-unchecked-optional-access) - virtual T get_state_default(T default_value) const { return this->state_.value_or(default_value); } + /// Return the current state value. Only valid when has_state() is true. + virtual const T &get_state() const = 0; + /// Return the current state if available, otherwise return the provided default. + T get_state_default(T default_value) const { return this->has_state() ? this->get_state() : default_value; } + /// Clear the state — sets has_state() to false and fires callbacks with nullopt. void invalidate_state() { this->set_new_state({}); } template void add_full_state_callback(F &&callback) { @@ -314,33 +335,46 @@ template class StatefulEntityBase : public EntityBase { this->state_callbacks_.add(std::forward(callback)); } - void set_trigger_on_initial_state(bool trigger_on_initial_state) { - this->trigger_on_initial_state_ = trigger_on_initial_state; - } - protected: - optional state_{}; - /** - * Set a new state for this entity. This will trigger callbacks only if the new state is different from the previous. + /// Subclasses return whether callbacks should fire on the very first state. + virtual bool get_trigger_on_initial_state() const = 0; + + /** Apply a new state, de-duplicating and firing callbacks as needed. * - * @param new_state The new state. - * @return True if the state was changed, false if it was the same as before. + * Pass nullopt to invalidate (clear) the state. Pass a value to set it. + * Returns true if the state actually changed, false if it was the same. + * Subclasses may override to add logging/notifications after calling the base. */ virtual bool set_new_state(const optional &new_state) { - if (this->state_ != new_state) { - // call the full state callbacks with the previous and new state - this->full_state_callbacks_.call(this->state_, new_state); - // trigger legacy callbacks only if the new state is valid and either the trigger on initial state is enabled or - // the previous state was valid - auto had_state = this->has_state(); - this->state_ = new_state; - if (new_state.has_value() && (this->trigger_on_initial_state_ || had_state)) - this->state_callbacks_.call(new_state.value()); - return true; + // Access flags_ directly to avoid function call overhead in this hot path + bool had_state = this->flags_.has_state; + // Use pointer to avoid requiring T to be default-constructible + const T *current = had_state ? &this->get_state() : nullptr; + if (new_state.has_value()) { + if (current != nullptr && *current == new_state.value()) + return false; // same value, no change + } else if (!had_state) { + return false; // already invalidated, no change } - return false; + // Capture old_state before set_state_value — current pointer aliases subclass storage + bool has_full_cbs = !this->full_state_callbacks_.empty(); + optional old_state; + if (has_full_cbs) + old_state = current != nullptr ? optional(*current) : nullopt; + // Update storage before firing callbacks so callback code can inspect current state + this->flags_.has_state = new_state.has_value(); + if (new_state.has_value()) { + this->set_state_value(new_state.value()); + } + if (has_full_cbs) + this->full_state_callbacks_.call(old_state, new_state); + // had_state first: on every change except the first, skips the virtual call + if (new_state.has_value() && (had_state || this->get_trigger_on_initial_state())) + this->state_callbacks_.call(new_state.value()); + return true; } - bool trigger_on_initial_state_{true}; + /// Subclasses implement this to store the actual value into their own storage. + virtual void set_state_value(const T &value) = 0; LazyCallbackManager previous, optional current)> full_state_callbacks_; LazyCallbackManager state_callbacks_; }; diff --git a/esphome/core/entity_helpers.py b/esphome/core/entity_helpers.py index 0589b92364..fc931c2baa 100644 --- a/esphome/core/entity_helpers.py +++ b/esphome/core/entity_helpers.py @@ -17,7 +17,11 @@ from esphome.const import ( CONF_UNIT_OF_MEASUREMENT, ) from esphome.core import CORE, ID, CoroPriority, coroutine_with_priority -from esphome.core.config import DEVICE_CLASS_MAX_LENGTH, ICON_MAX_LENGTH +from esphome.core.config import ( + DEVICE_CLASS_MAX_LENGTH, + ICON_MAX_LENGTH, + UNIT_OF_MEASUREMENT_MAX_LENGTH, +) from esphome.cpp_generator import MockObj, RawStatement, add, get_variable import esphome.final_validate as fv from esphome.helpers import cpp_string_escape, fnv1_hash_object_id, sanitize, snake_case @@ -200,6 +204,11 @@ def register_device_class(value: str) -> int: def register_unit_of_measurement(value: str) -> int: """Register a unit_of_measurement string and return its 1-based index.""" + if value and len(value) > UNIT_OF_MEASUREMENT_MAX_LENGTH: + raise ValueError( + f"Unit of measurement string too long ({len(value)} chars, " + f"max {UNIT_OF_MEASUREMENT_MAX_LENGTH}): '{value}'" + ) return _register_string(value, _get_pool().units, _MAX_UNITS, "unit_of_measurement") diff --git a/esphome/core/finite_set_mask.h b/esphome/core/finite_set_mask.h index f9cd0377c7..616c69353d 100644 --- a/esphome/core/finite_set_mask.h +++ b/esphome/core/finite_set_mask.h @@ -119,7 +119,7 @@ template(mask)); + } else if constexpr (sizeof(bitmask_t) <= sizeof(uint32_t)) { + bit = __builtin_ctzl(static_cast(mask)); + } else { + bit = __builtin_ctzll(static_cast(mask)); + } + return bit < BitPolicy::MAX_BITS ? bit : BitPolicy::MAX_BITS; +#else + int bit = 0; while (bit < BitPolicy::MAX_BITS && !(mask & (static_cast(1) << bit))) { ++bit; } return bit; +#endif } protected: diff --git a/esphome/core/helpers.cpp b/esphome/core/helpers.cpp index 1732fc72e8..5940f6ec98 100644 --- a/esphome/core/helpers.cpp +++ b/esphome/core/helpers.cpp @@ -22,6 +22,19 @@ namespace esphome { static const char *const TAG = "helpers"; +__attribute__((noinline, cold)) void *callback_manager_grow(void *data, uint16_t size, uint16_t &capacity, + size_t elem_size) { + ESPHOME_DEBUG_ASSERT(size < UINT16_MAX); + uint16_t new_cap = size + 1; + auto *new_data = ::operator new(new_cap *elem_size); + if (data) { + __builtin_memcpy(new_data, data, size * elem_size); + ::operator delete(data); + } + capacity = new_cap; + return new_data; +} + static const uint16_t CRC16_A001_LE_LUT_L[] = {0x0000, 0xc0c1, 0xc181, 0x0140, 0xc301, 0x03c0, 0x0280, 0xc241, 0xc601, 0x06c0, 0x0780, 0xc741, 0x0500, 0xc5c1, 0xc481, 0x0440}; static const uint16_t CRC16_A001_LE_LUT_H[] = {0x0000, 0xcc01, 0xd801, 0x1400, 0xf001, 0x3c00, 0x2800, 0xe401, diff --git a/esphome/core/helpers.h b/esphome/core/helpers.h index 82c6b3833c..c26bbe17b7 100644 --- a/esphome/core/helpers.h +++ b/esphome/core/helpers.h @@ -2,6 +2,7 @@ #include #include +#include #include #include #include @@ -497,6 +498,24 @@ template::max()> index_type capacity_{0}; }; +/// Initialize a std::array from an initializer_list. Uses memcpy for trivially copyable types (optimal codegen), +/// falls back to element-wise copy for non-trivially copyable types (e.g. TemplatableValue). +/// N is always set by code generation — the caller is responsible for ensuring src.size() == N. +/// The debug assert is a safety net for development, not a runtime check. +template inline void init_array_from(std::array &dest, std::initializer_list src) { +#ifdef ESPHOME_DEBUG + assert(src.size() == N); +#endif + if constexpr (std::is_trivially_copyable_v) { + __builtin_memcpy(dest.data(), src.begin(), N * sizeof(T)); + } else { + size_t i = 0; + for (const auto &v : src) { + dest[i++] = v; + } + } +} + /// Fixed-capacity vector - allocates once at runtime, never reallocates /// This avoids std::vector template overhead (_M_realloc_insert, _M_default_append) /// when size is known at initialization but not at compile time @@ -1782,19 +1801,96 @@ template struct Callback { } }; +/// Grow a CallbackManager's backing array to exactly size+1. Defined in helpers.cpp. +void *callback_manager_grow(void *data, uint16_t size, uint16_t &capacity, size_t elem_size); + template class CallbackManager; /** Helper class to allow having multiple subscribers to a callback. + * + * Uses a trivial-copyable-specialized container instead of std::vector to avoid + * template bloat (_M_realloc_insert, exception-safe copies). Since Callback is + * trivially copyable (just {fn_ptr, ctx_ptr}), reallocation is a plain memcpy. + * Uses uint16_t for size/capacity (8 bytes on 32-bit vs 12 for std::vector). + * Grows to exact size on each add — callbacks are registered during setup() + * and most instances have only 1-2 callbacks, so slack capacity is wasteful. * * @tparam Ts The arguments for the callbacks, wrapped in void(). */ template class CallbackManager { + using CbType = Callback; + static_assert(std::is_trivially_copyable_v, "Callback must be trivially copyable"); + public: + CallbackManager() = default; + ~CallbackManager() { ::operator delete(this->data_); } + + // Non-copyable (would alias data_), movable (for std::map support) + CallbackManager(const CallbackManager &) = delete; + CallbackManager &operator=(const CallbackManager &) = delete; + CallbackManager(CallbackManager &&other) noexcept + : data_(other.data_), size_(other.size_), capacity_(other.capacity_) { + other.data_ = nullptr; + other.size_ = 0; + other.capacity_ = 0; + } + CallbackManager &operator=(CallbackManager &&other) noexcept { + std::swap(this->data_, other.data_); + std::swap(this->size_, other.size_); + std::swap(this->capacity_, other.capacity_); + return *this; + } + /// Add any callable. Small trivially-copyable callables (like [this] lambdas) /// are stored inline without heap allocation or std::function. + template void add(F &&callback) { this->add_(CbType::create(std::forward(callback))); } + + /// Call all callbacks in this manager. + inline void ESPHOME_ALWAYS_INLINE call(Ts... args) { + if (this->size_ != 0) { + for (auto *it = this->data_, *end = it + this->size_; it != end; ++it) { + it->call(args...); + } + } + } + uint16_t size() const { return this->size_; } + + /// Call all callbacks in this manager. + void operator()(Ts... args) { this->call(args...); } + + protected: + template friend class LazyCallbackManager; + /// Non-template core to avoid code duplication per lambda type. + /// Inline fast path; cold growth path is in helpers.cpp via callback_manager_grow(). + void add_(CbType cb) { + if (this->size_ == this->capacity_) { + this->data_ = + static_cast(callback_manager_grow(this->data_, this->size_, this->capacity_, sizeof(CbType))); + } + this->data_[this->size_++] = cb; + } + CbType *data_{nullptr}; + uint16_t size_{0}; + uint16_t capacity_{0}; +}; + +/** CallbackManager backed by StaticVector for compile-time-known callback counts. + * + * Drop-in replacement for CallbackManager that avoids std::vector template bloat + * (_M_realloc_insert, etc.) when the maximum number of callbacks is known at compile time. + * + * @tparam N Maximum number of callbacks (compile-time constant, typically from cg.add_define()) + * @tparam Ts The arguments for the callbacks, wrapped in void(). + */ +template class StaticCallbackManager; + +template class StaticCallbackManager { + public: + /// Add any callable. Small trivially-copyable callables (like [this] lambdas) + /// are stored inline without heap allocation. template void add(F &&callback) { this->add_(Callback::create(std::forward(callback))); } - /// Call all callbacks in this manager. No null check on invoke. + /// Call all callbacks in this manager. void call(Ts... args) { for (auto &cb : this->callbacks_) cb.call(args...); @@ -1805,10 +1901,9 @@ template class CallbackManager { void operator()(Ts... args) { call(args...); } protected: - template friend class LazyCallbackManager; /// Non-template core to avoid code duplication per lambda type. void add_(Callback cb) { this->callbacks_.push_back(cb); } - std::vector> callbacks_; + StaticVector, N> callbacks_; }; template class LazyCallbackManager; @@ -1820,7 +1915,7 @@ template class LazyCallbackManager; * from API and web_server components). * * Memory overhead comparison (32-bit systems): - * - CallbackManager: 12 bytes (empty std::vector) + * - CallbackManager: 8 bytes (pointer + uint16 size + uint16 capacity) * - LazyCallbackManager: 4 bytes (nullptr pointer) * * Uses plain pointer instead of unique_ptr to avoid template instantiation overhead. diff --git a/esphome/core/lwip_fast_select.c b/esphome/core/lwip_fast_select.c index a695fa396b..bb3acbafcb 100644 --- a/esphome/core/lwip_fast_select.c +++ b/esphome/core/lwip_fast_select.c @@ -63,12 +63,12 @@ // // Shared state and safety rationale: // -// s_main_loop_task (TaskHandle_t, 4 bytes): -// Written once by main loop in init(). Read by TCP/IP thread (in callback) -// and background tasks (in wake). -// Safe: write-once-then-read pattern. Socket hooks may run before init(), -// but the NULL check on s_main_loop_task in the callback provides correct -// degraded behavior — notifications are simply skipped until init() completes. +// esphome_main_task_handle (TaskHandle_t, 4 bytes, defined in main_task.c): +// Written once by main loop in Application::setup(). Read by TCP/IP thread +// (in callback) and background tasks (in wake). +// Safe: write-once-then-read pattern. Socket hooks may run before setup(), +// but the NULL check on esphome_main_task_handle in the callback provides correct +// degraded behavior — notifications are simply skipped until setup() completes. // // s_original_callback (netconn_callback, 4-byte function pointer): // Written by main loop in hook_socket() (only when NULL — set once). @@ -123,15 +123,10 @@ #endif #include "esphome/core/lwip_fast_select.h" +#include "esphome/core/main_task.h" #include -// IRAM_ATTR is defined by esp_attr.h (included via FreeRTOS headers) on ESP32. -// On LibreTiny it's not defined — provide a no-op fallback. -#ifndef IRAM_ATTR -#define IRAM_ATTR -#endif - // Compile-time verification of thread safety assumptions. // On ESP32 (Xtensa/RISC-V) and LibreTiny (ARM Cortex-M), naturally-aligned // reads/writes up to 32 bits are atomic. @@ -157,8 +152,7 @@ _Static_assert(offsetof(struct lwip_sock, rcvevent) % sizeof(((struct lwip_sock _Static_assert(offsetof(struct lwip_sock, rcvevent) == ESPHOME_LWIP_SOCK_RCVEVENT_OFFSET, "lwip_sock.rcvevent offset changed — update ESPHOME_LWIP_SOCK_RCVEVENT_OFFSET in lwip_fast_select.h"); -// Task handle for the main loop — written once in init(), read from TCP/IP and background tasks. -static TaskHandle_t s_main_loop_task = NULL; +// Task handle is in main_task.c (esphome_main_task_handle) — shared with wake.h. // Saved original event_callback pointer — written once in first hook_socket(), read from TCP/IP task. static netconn_callback s_original_callback = NULL; @@ -177,15 +171,13 @@ static void esphome_socket_event_callback(struct netconn *conn, enum netconn_evt // (rcvevent++ with a NULL pbuf or error in recvmbox), so error conditions // already wake the main loop through the RCVPLUS path. if (evt == NETCONN_EVT_RCVPLUS) { - TaskHandle_t task = s_main_loop_task; + TaskHandle_t task = esphome_main_task_handle; if (task != NULL) { xTaskNotifyGive(task); } } } -void esphome_lwip_fast_select_init(void) { s_main_loop_task = xTaskGetCurrentTaskHandle(); } - // lwip_socket_dbg_get_socket() is a thin wrapper around the static // tryget_socket_unconn_nouse() — a direct array lookup without the refcount // that get_socket()/done_socket() uses. This is safe because: @@ -232,35 +224,4 @@ bool esphome_lwip_set_nodelay(struct lwip_sock *sock, bool enable) { return true; } -// Wake the main loop from another FreeRTOS task. NOT ISR-safe. -void esphome_lwip_wake_main_loop(void) { - TaskHandle_t task = s_main_loop_task; - if (task != NULL) { - xTaskNotifyGive(task); - } -} - -// Wake the main loop from an ISR. ISR-safe variant. -void IRAM_ATTR esphome_lwip_wake_main_loop_from_isr(int *px_higher_priority_task_woken) { - TaskHandle_t task = s_main_loop_task; - if (task != NULL) { - vTaskNotifyGiveFromISR(task, (BaseType_t *) px_higher_priority_task_woken); - } -} - -// Wake the main loop from any context (ISR, thread, or main loop). -// ESP32-only: uses xPortInIsrContext() to detect ISR context. -// LibreTiny is excluded because it lacks IRAM_ATTR support needed for ISR-safe paths. -#ifdef USE_ESP32 -void IRAM_ATTR esphome_lwip_wake_main_loop_any_context(void) { - if (xPortInIsrContext()) { - int px_higher_priority_task_woken = 0; - esphome_lwip_wake_main_loop_from_isr(&px_higher_priority_task_woken); - portYIELD_FROM_ISR(px_higher_priority_task_woken); - } else { - esphome_lwip_wake_main_loop(); - } -} -#endif - #endif // USE_LWIP_FAST_SELECT diff --git a/esphome/core/lwip_fast_select.h b/esphome/core/lwip_fast_select.h index 50706ba9f6..20ac191673 100644 --- a/esphome/core/lwip_fast_select.h +++ b/esphome/core/lwip_fast_select.h @@ -20,10 +20,6 @@ enum { ESPHOME_LWIP_SOCK_RCVEVENT_OFFSET = 8 }; extern "C" { #endif -/// Initialize fast select — must be called from the main loop task during setup(). -/// Saves the current task handle for xTaskNotifyGive() wake notifications. -void esphome_lwip_fast_select_init(void); - /// Look up a LwIP socket struct from a file descriptor. /// Returns NULL if fd is invalid or the socket/netconn is not initialized. /// Use this at registration time to cache the pointer for esphome_lwip_socket_has_data(). @@ -57,15 +53,6 @@ static inline bool esphome_lwip_socket_has_data(struct lwip_sock *sock) { /// The sock pointer must have been obtained from esphome_lwip_get_sock(). void esphome_lwip_hook_socket(struct lwip_sock *sock); -/// Wake the main loop task from another FreeRTOS task — costs <1 us. -/// NOT ISR-safe — must only be called from task context. -void esphome_lwip_wake_main_loop(void); - -/// Wake the main loop task from an ISR — costs <1 us. -/// ISR-safe variant using vTaskNotifyGiveFromISR(). -/// @param px_higher_priority_task_woken Set to pdTRUE if a context switch is needed. -void esphome_lwip_wake_main_loop_from_isr(int *px_higher_priority_task_woken); - /// Set or clear TCP_NODELAY on a socket's tcp_pcb directly. /// Must be called with the TCPIP core lock held (LwIPLock in C++). /// This bypasses lwip_setsockopt() overhead (socket lookups, switch cascade, @@ -73,13 +60,6 @@ void esphome_lwip_wake_main_loop_from_isr(int *px_higher_priority_task_woken); /// Returns true if successful, false if sock/conn/pcb is NULL or the socket is not TCP. bool esphome_lwip_set_nodelay(struct lwip_sock *sock, bool enable); -/// Wake the main loop task from any context (ISR, thread, or main loop). -/// ESP32-only: uses xPortInIsrContext() to detect ISR context. -/// LibreTiny lacks IRAM_ATTR support needed for ISR-safe paths. -#ifdef USE_ESP32 -void esphome_lwip_wake_main_loop_any_context(void); -#endif - #ifdef __cplusplus } #endif diff --git a/esphome/core/main_task.c b/esphome/core/main_task.c new file mode 100644 index 0000000000..52d9c2951a --- /dev/null +++ b/esphome/core/main_task.c @@ -0,0 +1,5 @@ +#include "esphome/core/main_task.h" + +#if defined(USE_ESP32) || defined(USE_LIBRETINY) +TaskHandle_t esphome_main_task_handle = NULL; +#endif diff --git a/esphome/core/main_task.h b/esphome/core/main_task.h new file mode 100644 index 0000000000..ed2885d2e2 --- /dev/null +++ b/esphome/core/main_task.h @@ -0,0 +1,55 @@ +#pragma once + +/// Main loop task handle and wake helpers — shared between wake.h (C++) and lwip_fast_select.c (C). +/// esphome_main_task_handle is set once during Application::setup() via xTaskGetCurrentTaskHandle(). + +#if defined(USE_ESP32) || defined(USE_LIBRETINY) + +#ifdef USE_ESP32 +#include +#include +#else +#include +#include +#endif + +#ifdef __cplusplus +extern "C" { +#endif + +extern TaskHandle_t esphome_main_task_handle; + +/// Wake the main loop task from another FreeRTOS task. NOT ISR-safe. +static inline void esphome_main_task_notify() { + TaskHandle_t task = esphome_main_task_handle; + if (task != NULL) { + xTaskNotifyGive(task); + } +} + +/// Wake the main loop task from an ISR. ISR-safe. +static inline void esphome_main_task_notify_from_isr(BaseType_t *px_higher_priority_task_woken) { + TaskHandle_t task = esphome_main_task_handle; + if (task != NULL) { + vTaskNotifyGiveFromISR(task, px_higher_priority_task_woken); + } +} + +#ifdef USE_ESP32 +/// Wake the main loop from any context (ISR or task). ESP32-only (needs xPortInIsrContext). +static inline void esphome_main_task_notify_any_context() { + if (xPortInIsrContext()) { + int px_higher_priority_task_woken = 0; + esphome_main_task_notify_from_isr(&px_higher_priority_task_woken); + portYIELD_FROM_ISR(px_higher_priority_task_woken); + } else { + esphome_main_task_notify(); + } +} +#endif + +#ifdef __cplusplus +} +#endif + +#endif // USE_ESP32 || USE_LIBRETINY diff --git a/esphome/core/scheduler.cpp b/esphome/core/scheduler.cpp index 9ee3b2fdd2..71b29390d6 100644 --- a/esphome/core/scheduler.cpp +++ b/esphome/core/scheduler.cpp @@ -214,8 +214,9 @@ void HOT Scheduler::set_timer_common_(Component *component, SchedulerItem::Type #endif /* ESPHOME_DEBUG_SCHEDULER */ } - // Common epilogue: atomic cancel-and-add (unless skip_cancel is true) - if (!skip_cancel) { + // Common epilogue: atomic cancel-and-add (unless skip_cancel is true or anonymous) + // Anonymous items (STATIC_STRING with nullptr) can never match anything, so skip the scan. + if (!skip_cancel && (name_type != NameType::STATIC_STRING || static_name != nullptr)) { this->cancel_item_locked_(component, name_type, static_name, hash_or_id, type, /* match_retry= */ false, /* find_first= */ true); } @@ -742,6 +743,23 @@ bool HOT Scheduler::cancel_item_(Component *component, NameType name_type, const // When find_first=false, cancels ALL matches across all containers (needed for // public cancel path where DelayAction parallel mode can create duplicates). // name_type determines matching: STATIC_STRING uses static_name, others use hash_or_id +size_t Scheduler::mark_matching_items_removed_slow_locked_(std::vector &container, + Component *component, NameType name_type, + const char *static_name, uint32_t hash_or_id, + SchedulerItem::Type type, bool match_retry, + bool find_first) { + size_t count = 0; + for (auto *item : container) { + if (this->matches_item_locked_(item, component, name_type, static_name, hash_or_id, type, match_retry)) { + this->set_item_removed_(item, true); + if (find_first) + return 1; + count++; + } + } + return count; +} + bool HOT Scheduler::cancel_item_locked_(Component *component, NameType name_type, const char *static_name, uint32_t hash_or_id, SchedulerItem::Type type, bool match_retry, bool find_first) { @@ -767,7 +785,7 @@ bool HOT Scheduler::cancel_item_locked_(Component *component, NameType name_type // The main loop may be executing an item's callback right now, and recycling // would destroy the callback while it's running (use-after-free). // Only the main loop in call() should recycle items after execution completes. - if (!this->items_.empty()) { + { size_t heap_cancelled = this->mark_matching_items_removed_locked_(this->items_, component, name_type, static_name, hash_or_id, type, match_retry, find_first); total_cancelled += heap_cancelled; diff --git a/esphome/core/scheduler.h b/esphome/core/scheduler.h index 1e44f41da8..43a3ec7049 100644 --- a/esphome/core/scheduler.h +++ b/esphome/core/scheduler.h @@ -495,23 +495,23 @@ class Scheduler { // name_type determines matching: STATIC_STRING uses static_name, others use hash_or_id // Returns the number of items marked for removal. // IMPORTANT: Must be called with scheduler lock held - __attribute__((noinline)) size_t mark_matching_items_removed_locked_(std::vector &container, - Component *component, NameType name_type, - const char *static_name, uint32_t hash_or_id, - SchedulerItem::Type type, bool match_retry, - bool find_first = false) { - size_t count = 0; - for (auto *item : container) { - if (this->matches_item_locked_(item, component, name_type, static_name, hash_or_id, type, match_retry)) { - this->set_item_removed_(item, true); - if (find_first) - return 1; - count++; - } - } - return count; + // Inlined: the fast path (empty container) avoids calling the out-of-line scan. + inline size_t HOT mark_matching_items_removed_locked_(std::vector &container, Component *component, + NameType name_type, const char *static_name, + uint32_t hash_or_id, SchedulerItem::Type type, bool match_retry, + bool find_first = false) { + if (container.empty()) + return 0; + return this->mark_matching_items_removed_slow_locked_(container, component, name_type, static_name, hash_or_id, + type, match_retry, find_first); } + // Out-of-line slow path for mark_matching_items_removed_locked_ when container is non-empty. + // IMPORTANT: Must be called with scheduler lock held + __attribute__((noinline)) size_t mark_matching_items_removed_slow_locked_( + std::vector &container, Component *component, NameType name_type, const char *static_name, + uint32_t hash_or_id, SchedulerItem::Type type, bool match_retry, bool find_first); + Mutex lock_; std::vector items_; std::vector to_add_; diff --git a/esphome/core/time.cpp b/esphome/core/time.cpp index 650c61d37b..b6fc9b90ad 100644 --- a/esphome/core/time.cpp +++ b/esphome/core/time.cpp @@ -2,6 +2,9 @@ #include "helpers.h" #include +#ifdef USE_TIME_TIMEZONE +#include "esphome/components/time/posix_tz.h" +#endif namespace esphome { @@ -14,12 +17,59 @@ uint8_t days_in_month(uint8_t month, uint16_t year) { size_t ESPTime::strftime(char *buffer, size_t buffer_len, const char *format) { struct tm c_tm = this->to_c_tm(); +#ifdef USE_TIME_TIMEZONE + // ::strftime uses libc's internal timezone state for %Z and %z, but we + // eliminated setenv("TZ")/tzset() on embedded platforms to save flash. + // Substitute %Z and %z with correct values from our parsed timezone. + // Quick scan: does format contain %Z or %z (but not %%Z/%%z)? + bool needs_subst = false; + for (const char *p = format; *p; p++) { + if (*p == '%' && *(p + 1)) { + p++; + if (*p == '%') + continue; // %% is a literal %, skip + if (*p == 'Z' || *p == 'z') { + needs_subst = true; + break; + } + } + } + if (needs_subst) { + const auto &tz = time::get_global_tz(); + char designation[6]; // "+HHMM" + null + int32_t offset = c_tm.tm_isdst > 0 ? tz.dst_offset_seconds : tz.std_offset_seconds; + time::format_designation(offset, designation, sizeof(designation)); + + char modified[STRFTIME_BUFFER_SIZE]; + char *out = modified; + char *out_end = modified + sizeof(modified) - 1; + for (const char *p = format; *p && out < out_end; p++) { + if (*p == '%') { + if (*(p + 1) == '%') { + // %% → copy both percent signs (literal %) + *out++ = *p++; + if (out < out_end) + *out++ = *p; + } else if (*(p + 1) == 'Z' || *(p + 1) == 'z') { + p++; // skip the Z/z + for (const char *d = designation; *d && out < out_end; d++) + *out++ = *d; + } else { + *out++ = *p; + } + } else { + *out++ = *p; + } + } + *out = '\0'; + return ::strftime(buffer, buffer_len, modified, &c_tm); + } +#endif return ::strftime(buffer, buffer_len, format, &c_tm); } size_t ESPTime::strftime_to(std::span buffer, const char *format) { - struct tm c_tm = this->to_c_tm(); - size_t len = ::strftime(buffer.data(), buffer.size(), format, &c_tm); + size_t len = this->strftime(buffer.data(), buffer.size(), format); if (len > 0) { return len; } diff --git a/esphome/core/wake.cpp b/esphome/core/wake.cpp new file mode 100644 index 0000000000..b6b59b5990 --- /dev/null +++ b/esphome/core/wake.cpp @@ -0,0 +1,82 @@ +#include "esphome/core/wake.h" +#include "esphome/core/hal.h" + +#ifdef USE_ESP8266 +#include +#endif + +#ifdef USE_HOST +#include "esphome/core/application.h" +#include +#endif + +namespace esphome { + +// === ESP32 — IRAM_ATTR entry points === +#ifdef USE_ESP32 +void IRAM_ATTR wake_loop_isrsafe(BaseType_t *px_higher_priority_task_woken) { + esphome_main_task_notify_from_isr(px_higher_priority_task_woken); +} +void IRAM_ATTR wake_loop_any_context() { esphome_main_task_notify_any_context(); } +#endif + +// === ESP8266 / RP2040 === +#if defined(USE_ESP8266) || defined(USE_RP2040) +// NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables) +volatile bool g_main_loop_woke = false; +#endif + +#ifdef USE_ESP8266 +void IRAM_ATTR wake_loop_any_context() { wake_loop_impl(); } +#endif + +// === RP2040 — wakeable_delay (needs file-scope state for alarm callback) === +#ifdef USE_RP2040 +// NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables) +static volatile bool s_delay_expired = false; + +static int64_t alarm_callback_(alarm_id_t id, void *user_data) { + (void) id; + (void) user_data; + s_delay_expired = true; + __sev(); + return 0; +} + +namespace internal { +void wakeable_delay(uint32_t ms) { + if (ms == 0) { + yield(); + return; + } + if (g_main_loop_woke) { + g_main_loop_woke = false; + return; + } + s_delay_expired = false; + alarm_id_t alarm = add_alarm_in_ms(ms, alarm_callback_, nullptr, true); + if (alarm <= 0) { + delay(ms); + return; + } + while (!g_main_loop_woke && !s_delay_expired) { + __wfe(); + } + if (!s_delay_expired) + cancel_alarm(alarm); + g_main_loop_woke = false; +} +} // namespace internal +#endif // USE_RP2040 + +// === Host (UDP loopback socket) === +#ifdef USE_HOST +void wake_loop_threadsafe() { + if (App.wake_socket_fd_ >= 0) { + const char dummy = 1; + ::send(App.wake_socket_fd_, &dummy, 1, 0); + } +} +#endif + +} // namespace esphome diff --git a/esphome/core/wake.h b/esphome/core/wake.h new file mode 100644 index 0000000000..a8c9b7ad08 --- /dev/null +++ b/esphome/core/wake.h @@ -0,0 +1,126 @@ +#pragma once + +/// @file wake.h +/// Platform-specific main loop wake primitives. +/// Always available on all platforms — no opt-in needed. + +#include "esphome/core/defines.h" +#include "esphome/core/hal.h" + +#if defined(USE_ESP32) || defined(USE_LIBRETINY) +#include "esphome/core/main_task.h" +#endif +#ifdef USE_ESP8266 +#include +#elif defined(USE_RP2040) +#include +#include +#endif + +namespace esphome { + +// === Wake flag for ESP8266/RP2040 === +#if defined(USE_ESP8266) || defined(USE_RP2040) +// NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables) +extern volatile bool g_main_loop_woke; +#endif + +// === ESP32 / LibreTiny (FreeRTOS) === +#if defined(USE_ESP32) || defined(USE_LIBRETINY) + +#ifdef USE_ESP32 +/// IRAM_ATTR entry point — defined in wake.cpp. +void wake_loop_isrsafe(BaseType_t *px_higher_priority_task_woken); +/// IRAM_ATTR entry point — defined in wake.cpp. +void wake_loop_any_context(); +#else +/// LibreTiny: IRAM_ATTR is not functional and the FreeRTOS port does not +/// provide vTaskNotifyGiveFromISR/portYIELD_FROM_ISR, so ISR-safe wake +/// is not possible. xTaskNotifyGive is used as the best available option. +inline void wake_loop_any_context() { esphome_main_task_notify(); } +#endif + +inline void wake_loop_threadsafe() { esphome_main_task_notify(); } + +namespace internal { +inline void wakeable_delay(uint32_t ms) { + if (ms == 0) { + yield(); + return; + } + ulTaskNotifyTake(pdTRUE, pdMS_TO_TICKS(ms)); +} +} // namespace internal + +// === ESP8266 === +#elif defined(USE_ESP8266) + +/// Inline implementation — IRAM callers inline this directly. +inline void ESPHOME_ALWAYS_INLINE wake_loop_impl() { + g_main_loop_woke = true; + esp_schedule(); +} + +/// IRAM_ATTR entry point for ISR callers — defined in wake.cpp. +void wake_loop_any_context(); + +/// Non-ISR: always inline. +inline void wake_loop_threadsafe() { wake_loop_impl(); } + +namespace internal { +inline void wakeable_delay(uint32_t ms) { + if (ms == 0) { + delay(0); + return; + } + if (g_main_loop_woke) { + g_main_loop_woke = false; + return; + } + esp_delay(ms, []() { return !g_main_loop_woke; }); +} +} // namespace internal + +// === RP2040 === +#elif defined(USE_RP2040) + +inline void wake_loop_any_context() { + g_main_loop_woke = true; + __sev(); +} + +inline void wake_loop_threadsafe() { wake_loop_any_context(); } + +/// RP2040 wakeable delay uses file-scope state (alarm callback + flag) — defined in wake.cpp. +namespace internal { +void wakeable_delay(uint32_t ms); +} // namespace internal + +// === Host / Zephyr / other === +#else + +#ifdef USE_HOST +/// Host: wakes select() via UDP loopback socket. Defined in wake.cpp. +void wake_loop_threadsafe(); +#else +/// Zephyr is currently the only platform without a wake mechanism. +/// wake_loop_threadsafe() is a no-op and wakeable_delay() falls back to delay(). +/// TODO: implement proper Zephyr wake using k_poll / k_sem or similar. +inline void wake_loop_threadsafe() {} +#endif + +inline void wake_loop_any_context() { wake_loop_threadsafe(); } + +namespace internal { +inline void wakeable_delay(uint32_t ms) { + if (ms == 0) { + yield(); + return; + } + delay(ms); +} +} // namespace internal + +#endif + +} // namespace esphome diff --git a/esphome/cpp_helpers.py b/esphome/cpp_helpers.py index 8f8c693140..479090016f 100644 --- a/esphome/cpp_helpers.py +++ b/esphome/cpp_helpers.py @@ -1,3 +1,4 @@ +from dataclasses import dataclass, field import logging from esphome.const import ( @@ -7,15 +8,132 @@ from esphome.const import ( CONF_UPDATE_INTERVAL, KEY_PAST_SAFE_MODE, ) -from esphome.core import CORE, ID, coroutine +from esphome.core import CORE, ID, CoroPriority, coroutine, coroutine_with_priority from esphome.coroutine import FakeAwaitable -from esphome.cpp_generator import LogStringLiteral, add, add_define, get_variable +from esphome.cpp_generator import ( + RawStatement, + add, + add_define, + add_global, + get_variable, +) from esphome.cpp_types import App +from esphome.helpers import cpp_string_escape from esphome.types import ConfigFragmentType, ConfigType from esphome.util import Registry, RegistryEntry _LOGGER = logging.getLogger(__name__) +_COMPONENT_SOURCE_DOMAIN = "component_source_pool" + +# Maximum unique component source names (8-bit index, 0 = not set) +_MAX_COMPONENT_SOURCES = 0xFF # 255 + + +@dataclass +class ComponentSourcePool: + """Pool of component source names for PROGMEM lookup table. + + Source names are registered during to_code() and assigned 1-based indices. + Index 0 means "not set" (returns LOG_STR("")). At render time, + the pool generates a C++ PROGMEM table + lookup function. + """ + + sources: dict[str, int] = field(default_factory=dict) + table_registered: bool = False + + +def _get_source_pool() -> ComponentSourcePool: + """Get or create the component source pool from CORE.data.""" + if _COMPONENT_SOURCE_DOMAIN not in CORE.data: + CORE.data[_COMPONENT_SOURCE_DOMAIN] = ComponentSourcePool() + return CORE.data[_COMPONENT_SOURCE_DOMAIN] + + +def _ensure_source_table_registered() -> None: + """Schedule the table generation job (once).""" + pool = _get_source_pool() + if pool.table_registered: + return + pool.table_registered = True + CORE.add_job(_generate_component_source_table) + + +def register_component_source(name: str) -> int: + """Register a component source name and return its 1-based index. + + Deduplicates: multiple components from the same source share one index. + """ + if not name: + return 0 + pool = _get_source_pool() + if name in pool.sources: + return pool.sources[name] + idx = len(pool.sources) + 1 + if idx > _MAX_COMPONENT_SOURCES: + if not CORE.testing_mode: + _LOGGER.warning( + "Too many unique component source names (max %d), " + "'%s' will show as ''", + _MAX_COMPONENT_SOURCES, + name, + ) + return 0 + pool.sources[name] = idx + _ensure_source_table_registered() + return idx + + +def _generate_source_table_code( + table_var: str, + lookup_fn: str, + strings: dict[str, int], +) -> str: + """Generate C++ PROGMEM table + LogString* lookup for component sources. + + Same pattern as entity_helpers._generate_category_code but returns + const LogString* instead of const char* (needed for LOG_STR_ARG). + """ + if not strings: + return "" + + sorted_strings = sorted(strings.items(), key=lambda x: x[1]) + count = len(sorted_strings) + + # Emit individual PROGMEM char arrays so string data lives in flash on ESP8266 + lines: list[str] = [] + var_names: list[str] = [] + for i, (s, _) in enumerate(sorted_strings): + var_name = f"{table_var}_STR_{i}" + var_names.append(var_name) + lines.append( + f"static const char {var_name}[] PROGMEM = {cpp_string_escape(s)};" + ) + + entries = ", ".join(var_names) + lines.append(f"static const char *const {table_var}[] PROGMEM = {{{entries}}};") + lines.append(f"const LogString *{lookup_fn}(uint8_t index) {{") + lines.append(f' if (index == 0 || index > {count}) return LOG_STR("");') + lines.append(" return reinterpret_cast(") + lines.append(f" progmem_read_ptr(&{table_var}[index - 1]));") + lines.append("}") + return "\n".join(lines) + "\n" + + +@coroutine_with_priority(CoroPriority.FINAL) +async def _generate_component_source_table() -> None: + """Generate the component source lookup table as a FINAL-priority job. + + Runs after all component to_code() calls have registered their sources. + """ + pool = _get_source_pool() + if code := _generate_source_table_code( + "COMP_SRC_TABLE", "component_source_lookup", pool.sources + ): + add_global( + RawStatement(f"namespace esphome {{\n{code}}} // namespace esphome") + ) + async def gpio_pin_expression(conf): """Generate an expression for the given pin option. @@ -77,7 +195,8 @@ async def register_component(var, config): "Error while finding name of component, please report this", exc_info=e ) if name is not None: - add(var.set_component_source(LogStringLiteral(name))) + idx = register_component_source(name) + add(var.set_component_source_(idx)) add(App.register_component_(var)) diff --git a/esphome/espota2.py b/esphome/espota2.py index c412bb51ff..39f51e02e9 100644 --- a/esphome/espota2.py +++ b/esphome/espota2.py @@ -40,6 +40,8 @@ RESPONSE_ERROR_ESP8266_NOT_ENOUGH_SPACE = 0x88 RESPONSE_ERROR_ESP32_NOT_ENOUGH_SPACE = 0x89 RESPONSE_ERROR_NO_UPDATE_PARTITION = 0x8A RESPONSE_ERROR_MD5_MISMATCH = 0x8B +RESPONSE_ERROR_RP2040_NOT_ENOUGH_SPACE = 0x8C +RESPONSE_ERROR_SIGNATURE_INVALID = 0x8D RESPONSE_ERROR_UNKNOWN = 0xFF OTA_VERSION_1_0 = 1 @@ -128,7 +130,7 @@ def check_error(data: list[int] | bytes, expect: int | list[int] | None) -> None :param expect: Expected response code(s), None to skip validation. :raises OTAError: If an error code is detected or response doesn't match expected. """ - if not expect: + if expect is None: return if not data: raise OTAError( @@ -192,6 +194,12 @@ def check_error(data: list[int] | bytes, expect: int | list[int] | None) -> None "Error: Application MD5 code mismatch. Please try again " "or flash over USB with a good quality cable." ) + if dat == RESPONSE_ERROR_SIGNATURE_INVALID: + raise OTAError( + "Error: Firmware signature verification failed. The firmware was not signed " + "with the correct key. Ensure the signing key matches the one used to build " + "the firmware currently running on the device." + ) if dat == RESPONSE_ERROR_UNKNOWN: raise OTAError("Unknown error from ESP") if not isinstance(expect, (list, tuple)): @@ -270,7 +278,7 @@ def perform_ota( raise OTAError("ESP requests password, but no password given!") nonce_bytes = receive_exactly( - sock, nonce_size, f"{hash_name} authentication nonce", [], decode=False + sock, nonce_size, f"{hash_name} authentication nonce", None, decode=False ) assert isinstance(nonce_bytes, bytes) nonce = nonce_bytes.decode() diff --git a/esphome/external_files.py b/esphome/external_files.py index 72a3f33fdc..18b68fba08 100644 --- a/esphome/external_files.py +++ b/esphome/external_files.py @@ -1,6 +1,6 @@ from __future__ import annotations -from datetime import datetime +from datetime import UTC, datetime import logging from pathlib import Path @@ -27,8 +27,8 @@ def has_remote_file_changed(url: str, local_file_path: Path) -> bool: _LOGGER.debug("has_remote_file_changed: File exists at %s", local_file_path) try: local_modification_time = local_file_path.stat().st_mtime - local_modification_time_str = datetime.utcfromtimestamp( - local_modification_time + local_modification_time_str = datetime.fromtimestamp( + local_modification_time, tz=UTC ).strftime("%a, %d %b %Y %H:%M:%S GMT") headers = { diff --git a/esphome/git.py b/esphome/git.py index a45768b5cd..096ff483a7 100644 --- a/esphome/git.py +++ b/esphome/git.py @@ -102,6 +102,7 @@ def clone_or_update( username: str = None, password: str = None, submodules: list[str] | None = None, + subpath: Path | None = None, _recover_broken: bool = True, ) -> tuple[Path, Callable[[], None] | None]: key = f"{url}@{ref}" @@ -112,6 +113,9 @@ def clone_or_update( ) repo_dir = _compute_destination_path(key, domain) + if subpath: + repo_dir = repo_dir / subpath + if not repo_dir.is_dir(): _LOGGER.info("Cloning %s", key) _LOGGER.debug("Location: %s", repo_dir) diff --git a/esphome/idf_component.yml b/esphome/idf_component.yml index c44853969e..1e40fef2dc 100644 --- a/esphome/idf_component.yml +++ b/esphome/idf_component.yml @@ -3,6 +3,8 @@ dependencies: version: "7.4.2" esphome/esp-audio-libs: version: 2.0.4 + esphome/micro-flac: + version: 0.1.1 esphome/micro-opus: version: 0.3.6 espressif/esp-dsp: @@ -10,7 +12,7 @@ dependencies: espressif/esp-tflite-micro: version: 1.3.3~1 espressif/esp32-camera: - version: 2.1.5 + version: 2.1.6 espressif/mdns: version: 1.10.0 espressif/esp_wifi_remote: diff --git a/esphome/zeroconf.py b/esphome/zeroconf.py index dc4ca77eb4..dd45b58a6c 100644 --- a/esphome/zeroconf.py +++ b/esphome/zeroconf.py @@ -25,7 +25,7 @@ _BACKGROUND_TASKS: set[asyncio.Task] = set() class DashboardStatus: - def __init__(self, on_update: Callable[[dict[str, bool | None], []]]) -> None: + def __init__(self, on_update: Callable[[dict[str, bool | None]], None]) -> None: """Initialize the dashboard status.""" self.on_update = on_update diff --git a/requirements.txt b/requirements.txt index c74dd265c7..db4a2b19e0 100644 --- a/requirements.txt +++ b/requirements.txt @@ -10,21 +10,21 @@ tzdata>=2021.1 # from time pyserial==3.5 platformio==6.1.19 esptool==5.2.0 -click==8.3.1 +click==8.3.2 esphome-dashboard==20260210.0 -aioesphomeapi==44.8.0 +aioesphomeapi==44.11.1 zeroconf==0.148.0 puremagic==1.30 ruamel.yaml==0.19.1 # dashboard_import ruamel.yaml.clib==0.2.15 # dashboard_import esphome-glyphsets==0.2.0 -pillow==12.1.1 +pillow==12.2.0 resvg-py==0.2.6 freetype-py==2.5.1 jinja2==3.1.6 bleak==2.1.1 smpclient==6.0.0 -requests==2.33.0 +requests==2.33.1 # esp-idf >= 5.0 requires this pyparsing >= 3.0 diff --git a/requirements_test.txt b/requirements_test.txt index 3b277e214d..a191378dd7 100644 --- a/requirements_test.txt +++ b/requirements_test.txt @@ -1,6 +1,6 @@ pylint==4.0.5 flake8==7.3.0 # also change in .pre-commit-config.yaml when updating -ruff==0.15.8 # also change in .pre-commit-config.yaml when updating +ruff==0.15.9 # also change in .pre-commit-config.yaml when updating pyupgrade==3.21.2 # also change in .pre-commit-config.yaml when updating pre-commit diff --git a/script/api_protobuf/api_protobuf.py b/script/api_protobuf/api_protobuf.py index f2a11141af..526644842d 100755 --- a/script/api_protobuf/api_protobuf.py +++ b/script/api_protobuf/api_protobuf.py @@ -56,6 +56,10 @@ FILE_HEADER = """// This file was automatically generated with a tool. // See script/api_protobuf/api_protobuf.py """ +# Populated by main() before any TypeInfo creation. +# Maps enum type name (e.g. ".BluetoothDeviceRequestType") to max enum value. +_enum_max_values: dict[str, int] = {} + def indent_list(text: str, padding: str = " ") -> list[str]: """Indent each line of the given text with the specified padding.""" @@ -156,6 +160,16 @@ class TypeInfo(ABC): """Check if this field should always be encoded (skip zero/empty check).""" return get_field_opt(self._field, pb.force, False) + @property + def max_value(self) -> int | None: + """Get the max_value option for this field, or None if not set.""" + return get_field_opt(self._field, pb.max_value, None) + + @property + def max_data_length(self) -> int | None: + """Get the max_data_length option for this field, or None if not set.""" + return get_field_opt(self._field, pb.max_data_length, None) + @property def wire_type(self) -> WireType: """Get the wire type for the field.""" @@ -227,46 +241,63 @@ class TypeInfo(ABC): # eliminating the zero-check branch and encode_field_raw indirection. # {value} is replaced with the actual field expression. RAW_ENCODE_MAP: dict[str, str] = { - "encode_uint32": "buffer.encode_varint_raw({value});", - "encode_uint64": "buffer.encode_varint_raw_64({value});", - "encode_sint32": "buffer.encode_varint_raw(encode_zigzag32({value}));", - "encode_sint64": "buffer.encode_varint_raw_64(encode_zigzag64({value}));", - "encode_int64": "buffer.encode_varint_raw_64(static_cast({value}));", - "encode_bool": "buffer.write_raw_byte({value} ? 0x01 : 0x00);", + "encode_uint32": "ProtoEncode::encode_varint_raw(pos, {value});", + "encode_uint64": "ProtoEncode::encode_varint_raw_64(pos, {value});", + "encode_sint32": "ProtoEncode::encode_varint_raw_short(pos, encode_zigzag32({value}));", + "encode_sint64": "ProtoEncode::encode_varint_raw_64(pos, encode_zigzag64({value}));", + "encode_int64": "ProtoEncode::encode_varint_raw_64(pos, static_cast({value}));", + "encode_bool": "ProtoEncode::write_raw_byte(pos, {value} ? 0x01 : 0x00);", } def _encode_with_precomputed_tag(self, value_expr: str) -> str | None: - """Try to emit a precomputed-tag encode for a forced field. + """Try to emit a precomputed-tag encode for a field. + + For forced fields: emits raw tag + value unconditionally. + For non-forced fields with single-byte tag: emits inline zero-check + + raw tag + value, avoiding an outlined function call. Returns the raw encode string if the tag is a single byte and the encode_func has a known raw equivalent, or None otherwise. """ - if not self.force: - return None tag = self.calculate_tag() if tag >= 128: return None - raw_expr = self.RAW_ENCODE_MAP.get(self.encode_func) + max_val = self.max_value + # Only use RAW_ENCODE_MAP for forced fields or fields with max_value + raw_expr = None + if self.force or max_val is not None: + raw_expr = self.RAW_ENCODE_MAP.get(self.encode_func) if raw_expr is None: return None - return f"buffer.write_raw_byte({tag});\n{raw_expr.format(value=value_expr)}" + body = f"ProtoEncode::write_raw_byte(pos, {tag});\n{raw_expr.format(value=value_expr)}" + if self.force: + return body + # Non-forced with max_value: inline zero-check + raw encode + return f"if ({value_expr}) {{\n {body}\n}}" def _encode_bytes_with_precomputed_tag( - self, data_expr: str, len_expr: str + self, data_expr: str, len_expr: str, max_len: int | None = None ) -> str | None: """Try to emit a precomputed-tag encode for a forced bytes/string field. Returns the raw encode string if the tag is a single byte, or None. + When max_len < 128, uses direct byte write for the length varint. """ if not self.force: return None tag = self.calculate_tag() if tag >= 128: return None + # When max_len < 128, length varint is always 1 byte + len_encode = ( + f"ProtoEncode::write_raw_byte(pos, static_cast({len_expr}));" + if max_len is not None and max_len < 128 + else f"ProtoEncode::encode_varint_raw(pos, {len_expr});" + ) return ( - f"buffer.write_raw_byte({tag});\n" - f"buffer.encode_varint_raw({len_expr});\n" - f"buffer.encode_raw({data_expr}, {len_expr});" + f"ProtoEncode::write_raw_byte(pos, {tag});\n" + f"{len_encode}\n" + f"ProtoEncode::encode_raw(pos, {data_expr}, {len_expr});" ) @property @@ -274,8 +305,8 @@ class TypeInfo(ABC): if result := self._encode_with_precomputed_tag(f"this->{self.field_name}"): return result if self.force: - return f"buffer.{self.encode_func}({self.number}, this->{self.field_name}, true);" - return f"buffer.{self.encode_func}({self.number}, this->{self.field_name});" + return f"ProtoEncode::{self.encode_func}(pos, {self.number}, this->{self.field_name}, true);" + return f"ProtoEncode::{self.encode_func}(pos, {self.number}, this->{self.field_name});" encode_func = None @@ -346,6 +377,31 @@ class TypeInfo(ABC): value = value_expr or name return f"size += ProtoSize::{method}({field_id_size}, {value});" + def _get_single_byte_varint_size( + self, + name: str, + force: bool, + extra_expr: str | None = None, + zero_check: str | None = None, + ) -> str: + """Size calculation when the varint is guaranteed to be 1 byte. + + Used when max_value < 128 or fixed_array_size < 128. + The fixed part is field_id_size + 1 (tag + 1-byte varint). + + Args: + name: Expression to check for zero (non-force only) + force: Whether to skip the zero check + extra_expr: Additional variable expression to add (e.g., data length) + zero_check: Override expression for the zero check (e.g., "!x.empty()") + """ + fixed = self.calculate_field_id_size() + 1 + size_expr = f"{fixed} + {extra_expr}" if extra_expr else str(fixed) + if force: + return f"size += {size_expr};" + check = zero_check or name + return f"size += {check} ? {size_expr} : 0;" + @abstractmethod def get_size_calculation(self, name: str, force: bool = False) -> str: """Calculate the size needed for encoding this field. @@ -614,10 +670,10 @@ class Fixed32Type(TypeInfo): tag = self.calculate_tag() if self.force and tag < 128: # Emit combined tag+value write: precomputed tag + direct memcpy - return f"buffer.write_tag_and_fixed32({tag}, this->{self.field_name});" + return f"ProtoEncode::write_tag_and_fixed32(pos, {tag}, this->{self.field_name});" if self.force: - return f"buffer.{self.encode_func}({self.number}, this->{self.field_name}, true);" - return f"buffer.{self.encode_func}({self.number}, this->{self.field_name});" + return f"ProtoEncode::{self.encode_func}(pos, {self.number}, this->{self.field_name}, true);" + return f"ProtoEncode::{self.encode_func}(pos, {self.number}, this->{self.field_name});" def get_size_calculation(self, name: str, force: bool = False) -> str: field_id_size = self.calculate_field_id_size() @@ -691,8 +747,8 @@ class StringType(TypeInfo): ): return result if self.force: - return f"buffer.encode_string({self.number}, this->{self.field_name}_ref_, true);" - return f"buffer.encode_string({self.number}, this->{self.field_name}_ref_);" + return f"ProtoEncode::encode_string(pos, {self.number}, this->{self.field_name}_ref_, true);" + return f"ProtoEncode::encode_string(pos, {self.number}, this->{self.field_name}_ref_);" def dump(self, name): # If name is 'it', this is a repeated field element - always use string @@ -779,8 +835,8 @@ class MessageType(TypeInfo): @property def encode_content(self) -> str: - # encode_sub_message always encodes (uses backpatch), no force needed - return f"buffer.{self.encode_func}({self.number}, this->{self.field_name});" + # Sub-message encoding needs buffer for backpatch/sync + return f"ProtoEncode::{self.encode_func}(pos, buffer, {self.number}, this->{self.field_name});" @property def decode_length(self) -> str: @@ -861,8 +917,8 @@ class BytesType(TypeInfo): ): return result if self.force: - return f"buffer.encode_bytes({self.number}, this->{self.field_name}_ptr_, this->{self.field_name}_len_, true);" - return f"buffer.encode_bytes({self.number}, this->{self.field_name}_ptr_, this->{self.field_name}_len_);" + return f"ProtoEncode::encode_bytes(pos, {self.number}, this->{self.field_name}_ptr_, this->{self.field_name}_len_, true);" + return f"ProtoEncode::encode_bytes(pos, {self.number}, this->{self.field_name}_ptr_, this->{self.field_name}_len_);" def dump(self, name: str) -> str: ptr_dump = f"format_hex_pretty(this->{self.field_name}_ptr_, this->{self.field_name}_len_)" @@ -972,8 +1028,8 @@ class PointerToBytesBufferType(PointerToBufferTypeBase): ): return result if self.force: - return f"buffer.encode_bytes({self.number}, this->{self.field_name}, this->{self.field_name}_len, true);" - return f"buffer.encode_bytes({self.number}, this->{self.field_name}, this->{self.field_name}_len);" + return f"ProtoEncode::encode_bytes(pos, {self.number}, this->{self.field_name}, this->{self.field_name}_len, true);" + return f"ProtoEncode::encode_bytes(pos, {self.number}, this->{self.field_name}, this->{self.field_name}_len);" @property def decode_length_content(self) -> str | None: @@ -1020,15 +1076,21 @@ class PointerToStringBufferType(PointerToBufferTypeBase): @property def encode_content(self) -> str: + max_len = self.max_data_length + if max_len is not None and max_len < 128 and self.force: + tag = self.calculate_tag() + if tag < 128: + return f"ProtoEncode::encode_short_string_force(pos, {tag}, this->{self.field_name});" if result := self._encode_bytes_with_precomputed_tag( - f"this->{self.field_name}.c_str()", f"this->{self.field_name}.size()" + f"this->{self.field_name}.c_str()", + f"this->{self.field_name}.size()", ): return result if self.force: - return ( - f"buffer.encode_string({self.number}, this->{self.field_name}, true);" - ) - return f"buffer.encode_string({self.number}, this->{self.field_name});" + return f"ProtoEncode::encode_string(pos, {self.number}, this->{self.field_name}, true);" + return ( + f"ProtoEncode::encode_string(pos, {self.number}, this->{self.field_name});" + ) @property def decode_length_content(self) -> str | None: @@ -1046,7 +1108,16 @@ class PointerToStringBufferType(PointerToBufferTypeBase): return f'dump_field(out, ESPHOME_PSTR("{self.name}"), this->{self.field_name});' def get_size_calculation(self, name: str, force: bool = False) -> str: - return f"size += ProtoSize::calc_length({self.calculate_field_id_size()}, this->{self.field_name}.size());" + size_field = f"this->{self.field_name}.size()" + max_len = self.max_data_length + if max_len is not None and max_len < 128: + return self._get_single_byte_varint_size( + size_field, + force, + extra_expr=size_field, + zero_check=f"!this->{self.field_name}.empty()", + ) + return self._get_simple_size_calculation(size_field, force, "length") def get_estimated_size(self) -> int: return self.calculate_field_id_size() + 8 # field ID + 8 bytes typical string @@ -1191,13 +1262,14 @@ class FixedArrayBytesType(TypeInfo): @property def encode_content(self) -> str: + max_len = self.array_size if isinstance(self.array_size, int) else None if result := self._encode_bytes_with_precomputed_tag( - f"this->{self.field_name}", f"this->{self.field_name}_len" + f"this->{self.field_name}", f"this->{self.field_name}_len", max_len=max_len ): return result if self.force: - return f"buffer.encode_bytes({self.number}, this->{self.field_name}, this->{self.field_name}_len, true);" - return f"buffer.encode_bytes({self.number}, this->{self.field_name}, this->{self.field_name}_len);" + return f"ProtoEncode::encode_bytes(pos, {self.number}, this->{self.field_name}, this->{self.field_name}_len, true);" + return f"ProtoEncode::encode_bytes(pos, {self.number}, this->{self.field_name}, this->{self.field_name}_len);" def dump(self, name: str) -> str: return f"out.append(format_hex_pretty({name}, {name}_len));" @@ -1212,13 +1284,14 @@ class FixedArrayBytesType(TypeInfo): def get_size_calculation(self, name: str, force: bool = False) -> str: # Use the actual length stored in the _len field length_field = f"this->{self.field_name}_len" - field_id_size = self.calculate_field_id_size() - if force: - # For repeated fields, always calculate size (no zero check) - return f"size += ProtoSize::calc_length_force({field_id_size}, {length_field});" - # For non-repeated fields, length already checks for zero - return f"size += ProtoSize::calc_length({field_id_size}, {length_field});" + # When array_size < 128, length varint is always 1 byte + if isinstance(self.array_size, int) and self.array_size < 128: + return self._get_single_byte_varint_size( + length_field, force, extra_expr=length_field + ) + + return self._get_simple_size_calculation(length_field, force, "length") def get_estimated_size(self) -> int: # Estimate based on typical BLE advertisement size @@ -1245,6 +1318,9 @@ class UInt32Type(TypeInfo): return o def get_size_calculation(self, name: str, force: bool = False) -> str: + max_val = self.max_value + if max_val is not None and max_val < 128: + return self._get_single_byte_varint_size(name, force) return self._get_simple_size_calculation(name, force, "uint32") def get_estimated_size(self) -> int: @@ -1264,19 +1340,24 @@ class EnumType(TypeInfo): default_value = "" wire_type = WireType.VARINT # Uses wire type 0 + @property + def max_value(self) -> int | None: + """Get max_value from explicit annotation or auto-derive from enum definition.""" + explicit = super().max_value + if explicit is not None: + return explicit + return _enum_max_values.get(self._field.type_name) + @property def encode_func(self) -> str: return "encode_uint32" @property def encode_content(self) -> str: - if result := self._encode_with_precomputed_tag( - f"static_cast(this->{self.field_name})" - ): - return result + value_expr = f"static_cast(this->{self.field_name})" if self.force: - return f"buffer.{self.encode_func}({self.number}, static_cast(this->{self.field_name}), true);" - return f"buffer.{self.encode_func}({self.number}, static_cast(this->{self.field_name}));" + return f"ProtoEncode::{self.encode_func}(pos, {self.number}, {value_expr}, true);" + return f"ProtoEncode::{self.encode_func}(pos, {self.number}, {value_expr});" def dump(self, name: str) -> str: return f"out.append_p(proto_enum_to_string<{self.cpp_type}>({name}));" @@ -1286,6 +1367,9 @@ class EnumType(TypeInfo): return f"static_cast<{self.cpp_type}>({value})" def get_size_calculation(self, name: str, force: bool = False) -> str: + max_val = self.max_value + if max_val is not None and max_val < 128: + return self._get_single_byte_varint_size(name, force) return self._get_simple_size_calculation( name, force, "uint32", f"static_cast({name})" ) @@ -1439,11 +1523,13 @@ class FixedArrayRepeatedType(TypeInfo): def _encode_element(self, element: str) -> str: """Helper to generate encode statement for a single element.""" if isinstance(self._ti, EnumType): - return f"buffer.{self._ti.encode_func}({self.number}, static_cast({element}), true);" + return f"ProtoEncode::{self._ti.encode_func}(pos, {self.number}, static_cast({element}), true);" # Repeated message elements use encode_sub_message (force=true is default) if isinstance(self._ti, MessageType): - return f"buffer.encode_sub_message({self.number}, {element});" - return f"buffer.{self._ti.encode_func}({self.number}, {element}, true);" + return f"ProtoEncode::encode_sub_message(pos, buffer, {self.number}, {element});" + return ( + f"ProtoEncode::{self._ti.encode_func}(pos, {self.number}, {element}, true);" + ) @property def cpp_type(self) -> str: @@ -1767,11 +1853,13 @@ class RepeatedTypeInfo(TypeInfo): def _encode_element_call(self, element: str) -> str: """Helper to generate encode call for a single element.""" if isinstance(self._ti, EnumType): - return f"buffer.{self._ti.encode_func}({self.number}, static_cast({element}), true);" + return f"ProtoEncode::{self._ti.encode_func}(pos, {self.number}, static_cast({element}), true);" # Repeated message elements use encode_sub_message (force=true is default) if isinstance(self._ti, MessageType): - return f"buffer.encode_sub_message({self.number}, {element});" - return f"buffer.{self._ti.encode_func}({self.number}, {element}, true);" + return f"ProtoEncode::encode_sub_message(pos, buffer, {self.number}, {element});" + return ( + f"ProtoEncode::{self._ti.encode_func}(pos, {self.number}, {element}, true);" + ) @property def encode_content(self) -> str: @@ -1780,7 +1868,7 @@ class RepeatedTypeInfo(TypeInfo): # Special handling for const char* elements (when container_no_template contains "const char") if "const char" in self._container_no_template: o = f"for (const char *it : *this->{self.field_name}) {{\n" - o += f" buffer.{self._ti.encode_func}({self.number}, it, strlen(it), true);\n" + o += f" ProtoEncode::{self._ti.encode_func}(pos, {self.number}, it, strlen(it), true);\n" else: o = f"for (const auto &it : *this->{self.field_name}) {{\n" o += f" {self._encode_element_call('it')}\n" @@ -1844,17 +1932,27 @@ class RepeatedTypeInfo(TypeInfo): size_expr = f"{name}->size()" if self._use_pointer else f"{name}.size()" o += f" size += {size_expr} * {bytes_per_element};\n" else: - # Other types need the actual value + # Check if inner type produces a constant size (doesn't depend on value) + inner_size = self._ti.get_size_calculation("it", True) + if "it" not in inner_size: + # Constant size per element — use multiply instead of loop + # Extract the constant from "size += N;" + const_val = ( + inner_size.strip().removeprefix("size += ").removesuffix(";") + ) + size_expr = f"{name}->size()" if self._use_pointer else f"{name}.size()" + o += f" size += {size_expr} * {const_val};\n" # Special handling for const char* elements - if self._use_pointer and "const char" in self._container_no_template: + elif self._use_pointer and "const char" in self._container_no_template: field_id_size = self.calculate_field_id_size() o += f" for (const char *it : {container_ref}) {{\n" o += f" size += ProtoSize::calc_length_force({field_id_size}, strlen(it));\n" + o += " }\n" else: auto_ref = "" if self._ti_is_bool else "&" o += f" for (const auto {auto_ref}it : {container_ref}) {{\n" - o += f" {self._ti.get_size_calculation('it', True)}\n" - o += " }\n" + o += f" {inner_size}\n" + o += " }\n" o += "}" return o @@ -2355,15 +2453,19 @@ def build_message_type( # Only generate encode method if this message needs encoding and has fields if needs_encode and encode: - o = f"void {desc.name}::encode(ProtoWriteBuffer &buffer) const {{" - if len(encode) == 1 and len(encode[0]) + len(o) + 3 < 120: - o += f" {encode[0]} }}\n" - else: - o += "\n" - o += indent("\n".join(encode)) + "\n" - o += "}\n" + # Add PROTO_ENCODE_DEBUG_ARG after pos in all proto_* calls + encode_debug = [ + line.replace("(pos,", "(pos PROTO_ENCODE_DEBUG_ARG,") for line in encode + ] + o = f"uint8_t *{desc.name}::encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const {{\n" + o += " uint8_t *__restrict__ pos = buffer.get_pos();\n" + o += indent("\n".join(encode_debug)) + "\n" + o += " return pos;\n" + o += "}\n" cpp += o - prot = "void encode(ProtoWriteBuffer &buffer) const;" + prot = ( + "uint8_t *encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const;" + ) public_content.append(prot) # If no fields to encode or message doesn't need encoding, the default implementation in ProtoMessage will be used @@ -2723,6 +2825,11 @@ def main() -> None: file = d.file[0] + # Build enum max value map so EnumType can auto-derive max_value + for enum in file.enum_type: + if not enum.options.deprecated and enum.value: + _enum_max_values[f".{enum.name}"] = max(v.number for v in enum.value) + # Build dynamic ifdef mappings early so we can emit USE_API_VARINT64 before includes enum_ifdef_map, message_ifdef_map, message_source_map, used_messages = ( build_type_usage_map(file) diff --git a/script/build_language_schema.py b/script/build_language_schema.py index bea540dc63..09ff999901 100755 --- a/script/build_language_schema.py +++ b/script/build_language_schema.py @@ -67,19 +67,16 @@ def get_component_names(): # pylint: disable-next=redefined-outer-name,reimported from esphome.loader import CORE_COMPONENTS_PATH - component_names = ["esphome", "sensor", "esp32", "esp8266"] - skip_components = [] - - for d in CORE_COMPONENTS_PATH.iterdir(): - if ( - not d.name.startswith("__") - and d.is_dir() - and d.name not in component_names - and d.name not in skip_components - ): - component_names.append(d.name) - - return sorted(component_names) + # return sorted( + # ["esphome", "sensor", "esp32", "esp8266", "adc", "touchscreen", "xpt2046"] + # ) + return sorted( + [ + d.name + for d in CORE_COMPONENTS_PATH.iterdir() + if not d.name.startswith("__") and d.is_dir() + ] + ) def load_components(): @@ -120,39 +117,57 @@ from esphome.util import Registry # noqa: E402 # pylint: enable=wrong-import-position +def sort_obj(obj): + if isinstance(obj, dict): + return {k: sort_obj(v) for k, v in sorted(obj.items(), key=lambda x: str(x[0]))} + if isinstance(obj, list): + return [sort_obj(item) for item in obj] + return obj + + def write_file(name, obj): full_path = Path(args.output_path) / f"{name}.json" + sorted_obj = sort_obj(obj) if JSON_DUMP_PRETTY: - json_str = json.dumps(obj, indent=2) + json_str = json.dumps(sorted_obj, indent=2) else: - json_str = json.dumps(obj, separators=(",", ":")) + json_str = json.dumps(sorted_obj, separators=(",", ":")) write_file_if_changed(full_path, json_str) - print(f"Wrote {full_path}") def delete_extra_files(keep_names): output_path = Path(args.output_path) + count = 0 for d in output_path.iterdir(): if d.suffix == ".json" and d.stem not in keep_names: + count += 1 d.unlink() - print(f"Deleted {d}") + return count def register_module_schemas(key, module, manifest=None): + count = 0 for name, schema in module_schemas(module): + count += 1 register_known_schema(key, name, schema) - if manifest and manifest.multi_conf and S_CONFIG_SCHEMA in output[key][S_SCHEMAS]: + if ( + manifest + and manifest.multi_conf + and key in output + and S_CONFIG_SCHEMA in output[key][S_SCHEMAS] + ): # Multi conf should allow list of components # not sure about 2nd part of the if, might be useless config (e.g. as3935) output[key][S_SCHEMAS][S_CONFIG_SCHEMA]["is_list"] = True + return count def register_known_schema(module, name, schema): if module not in output: output[module] = {S_SCHEMAS: {}} config = convert_config(schema, f"{module}/{name}") - if S_TYPE not in config: + if S_TYPE not in config and name != "FINAL_VALIDATE_SCHEMA" and module != "core": print(f"Config var without type: {module}.{name}") output[module][S_SCHEMAS][name] = config @@ -175,14 +190,23 @@ def module_schemas(module): except OSError: # some empty __init__ files module_str = "" - schemas = {} + schemas = [] for m_attr_name in dir(module): m_attr_obj = getattr(module, m_attr_name) if is_convertible_schema(m_attr_obj): - schemas[module_str.find(m_attr_name)] = [m_attr_name, m_attr_obj] + # Find where the name is assigned in the module source to preserve + # definition order. Using ^NAME\s*= (multiline) targets assignments + # at column 0, so "CONFIG_SCHEMA" won't collide with "CONFIG_SCHEMA_BASE". + match = re.search( + r"^" + re.escape(m_attr_name) + r"\s*=", + module_str, + re.MULTILINE, + ) + pos = match.start() if match else -1 + schemas.append((pos, m_attr_name, m_attr_obj)) - for pos in sorted(schemas.keys()): - yield schemas[pos] + for _, name, obj in sorted(schemas, key=lambda x: x[0]): + yield name, obj found_registries = {} @@ -240,9 +264,16 @@ def add_module_registries(domain, module): if len(parts) == 2: reg_domain = parts[0] reg_entry_name = parts[1] - else: - reg_domain = ".".join([parts[1], parts[0]]) - reg_entry_name = parts[2] + elif len(parts) == 3: + # is a platform or a component? + if parts[0] in schema_core[S_PLATFORMS]: + reg_domain = ".".join([parts[1], parts[0]]) + reg_entry_name = parts[2] + elif parts[0] in schema_core[S_COMPONENTS]: + reg_domain = parts[0] + reg_entry_name = ".".join([parts[1], parts[2]]) + else: + print(f"registry {name} is unknown") if reg_domain not in output: output[reg_domain] = {} @@ -252,8 +283,6 @@ def add_module_registries(domain, module): attr_obj[name].schema, f"{reg_domain}/{reg_type}/{reg_entry_name}" ) - # print(f"{domain} - {attr_name} - {name}") - def do_pins(): # do pin registries @@ -330,6 +359,35 @@ def fix_font(): ) +def fix_globals(): + if "globals" not in output: + return + from esphome.components.globals import _NON_RESTORING_SCHEMA + + config = convert_config(_NON_RESTORING_SCHEMA, "globals/CONFIG_SCHEMA") + config["is_list"] = True + output["globals"][S_SCHEMAS][S_CONFIG_SCHEMA] = config + + +def fix_mapping(): + if "mapping" not in output: + return + from esphome.components.mapping import BASE_SCHEMA + + config = convert_config(BASE_SCHEMA, "mapping/CONFIG_SCHEMA") + output["mapping"][S_SCHEMAS][S_CONFIG_SCHEMA] = config + + +def fix_image(): + if "image" not in output: + return + from esphome.components.image import IMAGE_SCHEMA + + config = convert_config(IMAGE_SCHEMA, "image/CONFIG_SCHEMA") + config["is_list"] = True + output["image"][S_SCHEMAS][S_CONFIG_SCHEMA] = config + + def fix_menu(): if "display_menu_base" not in output: return @@ -355,7 +413,7 @@ def fix_menu(): # 4. Configure menu items inside as recursive menu = schemas["MENU_TYPES"][S_SCHEMA][S_CONFIG_VARS]["items"]["types"]["menu"] menu[S_CONFIG_VARS].pop("items") - menu[S_EXTENDS] = ["display_menu_base.MENU_TYPES"] + menu[S_EXTENDS].append("display_menu_base.MENU_TYPES") def get_logger_tags(): @@ -531,7 +589,6 @@ def shrink(): else: arr_s.pop(S_EXTENDS) arr_s |= key_s[S_SCHEMA] - print(x) # simple types should be spread on each component, # for enums so far these are logger.is_log_level, cover.validate_cover_state and pulse_counter.sensor.COUNT_MODE_SCHEMA @@ -580,6 +637,10 @@ def shrink(): domain_schemas[S_SCHEMAS].pop(schema_name) +def is_cv_invalid(schema): + return repr(schema).startswith(".validator") + + def build_schema(): print("Building schema") @@ -610,7 +671,8 @@ def build_schema(): output[domain] = {S_COMPONENTS: {}, S_SCHEMAS: {}} platforms[domain] = {} elif manifest.config_schema is not None: - # e.g. dallas + if is_cv_invalid(manifest.config_schema): + continue output[domain] = {S_SCHEMAS: {S_CONFIG_SCHEMA: {}}} # Generate platforms (e.g. sensor, binary_sensor, climate ) @@ -621,7 +683,9 @@ def build_schema(): # Generate components for domain, manifest in components.items(): if domain not in platforms: - if manifest.config_schema is not None: + if manifest.config_schema is not None and not is_cv_invalid( + manifest.config_schema + ): core_components[domain] = {} if len(manifest.dependencies) > 0: core_components[domain]["dependencies"] = manifest.dependencies @@ -630,14 +694,15 @@ def build_schema(): for platform in platforms: platform_manifest = get_platform(domain=platform, platform=domain) if platform_manifest is not None: - output[platform][S_COMPONENTS][domain] = {} - if len(platform_manifest.dependencies) > 0: - output[platform][S_COMPONENTS][domain]["dependencies"] = ( - platform_manifest.dependencies - ) - register_module_schemas( + count = register_module_schemas( f"{domain}.{platform}", platform_manifest.module, platform_manifest ) + if count > 0: + output[platform][S_COMPONENTS].setdefault(domain, {}) + if len(platform_manifest.dependencies) > 0: + output[platform][S_COMPONENTS][domain]["dependencies"] = ( + platform_manifest.dependencies + ) # Do registries add_module_registries("core", automation) @@ -657,6 +722,9 @@ def build_schema(): fix_remote_receiver() fix_script() fix_font() + fix_globals() + fix_mapping() + fix_image() add_logger_tags() shrink() fix_menu() @@ -677,12 +745,19 @@ def build_schema(): # bundle core inside esphome data["esphome"]["core"] = data.pop("core")["core"] + if GENERATED_ID_TYPES: + print( + "Unconsumed id_type matchers:", + [id_type for _, id_type in GENERATED_ID_TYPES], + ) + if args.check: # do not gen files return for c, s in data.items(): write_file(c, s) - delete_extra_files(data.keys()) + deleted = delete_extra_files(data.keys()) + print(f"Written {len(data.items())} deleted {deleted} files.") def is_convertible_schema(schema): @@ -711,6 +786,30 @@ def convert_config(schema, path): return converted +GENERATED_ID_TYPES = [ + ( + lambda p: p.startswith("i2c/CONFIG_SCHEMA/") and p.endswith("/id"), + {"class": "i2c::I2CBus", "parents": ["Component"]}, + ), + ( + lambda p: p == "uart/CONFIG_SCHEMA/val 1/ext0/all/id", + {"class": "uart::UARTComponent", "parents": ["Component"]}, + ), + ( + lambda p: p == "http_request/CONFIG_SCHEMA/val 1/ext0/all/id", + {"class": "http_request::HttpRequestComponent", "parents": ["Component"]}, + ), + ( + lambda p: ( + p + == "uptime.sensor/CONFIG_SCHEMA/type_timestamp/ext0/ext1/all/time_id/val 1" + ), + {}, + ), + (lambda p: p == "esp_ldo/action/voltage.adjust/all/all/id", {}), +] + + def convert(schema, config_var, path): """config_var can be a config_var or a schema: both are dicts config_var has a S_TYPE property, if this is S_SCHEMA, then it has a S_SCHEMA property @@ -718,9 +817,6 @@ def convert(schema, config_var, path): """ repr_schema = repr(schema) - if path.startswith("ads1115.sensor") and path.endswith("gain"): - print(path) - if repr_schema in known_schemas: schema_info = known_schemas[(repr_schema)] for schema_instance, name in schema_info: @@ -841,8 +937,6 @@ def convert(schema, config_var, path): schema({"delay": "1s"}) except cv.Invalid: config_var["has_required_var"] = True - else: - print("figure out " + path) elif schema_type == "effects": config_var[S_TYPE] = "registry" config_var["registry"] = "light.effects" @@ -879,8 +973,6 @@ def convert(schema, config_var, path): "id" ]["id_type"]["class"] config_var[S_TYPE] = "use_id" - else: - print("TODO deferred?") elif isinstance(data, str): # TODO: Figure out why pipsolar does this config_var["use_id_type"] = data @@ -890,23 +982,11 @@ def convert(schema, config_var, path): else: raise TypeError("Unknown extracted schema type") elif config_var.get("key") == "GeneratedID": - if path.startswith("i2c/CONFIG_SCHEMA/") and path.endswith("/id"): - config_var["id_type"] = { - "class": "i2c::I2CBus", - "parents": ["Component"], - } - elif path == "uart/CONFIG_SCHEMA/val 1/ext0/all/id": - config_var["id_type"] = { - "class": "uart::UARTComponent", - "parents": ["Component"], - } - elif path == "http_request/CONFIG_SCHEMA/val 1/ext0/all/id": - config_var["id_type"] = { - "class": "http_request::HttpRequestComponent", - "parents": ["Component"], - } - elif path == "pins/esp32/val 1/id": - config_var["id_type"] = "pin" + for i, (matcher, id_type) in enumerate(GENERATED_ID_TYPES): + if matcher(path): + config_var["id_type"] = id_type + GENERATED_ID_TYPES.pop(i) + break else: print("Cannot determine id_type for " + path) diff --git a/script/ci-custom.py b/script/ci-custom.py index 7d0680a491..ad39f92005 100755 --- a/script/ci-custom.py +++ b/script/ci-custom.py @@ -1006,6 +1006,18 @@ def lint_log_in_header(fname, line, col, content): ) +@lint_content_find_check( + "FINAL_VALIDATE_SCHEMA", + include=["esphome/core/*.py"], + exclude=["esphome/core/entity_helpers.py"], +) +def lint_final_validate_in_core(fname, line, col, content): + return ( + "FINAL_VALIDATE_SCHEMA in esphome/core/ is not picked up by the component loader. " + "Use CoreFinalValidateStep in esphome/config.py instead." + ) + + def main(): colorama.init() diff --git a/script/cpp_benchmark.py b/script/cpp_benchmark.py index a54d3752df..92faa05819 100755 --- a/script/cpp_benchmark.py +++ b/script/cpp_benchmark.py @@ -21,6 +21,10 @@ BENCHMARKS_DIR: Path = Path(root_path) / "tests" / "benchmarks" / "components" # Path to /tests/benchmarks/core (always included, not a component) CORE_BENCHMARKS_DIR: Path = Path(root_path) / "tests" / "benchmarks" / "core" +# Stub headers for ESP32-only components (e.g. bluetooth_proxy) that +# allow benchmarks to compile on the host platform. +STUBS_DIR: Path = Path(root_path) / "tests" / "benchmarks" / "stubs" + PLATFORMIO_OPTIONS = { "build_unflags": [ "-Os", # remove default size-opt @@ -29,6 +33,7 @@ PLATFORMIO_OPTIONS = { "-O2", # optimize for speed (CodSpeed recommends RelWithDebInfo) "-g", # debug symbols for profiling "-DUSE_BENCHMARK", # disable WarnIfComponentBlockingGuard in finish() + f"-I{STUBS_DIR}", # stub headers for ESP32-only components ], # Use deep+ LDF mode to ensure PlatformIO detects the benchmark # library dependency from nested includes. diff --git a/script/helpers.py b/script/helpers.py index 290dcadf0b..c9c550d889 100644 --- a/script/helpers.py +++ b/script/helpers.py @@ -15,8 +15,6 @@ from typing import Any import colorama -from esphome.loader import get_platform - root_path = os.path.abspath(os.path.normpath(os.path.join(__file__, "..", ".."))) basepath = os.path.join(root_path, "esphome") temp_folder = os.path.join(root_path, ".temp") @@ -644,7 +642,7 @@ def get_all_dependencies( PLATFORM_HOST, ) from esphome.core import CORE - from esphome.loader import get_component + from esphome.loader import get_component, get_platform all_components: set[str] = set(component_names) diff --git a/script/run-in-env.py b/script/run-in-env.py index 886e65db27..9283ba9940 100755 --- a/script/run-in-env.py +++ b/script/run-in-env.py @@ -44,7 +44,8 @@ def find_and_activate_virtualenv(): def run_command(): # Execute the remaining arguments in the new environment if len(sys.argv) > 1: - subprocess.run(sys.argv[1:], check=False, close_fds=False) + result = subprocess.run(sys.argv[1:], check=False, close_fds=False) + sys.exit(result.returncode) else: print( "No command provided to run in the virtual environment.", diff --git a/tests/benchmarks/components/api/__init__.py b/tests/benchmarks/components/api/__init__.py index 0687c3f87f..eb86492964 100644 --- a/tests/benchmarks/components/api/__init__.py +++ b/tests/benchmarks/components/api/__init__.py @@ -1,3 +1,4 @@ +import esphome.codegen as cg from tests.testing_helpers import ComponentManifestOverride @@ -5,3 +6,16 @@ def override_manifest(manifest: ComponentManifestOverride) -> None: # api must run its to_code to define USE_API, USE_API_PLAINTEXT, # and add the noise-c library dependency. manifest.enable_codegen() + + original_to_code = manifest.to_code + + async def to_code(config): + await original_to_code(config) + # Enable BLE proto message types for benchmarks. The real + # bluetooth_proxy component is ESP32-only; a lightweight stub + # header in tests/benchmarks/stubs/ satisfies the include. + cg.add_define("USE_BLUETOOTH_PROXY") + cg.add_define("BLUETOOTH_PROXY_MAX_CONNECTIONS", 3) + cg.add_define("BLUETOOTH_PROXY_ADVERTISEMENT_BATCH_SIZE", 16) + + manifest.to_code = to_code diff --git a/tests/benchmarks/components/api/bench_helpers.h b/tests/benchmarks/components/api/bench_helpers.h new file mode 100644 index 0000000000..73e51bce3d --- /dev/null +++ b/tests/benchmarks/components/api/bench_helpers.h @@ -0,0 +1,67 @@ +#pragma once + +#include +#include +#include +#include +#include + +#include +#include + +#include "esphome/components/socket/socket.h" + +namespace esphome::api::benchmarks { + +// Helper to drain accumulated data from the read side of a socket +// to prevent the write side from blocking. +inline void drain_socket(int fd) { + char buf[65536]; + while (::read(fd, buf, sizeof(buf)) > 0) { + } +} + +// Create a TCP loopback socket pair. Returns the write-side Socket +// (wrapped for ESPHome) and the raw read-side fd for draining. +// Both ends are non-blocking with 16MB buffers. +inline std::pair, int> create_tcp_loopback() { + // Create a TCP listener on loopback + int listen_fd = ::socket(AF_INET, SOCK_STREAM, 0); + int opt = 1; + ::setsockopt(listen_fd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt)); + + struct sockaddr_in addr {}; + addr.sin_family = AF_INET; + addr.sin_addr.s_addr = htonl(INADDR_LOOPBACK); + addr.sin_port = 0; // OS-assigned port + ::bind(listen_fd, reinterpret_cast(&addr), sizeof(addr)); + ::listen(listen_fd, 1); + + // Get the assigned port + socklen_t addr_len = sizeof(addr); + ::getsockname(listen_fd, reinterpret_cast(&addr), &addr_len); + + // Connect from client side + int write_fd = ::socket(AF_INET, SOCK_STREAM, 0); + ::connect(write_fd, reinterpret_cast(&addr), sizeof(addr)); + + // Accept on server side (this is our read fd) + int read_fd = ::accept(listen_fd, nullptr, nullptr); + ::close(listen_fd); + + // Make both ends non-blocking + int flags = ::fcntl(write_fd, F_GETFL, 0); + ::fcntl(write_fd, F_SETFL, flags | O_NONBLOCK); + flags = ::fcntl(read_fd, F_GETFL, 0); + ::fcntl(read_fd, F_SETFL, flags | O_NONBLOCK); + + // Use large socket buffers so benchmarks never hit WOULD_BLOCK + // during a single outer iteration (2000 × ~15B messages = ~30KB). + int bufsize = 16 * 1024 * 1024; + ::setsockopt(write_fd, SOL_SOCKET, SO_SNDBUF, &bufsize, sizeof(bufsize)); + ::setsockopt(read_fd, SOL_SOCKET, SO_RCVBUF, &bufsize, sizeof(bufsize)); + + return {std::make_unique(write_fd), read_fd}; +} + +} // namespace esphome::api::benchmarks diff --git a/tests/benchmarks/components/api/bench_list_entities.cpp b/tests/benchmarks/components/api/bench_list_entities.cpp new file mode 100644 index 0000000000..02cef50d70 --- /dev/null +++ b/tests/benchmarks/components/api/bench_list_entities.cpp @@ -0,0 +1,235 @@ +#include + +#include "esphome/components/api/api_pb2.h" +#include "esphome/components/api/api_buffer.h" +#include "esphome/components/light/color_mode.h" + +namespace esphome::api::benchmarks { + +static constexpr int kInnerIterations = 2000; + +// --- ListEntitiesSensorResponse --- + +static ListEntitiesSensorResponse make_sensor_response() { + ListEntitiesSensorResponse msg; + msg.object_id = StringRef::from_lit("living_room_temperature"); + msg.key = 0x12345678; + msg.name = StringRef::from_lit("Living Room Temperature"); +#ifdef USE_ENTITY_ICON + msg.icon = StringRef::from_lit("mdi:thermometer"); +#endif + msg.entity_category = enums::ENTITY_CATEGORY_NONE; + msg.disabled_by_default = false; + msg.unit_of_measurement = StringRef::from_lit("°C"); + msg.accuracy_decimals = 1; + msg.force_update = false; + msg.device_class = StringRef::from_lit("temperature"); + msg.state_class = enums::STATE_CLASS_MEASUREMENT; +#ifdef USE_DEVICES + msg.device_id = 1; +#endif + return msg; +} + +static void CalculateSize_ListEntitiesSensorResponse(benchmark::State &state) { + auto msg = make_sensor_response(); + + for (auto _ : state) { + uint32_t result = 0; + for (int i = 0; i < kInnerIterations; i++) { + result += msg.calculate_size(); + } + benchmark::DoNotOptimize(result); + } + state.SetItemsProcessed(state.iterations() * kInnerIterations); +} +BENCHMARK(CalculateSize_ListEntitiesSensorResponse); + +static void Encode_ListEntitiesSensorResponse(benchmark::State &state) { + auto msg = make_sensor_response(); + APIBuffer buffer; + uint32_t size = msg.calculate_size(); + buffer.resize(size); + + for (auto _ : state) { + for (int i = 0; i < kInnerIterations; i++) { + ProtoWriteBuffer writer(&buffer, 0); + msg.encode(writer); + } + benchmark::DoNotOptimize(buffer.data()); + } + state.SetItemsProcessed(state.iterations() * kInnerIterations); +} +BENCHMARK(Encode_ListEntitiesSensorResponse); + +static void CalcAndEncode_ListEntitiesSensorResponse(benchmark::State &state) { + auto msg = make_sensor_response(); + APIBuffer buffer; + + for (auto _ : state) { + for (int i = 0; i < kInnerIterations; i++) { + uint32_t size = msg.calculate_size(); + buffer.resize(size); + ProtoWriteBuffer writer(&buffer, 0); + msg.encode(writer); + } + benchmark::DoNotOptimize(buffer.data()); + } + state.SetItemsProcessed(state.iterations() * kInnerIterations); +} +BENCHMARK(CalcAndEncode_ListEntitiesSensorResponse); + +// --- ListEntitiesBinarySensorResponse --- + +static ListEntitiesBinarySensorResponse make_binary_sensor_response() { + ListEntitiesBinarySensorResponse msg; + msg.object_id = StringRef::from_lit("front_door_contact"); + msg.key = 0xAABBCCDD; + msg.name = StringRef::from_lit("Front Door Contact"); +#ifdef USE_ENTITY_ICON + msg.icon = StringRef::from_lit("mdi:door"); +#endif + msg.entity_category = enums::ENTITY_CATEGORY_NONE; + msg.disabled_by_default = false; + msg.device_class = StringRef::from_lit("door"); + msg.is_status_binary_sensor = false; +#ifdef USE_DEVICES + msg.device_id = 2; +#endif + return msg; +} + +static void CalculateSize_ListEntitiesBinarySensorResponse(benchmark::State &state) { + auto msg = make_binary_sensor_response(); + + for (auto _ : state) { + uint32_t result = 0; + for (int i = 0; i < kInnerIterations; i++) { + result += msg.calculate_size(); + } + benchmark::DoNotOptimize(result); + } + state.SetItemsProcessed(state.iterations() * kInnerIterations); +} +BENCHMARK(CalculateSize_ListEntitiesBinarySensorResponse); + +static void Encode_ListEntitiesBinarySensorResponse(benchmark::State &state) { + auto msg = make_binary_sensor_response(); + APIBuffer buffer; + uint32_t size = msg.calculate_size(); + buffer.resize(size); + + for (auto _ : state) { + for (int i = 0; i < kInnerIterations; i++) { + ProtoWriteBuffer writer(&buffer, 0); + msg.encode(writer); + } + benchmark::DoNotOptimize(buffer.data()); + } + state.SetItemsProcessed(state.iterations() * kInnerIterations); +} +BENCHMARK(Encode_ListEntitiesBinarySensorResponse); + +static void CalcAndEncode_ListEntitiesBinarySensorResponse(benchmark::State &state) { + auto msg = make_binary_sensor_response(); + APIBuffer buffer; + + for (auto _ : state) { + for (int i = 0; i < kInnerIterations; i++) { + uint32_t size = msg.calculate_size(); + buffer.resize(size); + ProtoWriteBuffer writer(&buffer, 0); + msg.encode(writer); + } + benchmark::DoNotOptimize(buffer.data()); + } + state.SetItemsProcessed(state.iterations() * kInnerIterations); +} +BENCHMARK(CalcAndEncode_ListEntitiesBinarySensorResponse); + +// --- ListEntitiesLightResponse --- + +static light::ColorModeMask light_color_modes; +static FixedVector light_effects; + +static ListEntitiesLightResponse make_light_response() { + // Initialize static data on first call + static bool initialized = false; + if (!initialized) { + light_color_modes.insert(light::ColorMode::RGB_WHITE); + light_color_modes.insert(light::ColorMode::COLOR_TEMPERATURE); + light_effects.init(3); + light_effects.push_back("None"); + light_effects.push_back("Rainbow"); + light_effects.push_back("Strobe"); + initialized = true; + } + + ListEntitiesLightResponse msg; + msg.object_id = StringRef::from_lit("kitchen_ceiling_light"); + msg.key = 0x55667788; + msg.name = StringRef::from_lit("Kitchen Ceiling Light"); +#ifdef USE_ENTITY_ICON + msg.icon = StringRef::from_lit("mdi:ceiling-light"); +#endif + msg.entity_category = enums::ENTITY_CATEGORY_NONE; + msg.disabled_by_default = false; + msg.supported_color_modes = &light_color_modes; + msg.min_mireds = 153.0f; + msg.max_mireds = 500.0f; + msg.effects = &light_effects; +#ifdef USE_DEVICES + msg.device_id = 3; +#endif + return msg; +} + +static void CalculateSize_ListEntitiesLightResponse(benchmark::State &state) { + auto msg = make_light_response(); + + for (auto _ : state) { + uint32_t result = 0; + for (int i = 0; i < kInnerIterations; i++) { + result += msg.calculate_size(); + } + benchmark::DoNotOptimize(result); + } + state.SetItemsProcessed(state.iterations() * kInnerIterations); +} +BENCHMARK(CalculateSize_ListEntitiesLightResponse); + +static void Encode_ListEntitiesLightResponse(benchmark::State &state) { + auto msg = make_light_response(); + APIBuffer buffer; + uint32_t size = msg.calculate_size(); + buffer.resize(size); + + for (auto _ : state) { + for (int i = 0; i < kInnerIterations; i++) { + ProtoWriteBuffer writer(&buffer, 0); + msg.encode(writer); + } + benchmark::DoNotOptimize(buffer.data()); + } + state.SetItemsProcessed(state.iterations() * kInnerIterations); +} +BENCHMARK(Encode_ListEntitiesLightResponse); + +static void CalcAndEncode_ListEntitiesLightResponse(benchmark::State &state) { + auto msg = make_light_response(); + APIBuffer buffer; + + for (auto _ : state) { + for (int i = 0; i < kInnerIterations; i++) { + uint32_t size = msg.calculate_size(); + buffer.resize(size); + ProtoWriteBuffer writer(&buffer, 0); + msg.encode(writer); + } + benchmark::DoNotOptimize(buffer.data()); + } + state.SetItemsProcessed(state.iterations() * kInnerIterations); +} +BENCHMARK(CalcAndEncode_ListEntitiesLightResponse); + +} // namespace esphome::api::benchmarks diff --git a/tests/benchmarks/components/api/bench_plaintext_frame.cpp b/tests/benchmarks/components/api/bench_plaintext_frame.cpp index 79bffaf953..0caa50c748 100644 --- a/tests/benchmarks/components/api/bench_plaintext_frame.cpp +++ b/tests/benchmarks/components/api/bench_plaintext_frame.cpp @@ -2,12 +2,9 @@ #ifdef USE_API_PLAINTEXT #include -#include -#include -#include -#include #include +#include "bench_helpers.h" #include "esphome/components/api/api_frame_helper_plaintext.h" #include "esphome/components/api/api_pb2.h" #include "esphome/components/api/api_buffer.h" @@ -16,57 +13,12 @@ namespace esphome::api::benchmarks { static constexpr int kInnerIterations = 2000; -// Helper to drain accumulated data from the read side of a socket -// to prevent the write side from blocking. -static void drain_socket(int fd) { - char buf[65536]; - while (::read(fd, buf, sizeof(buf)) > 0) { - } -} - // Helper to create a TCP loopback connection with an APIPlaintextFrameHelper // on the write end. Returns the helper and the read-side fd. -// Uses real TCP sockets so TCP_NODELAY succeeds during init(). static std::pair, int> create_plaintext_helper() { - // Create a TCP listener on loopback - int listen_fd = ::socket(AF_INET, SOCK_STREAM, 0); - int opt = 1; - ::setsockopt(listen_fd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt)); - - struct sockaddr_in addr {}; - addr.sin_family = AF_INET; - addr.sin_addr.s_addr = htonl(INADDR_LOOPBACK); - addr.sin_port = 0; // OS-assigned port - ::bind(listen_fd, reinterpret_cast(&addr), sizeof(addr)); - ::listen(listen_fd, 1); - - // Get the assigned port - socklen_t addr_len = sizeof(addr); - ::getsockname(listen_fd, reinterpret_cast(&addr), &addr_len); - - // Connect from client side - int write_fd = ::socket(AF_INET, SOCK_STREAM, 0); - ::connect(write_fd, reinterpret_cast(&addr), sizeof(addr)); - - // Accept on server side (this is our read fd) - int read_fd = ::accept(listen_fd, nullptr, nullptr); - ::close(listen_fd); - - // Make both ends non-blocking - int flags = ::fcntl(write_fd, F_GETFL, 0); - ::fcntl(write_fd, F_SETFL, flags | O_NONBLOCK); - flags = ::fcntl(read_fd, F_GETFL, 0); - ::fcntl(read_fd, F_SETFL, flags | O_NONBLOCK); - - // Increase socket buffer sizes to reduce drain frequency - int bufsize = 1024 * 1024; - ::setsockopt(write_fd, SOL_SOCKET, SO_SNDBUF, &bufsize, sizeof(bufsize)); - ::setsockopt(read_fd, SOL_SOCKET, SO_RCVBUF, &bufsize, sizeof(bufsize)); - - auto sock = std::make_unique(write_fd); + auto [sock, read_fd] = create_tcp_loopback(); auto helper = std::make_unique(std::move(sock)); helper->init(); - return {std::move(helper), read_fd}; } @@ -97,9 +49,6 @@ static void PlaintextFrame_WriteSensorState(benchmark::State &state) { msg.encode(writer); helper->write_protobuf_packet(SensorStateResponse::MESSAGE_TYPE, writer); - - if ((i & 0xFF) == 0) - drain_socket(read_fd); } drain_socket(read_fd); benchmark::DoNotOptimize(helper.get()); @@ -144,9 +93,6 @@ static void PlaintextFrame_WriteBatch5(benchmark::State &state) { } helper->write_protobuf_messages(ProtoWriteBuffer(&buffer, 0), std::span(messages, 5)); - - if ((i & 0xFF) == 0) - drain_socket(read_fd); } drain_socket(read_fd); benchmark::DoNotOptimize(helper.get()); diff --git a/tests/benchmarks/components/api/bench_proto_decode.cpp b/tests/benchmarks/components/api/bench_proto_decode.cpp index 113201dd8a..961c629f2a 100644 --- a/tests/benchmarks/components/api/bench_proto_decode.cpp +++ b/tests/benchmarks/components/api/bench_proto_decode.cpp @@ -10,10 +10,9 @@ namespace esphome::api::benchmarks { // sub-microsecond benchmarks. static constexpr int kInnerIterations = 2000; -// Helper: encode a message into a buffer and return it. -// Benchmarks encode once in setup, then decode the resulting bytes in a loop. -// This keeps decode benchmarks in sync with the actual protobuf schema — -// hand-encoded byte arrays would silently break when fields change. +// Helper: encode a message into an APIBuffer for reuse in decode benchmarks. +// Optimization barriers are applied to the decode target objects via +// DoNotOptimize/ClobberMemory, not to this buffer. template static APIBuffer encode_message(const T &msg) { APIBuffer buffer; uint32_t size = msg.calculate_size(); @@ -23,6 +22,12 @@ template static APIBuffer encode_message(const T &msg) { return buffer; } +/// Force a pointer through an asm barrier so the compiler cannot +/// prove its contents are unchanged across iterations. +/// benchmark::DoNotOptimize/ClobberMemory are insufficient under +/// CodSpeed's valgrind-based instrumentation. +static void escape(void *p) { asm volatile("" : : "g"(p) : "memory"); } + // --- HelloRequest decode (string + varint fields) --- static void Decode_HelloRequest(benchmark::State &state) { @@ -31,13 +36,18 @@ static void Decode_HelloRequest(benchmark::State &state) { source.api_version_major = 1; source.api_version_minor = 10; auto encoded = encode_message(source); + auto *data = encoded.data(); + auto size = encoded.size(); + benchmark::DoNotOptimize(data); + benchmark::DoNotOptimize(size); for (auto _ : state) { - HelloRequest msg; for (int i = 0; i < kInnerIterations; i++) { - msg.decode(encoded.data(), encoded.size()); + HelloRequest msg; + escape(&msg); + msg.decode(data, size); + escape(&msg); } - benchmark::DoNotOptimize(msg.api_version_major); } state.SetItemsProcessed(state.iterations() * kInnerIterations); } @@ -50,13 +60,18 @@ static void Decode_SwitchCommandRequest(benchmark::State &state) { source.key = 0x12345678; source.state = true; auto encoded = encode_message(source); + auto *data = encoded.data(); + auto size = encoded.size(); + benchmark::DoNotOptimize(data); + benchmark::DoNotOptimize(size); for (auto _ : state) { - SwitchCommandRequest msg; for (int i = 0; i < kInnerIterations; i++) { - msg.decode(encoded.data(), encoded.size()); + SwitchCommandRequest msg; + escape(&msg); + msg.decode(data, size); + escape(&msg); } - benchmark::DoNotOptimize(msg.state); } state.SetItemsProcessed(state.iterations() * kInnerIterations); } @@ -78,13 +93,18 @@ static void Decode_LightCommandRequest(benchmark::State &state) { source.has_effect = true; source.effect = StringRef::from_lit("rainbow"); auto encoded = encode_message(source); + auto *data = encoded.data(); + auto size = encoded.size(); + benchmark::DoNotOptimize(data); + benchmark::DoNotOptimize(size); for (auto _ : state) { - LightCommandRequest msg; for (int i = 0; i < kInnerIterations; i++) { - msg.decode(encoded.data(), encoded.size()); + LightCommandRequest msg; + escape(&msg); + msg.decode(data, size); + escape(&msg); } - benchmark::DoNotOptimize(msg.brightness); } state.SetItemsProcessed(state.iterations() * kInnerIterations); } diff --git a/tests/benchmarks/components/api/bench_proto_encode.cpp b/tests/benchmarks/components/api/bench_proto_encode.cpp index 656c1e17db..1e2efcd281 100644 --- a/tests/benchmarks/components/api/bench_proto_encode.cpp +++ b/tests/benchmarks/components/api/bench_proto_encode.cpp @@ -295,4 +295,93 @@ static void CalcAndEncode_DeviceInfoResponse_Fresh(benchmark::State &state) { } BENCHMARK(CalcAndEncode_DeviceInfoResponse_Fresh); +// --- BluetoothLERawAdvertisementsResponse (12 adverts, highest-volume BLE message) --- + +#ifdef USE_BLUETOOTH_PROXY + +static BluetoothLERawAdvertisementsResponse make_ble_raw_advs_12() { + static const uint8_t fake_adv_data[] = { + 0x02, 0x01, 0x06, 0x03, 0x03, 0x9F, 0xFE, 0x17, 0x16, 0x9F, 0xFE, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + }; + BluetoothLERawAdvertisementsResponse msg; + msg.advertisements_len = 12; + for (int i = 0; i < 12; i++) { + auto &adv = msg.advertisements[i]; + adv.address = 0xAABBCCDD0000ULL + i; + adv.rssi = -60 - i; + adv.address_type = 1; + memcpy(adv.data, fake_adv_data, sizeof(fake_adv_data)); + adv.data_len = sizeof(fake_adv_data); + } + return msg; +} + +static void CalculateSize_BLERawAdvs12(benchmark::State &state) { + auto msg = make_ble_raw_advs_12(); + + for (auto _ : state) { + uint32_t result = 0; + for (int i = 0; i < kInnerIterations; i++) { + result += msg.calculate_size(); + } + benchmark::DoNotOptimize(result); + } + state.SetItemsProcessed(state.iterations() * kInnerIterations); +} +BENCHMARK(CalculateSize_BLERawAdvs12); + +static void Encode_BLERawAdvs12(benchmark::State &state) { + auto msg = make_ble_raw_advs_12(); + APIBuffer buffer; + uint32_t total_size = msg.calculate_size(); + buffer.resize(total_size); + + for (auto _ : state) { + for (int i = 0; i < kInnerIterations; i++) { + ProtoWriteBuffer writer(&buffer, 0); + msg.encode(writer); + } + benchmark::DoNotOptimize(buffer.data()); + } + state.SetItemsProcessed(state.iterations() * kInnerIterations); +} +BENCHMARK(Encode_BLERawAdvs12); + +static void CalcAndEncode_BLERawAdvs12(benchmark::State &state) { + auto msg = make_ble_raw_advs_12(); + APIBuffer buffer; + + for (auto _ : state) { + for (int i = 0; i < kInnerIterations; i++) { + uint32_t size = msg.calculate_size(); + buffer.resize(size); + ProtoWriteBuffer writer(&buffer, 0); + msg.encode(writer); + } + benchmark::DoNotOptimize(buffer.data()); + } + state.SetItemsProcessed(state.iterations() * kInnerIterations); +} +BENCHMARK(CalcAndEncode_BLERawAdvs12); + +static void CalcAndEncode_BLERawAdvs12_Fresh(benchmark::State &state) { + auto msg = make_ble_raw_advs_12(); + + for (auto _ : state) { + for (int i = 0; i < kInnerIterations; i++) { + APIBuffer buffer; + uint32_t size = msg.calculate_size(); + buffer.resize(size); + ProtoWriteBuffer writer(&buffer, 0); + msg.encode(writer); + benchmark::DoNotOptimize(buffer.data()); + } + } + state.SetItemsProcessed(state.iterations() * kInnerIterations); +} +BENCHMARK(CalcAndEncode_BLERawAdvs12_Fresh); + +#endif // USE_BLUETOOTH_PROXY + } // namespace esphome::api::benchmarks diff --git a/tests/benchmarks/components/api/bench_send_sensor_state.cpp b/tests/benchmarks/components/api/bench_send_sensor_state.cpp new file mode 100644 index 0000000000..815081374a --- /dev/null +++ b/tests/benchmarks/components/api/bench_send_sensor_state.cpp @@ -0,0 +1,191 @@ +#include "esphome/core/defines.h" +#if defined(USE_API_PLAINTEXT) && defined(USE_SENSOR) + +#include +#include + +#include "bench_helpers.h" +#include "esphome/components/api/api_connection.h" +#include "esphome/components/api/api_server.h" +#include "esphome/components/sensor/sensor.h" + +namespace esphome::api { + +// Friend functions declared in APIConnection for benchmark access. +void bench_enable_immediate_send(APIConnection *conn) { conn->flags_.should_try_send_immediately = true; } +void bench_clear_batch(APIConnection *conn) { conn->clear_batch_(); } +void bench_process_batch(APIConnection *conn) { conn->process_batch_(); } + +} // namespace esphome::api + +namespace esphome::api::benchmarks { + +static constexpr int kInnerIterations = 2000; + +// Helper to create a TCP loopback connection with an APIConnection. +// Returns the connection and the read-side fd for draining. +static std::pair, int> create_api_connection() { + auto [sock, read_fd] = create_tcp_loopback(); + auto conn = std::make_unique(std::move(sock), global_api_server); + conn->start(); + return {std::move(conn), read_fd}; +} + +// Test subclass to access protected configure_entity_() for benchmark setup. +class TestSensor : public sensor::Sensor { + public: + void configure(const char *name) { this->configure_entity_(name, 0x12345678, 0); } +}; + +// --- send_sensor_state: immediate send path --- +// Measures: send_message_smart_ → prepare buffer → dispatch_message_ → +// try_send_sensor_state → fill key/device_id + proto encode → frame write → +// TCP send. This is the per-client cost when batch_delay=0 and initial states +// have been sent. + +static void SendSensorState_Immediate(benchmark::State &state) { + auto [conn, read_fd] = create_api_connection(); + bench_enable_immediate_send(conn.get()); + // batch_delay must be 0 for should_send_immediately_ to return true + uint16_t saved_delay = global_api_server->get_batch_delay(); + global_api_server->set_batch_delay(0); + + TestSensor sensor; + sensor.configure("test_sensor"); + sensor.publish_state(23.5f); + + for (auto _ : state) { + for (int i = 0; i < kInnerIterations; i++) { + conn->send_sensor_state(&sensor); + } + drain_socket(read_fd); + benchmark::DoNotOptimize(conn.get()); + } + state.SetItemsProcessed(state.iterations() * kInnerIterations); + + global_api_server->set_batch_delay(saved_delay); + ::close(read_fd); +} +BENCHMARK(SendSensorState_Immediate); + +// --- send_sensor_state: batch path (cold — first call allocates) --- +// Measures: send_message_smart_ → schedule_message_ → deferred batch add. +// Includes one-time vector allocation cost. + +static void SendSensorState_Batch_Cold(benchmark::State &state) { + auto [conn, read_fd] = create_api_connection(); + + TestSensor sensor; + sensor.configure("test_sensor"); + sensor.publish_state(23.5f); + + for (auto _ : state) { + for (int i = 0; i < kInnerIterations; i++) { + conn->send_sensor_state(&sensor); + } + benchmark::DoNotOptimize(conn.get()); + } + state.SetItemsProcessed(state.iterations() * kInnerIterations); + + ::close(read_fd); +} +BENCHMARK(SendSensorState_Batch_Cold); + +// --- send_sensor_state: batch path (warm — buffer already allocated) --- +// Measures steady-state batch cost after the vector has been allocated +// and cleared at least once. This is the typical path during normal +// operation after the first batch has been processed. + +static void SendSensorState_Batch_Warm(benchmark::State &state) { + auto [conn, read_fd] = create_api_connection(); + + TestSensor sensor; + sensor.configure("test_sensor"); + sensor.publish_state(23.5f); + + // Warm up: send once to allocate, then clear to keep capacity + conn->send_sensor_state(&sensor); + bench_clear_batch(conn.get()); + + for (auto _ : state) { + for (int i = 0; i < kInnerIterations; i++) { + conn->send_sensor_state(&sensor); + } + benchmark::DoNotOptimize(conn.get()); + } + state.SetItemsProcessed(state.iterations() * kInnerIterations); + + ::close(read_fd); +} +BENCHMARK(SendSensorState_Batch_Warm); + +// --- process_batch_: single sensor state (encode + frame + write) --- +// Measures the deferred batch processing path: dispatch_message_ → +// try_send_sensor_state → fill + proto encode → send_buffer → frame write. +// This is the cost paid on the next loop() after batching. + +static void ProcessBatch_SingleSensor(benchmark::State &state) { + auto [conn, read_fd] = create_api_connection(); + + TestSensor sensor; + sensor.configure("test_sensor"); + sensor.publish_state(23.5f); + + // Warm up batch vector + conn->send_sensor_state(&sensor); + bench_process_batch(conn.get()); + drain_socket(read_fd); + + for (auto _ : state) { + for (int i = 0; i < kInnerIterations; i++) { + conn->send_sensor_state(&sensor); + bench_process_batch(conn.get()); + } + drain_socket(read_fd); + benchmark::DoNotOptimize(conn.get()); + } + state.SetItemsProcessed(state.iterations() * kInnerIterations); + + ::close(read_fd); +} +BENCHMARK(ProcessBatch_SingleSensor); + +// --- process_batch_: 5 different sensors --- +// Measures batch processing with multiple items queued. +// This exercises the multi-message path in process_batch_. + +static void ProcessBatch_5Sensors(benchmark::State &state) { + auto [conn, read_fd] = create_api_connection(); + + TestSensor sensors[5]; + for (int i = 0; i < 5; i++) { + char name[20]; + snprintf(name, sizeof(name), "sensor_%d", i); + sensors[i].configure(name); + sensors[i].publish_state(23.5f + static_cast(i)); + } + + // Warm up batch vector + for (auto &s : sensors) + conn->send_sensor_state(&s); + bench_process_batch(conn.get()); + drain_socket(read_fd); + + for (auto _ : state) { + for (int i = 0; i < kInnerIterations; i++) { + for (auto &s : sensors) + conn->send_sensor_state(&s); + bench_process_batch(conn.get()); + } + drain_socket(read_fd); + benchmark::DoNotOptimize(conn.get()); + } + state.SetItemsProcessed(state.iterations() * kInnerIterations); + + ::close(read_fd); +} +BENCHMARK(ProcessBatch_5Sensors); + +} // namespace esphome::api::benchmarks + +#endif // USE_API_PLAINTEXT && USE_SENSOR diff --git a/tests/benchmarks/components/button/__init__.py b/tests/benchmarks/components/button/__init__.py new file mode 100644 index 0000000000..b08f67a095 --- /dev/null +++ b/tests/benchmarks/components/button/__init__.py @@ -0,0 +1,5 @@ +from tests.testing_helpers import ComponentManifestOverride + + +def override_manifest(manifest: ComponentManifestOverride) -> None: + manifest.enable_codegen() diff --git a/tests/benchmarks/components/button/bench_button.cpp b/tests/benchmarks/components/button/bench_button.cpp new file mode 100644 index 0000000000..82f76961c9 --- /dev/null +++ b/tests/benchmarks/components/button/bench_button.cpp @@ -0,0 +1,55 @@ +#include + +#include "esphome/components/button/button.h" + +namespace esphome::button::benchmarks { + +static constexpr int kInnerIterations = 2000; + +// Minimal Button for benchmarking — press_action() is a no-op. +class BenchButton : public Button { + public: + void configure(const char *name) { this->configure_entity_(name, 0x12345678, 0); } + + protected: + void press_action() override {} +}; + +// --- Button::press() --- +// Measures: ESP_LOGD + press_action() + callback dispatch. + +static void ButtonPress(benchmark::State &state) { + BenchButton button; + button.configure("test_button"); + + for (auto _ : state) { + for (int i = 0; i < kInnerIterations; i++) { + button.press(); + } + benchmark::DoNotOptimize(&button); + } + state.SetItemsProcessed(state.iterations() * kInnerIterations); +} +BENCHMARK(ButtonPress); + +// --- Button::press() with callback --- +// Measures callback dispatch overhead. + +static void ButtonPress_WithCallback(benchmark::State &state) { + BenchButton button; + button.configure("test_button"); + + uint64_t callback_count = 0; + button.add_on_press_callback([&callback_count]() { callback_count++; }); + + for (auto _ : state) { + for (int i = 0; i < kInnerIterations; i++) { + button.press(); + } + benchmark::DoNotOptimize(callback_count); + } + state.SetItemsProcessed(state.iterations() * kInnerIterations); +} +BENCHMARK(ButtonPress_WithCallback); + +} // namespace esphome::button::benchmarks diff --git a/tests/benchmarks/components/button/benchmark.yaml b/tests/benchmarks/components/button/benchmark.yaml new file mode 100644 index 0000000000..75f089f793 --- /dev/null +++ b/tests/benchmarks/components/button/benchmark.yaml @@ -0,0 +1 @@ +button: diff --git a/tests/benchmarks/components/number/__init__.py b/tests/benchmarks/components/number/__init__.py new file mode 100644 index 0000000000..b08f67a095 --- /dev/null +++ b/tests/benchmarks/components/number/__init__.py @@ -0,0 +1,5 @@ +from tests.testing_helpers import ComponentManifestOverride + + +def override_manifest(manifest: ComponentManifestOverride) -> None: + manifest.enable_codegen() diff --git a/tests/benchmarks/components/number/bench_number.cpp b/tests/benchmarks/components/number/bench_number.cpp new file mode 100644 index 0000000000..57a73930b2 --- /dev/null +++ b/tests/benchmarks/components/number/bench_number.cpp @@ -0,0 +1,121 @@ +#include + +#include "esphome/components/number/number.h" + +namespace esphome::benchmarks { + +// Inner iteration count to amortize CodSpeed instrumentation overhead. +static constexpr int kInnerIterations = 2000; + +// Minimal Number for benchmarking — control() publishes the value back. +class BenchNumber : public number::Number { + public: + void configure(const char *name) { this->configure_entity_(name, 0x12345678, 0); } + + protected: + void control(float value) override { this->publish_state(value); } +}; + +// Helper to create a typical number entity for benchmarks. +static void setup_number(BenchNumber &number) { + number.configure("test_number"); + number.traits.set_min_value(0.0f); + number.traits.set_max_value(100.0f); + number.traits.set_step(1.0f); + number.traits.set_mode(number::NUMBER_MODE_SLIDER); +} + +// --- Number::publish_state() --- +// Measures the publish path: set_has_state, store value, callback dispatch. + +static void NumberPublish_State(benchmark::State &state) { + BenchNumber number; + setup_number(number); + + for (auto _ : state) { + for (int i = 0; i < kInnerIterations; i++) { + number.publish_state(static_cast(i % 100)); + } + benchmark::DoNotOptimize(number.state); + } + state.SetItemsProcessed(state.iterations() * kInnerIterations); +} +BENCHMARK(NumberPublish_State); + +// --- Number::publish_state() with callback --- +// Measures callback dispatch overhead. + +static void NumberPublish_WithCallback(benchmark::State &state) { + BenchNumber number; + setup_number(number); + + uint64_t callback_count = 0; + number.add_on_state_callback([&callback_count](float) { callback_count++; }); + + for (auto _ : state) { + for (int i = 0; i < kInnerIterations; i++) { + number.publish_state(static_cast(i % 100)); + } + benchmark::DoNotOptimize(callback_count); + } + state.SetItemsProcessed(state.iterations() * kInnerIterations); +} +BENCHMARK(NumberPublish_WithCallback); + +// --- NumberCall::perform() set value --- +// The most common number call — setting an absolute value. +// Exercises: validation against min/max, control() dispatch. + +static void NumberCall_SetValue(benchmark::State &state) { + BenchNumber number; + setup_number(number); + number.publish_state(50.0f); + + for (auto _ : state) { + for (int i = 0; i < kInnerIterations; i++) { + float val = static_cast(i % 100); + number.make_call().set_value(val).perform(); + } + benchmark::DoNotOptimize(number.state); + } + state.SetItemsProcessed(state.iterations() * kInnerIterations); +} +BENCHMARK(NumberCall_SetValue); + +// --- NumberCall::perform() increment --- +// Exercises: state read, step arithmetic, max clamping. + +static void NumberCall_Increment(benchmark::State &state) { + BenchNumber number; + setup_number(number); + number.publish_state(0.0f); + + for (auto _ : state) { + for (int i = 0; i < kInnerIterations; i++) { + number.make_call().number_increment(true).perform(); + } + benchmark::DoNotOptimize(number.state); + } + state.SetItemsProcessed(state.iterations() * kInnerIterations); +} +BENCHMARK(NumberCall_Increment); + +// --- NumberCall::perform() decrement --- +// Exercises: state read, step arithmetic, min clamping. + +static void NumberCall_Decrement(benchmark::State &state) { + BenchNumber number; + setup_number(number); + number.publish_state(100.0f); + + for (auto _ : state) { + for (int i = 0; i < kInnerIterations; i++) { + number.make_call().number_decrement(true).perform(); + } + benchmark::DoNotOptimize(number.state); + } + state.SetItemsProcessed(state.iterations() * kInnerIterations); +} +BENCHMARK(NumberCall_Decrement); + +} // namespace esphome::benchmarks diff --git a/tests/benchmarks/components/number/benchmark.yaml b/tests/benchmarks/components/number/benchmark.yaml new file mode 100644 index 0000000000..f435661270 --- /dev/null +++ b/tests/benchmarks/components/number/benchmark.yaml @@ -0,0 +1 @@ +number: diff --git a/tests/benchmarks/components/select/__init__.py b/tests/benchmarks/components/select/__init__.py new file mode 100644 index 0000000000..b08f67a095 --- /dev/null +++ b/tests/benchmarks/components/select/__init__.py @@ -0,0 +1,5 @@ +from tests.testing_helpers import ComponentManifestOverride + + +def override_manifest(manifest: ComponentManifestOverride) -> None: + manifest.enable_codegen() diff --git a/tests/benchmarks/components/select/bench_select.cpp b/tests/benchmarks/components/select/bench_select.cpp new file mode 100644 index 0000000000..8e047d9151 --- /dev/null +++ b/tests/benchmarks/components/select/bench_select.cpp @@ -0,0 +1,157 @@ +#include + +#include "esphome/components/select/select.h" + +namespace esphome::benchmarks { + +// Inner iteration count to amortize CodSpeed instrumentation overhead. +static constexpr int kInnerIterations = 2000; + +// Minimal Select for benchmarking — control() publishes directly by index. +class BenchSelect : public select::Select { + public: + void configure(const char *name) { this->configure_entity_(name, 0x12345678, 0); } + + protected: + void control(size_t index) override { this->publish_state(index); } +}; + +// Helper to create a select with the given options. +static void setup_select(BenchSelect &select, const char *name, std::initializer_list options) { + select.configure(name); + select.traits.set_options(options); + select.publish_state(size_t(0)); +} + +// --- Select::publish_state(size_t) --- +// The fast path: publish by index, no string lookup. + +static void SelectPublish_ByIndex(benchmark::State &state) { + BenchSelect select; + setup_select(select, "test_select", {"off", "still", "move", "still+move"}); + + for (auto _ : state) { + for (int i = 0; i < kInnerIterations; i++) { + select.publish_state(static_cast(i % 4)); + } + benchmark::DoNotOptimize(select.active_index()); + } + state.SetItemsProcessed(state.iterations() * kInnerIterations); +} +BENCHMARK(SelectPublish_ByIndex); + +// --- Select::publish_state(const char *) --- +// The string path: requires index_of() lookup via strncmp. + +static void SelectPublish_ByString(benchmark::State &state) { + BenchSelect select; + setup_select(select, "test_select", {"off", "still", "move", "still+move"}); + + const char *options[] = {"off", "still", "move", "still+move"}; + + for (auto _ : state) { + for (int i = 0; i < kInnerIterations; i++) { + select.publish_state(options[i % 4]); + } + benchmark::DoNotOptimize(select.active_index()); + } + state.SetItemsProcessed(state.iterations() * kInnerIterations); +} +BENCHMARK(SelectPublish_ByString); + +// --- Select::publish_state() with callback --- +// Measures callback dispatch overhead on the index path. + +static void SelectPublish_WithCallback(benchmark::State &state) { + BenchSelect select; + setup_select(select, "test_select", {"off", "still", "move", "still+move"}); + + uint64_t callback_count = 0; + select.add_on_state_callback([&callback_count](size_t) { callback_count++; }); + + for (auto _ : state) { + for (int i = 0; i < kInnerIterations; i++) { + select.publish_state(static_cast(i % 4)); + } + benchmark::DoNotOptimize(callback_count); + } + state.SetItemsProcessed(state.iterations() * kInnerIterations); +} +BENCHMARK(SelectPublish_WithCallback); + +// --- SelectCall::perform() set by index --- +// The fast call path — no string matching needed. + +static void SelectCall_SetByIndex(benchmark::State &state) { + BenchSelect select; + setup_select(select, "test_select", {"off", "still", "move", "still+move"}); + + for (auto _ : state) { + for (int i = 0; i < kInnerIterations; i++) { + select.make_call().set_index(i % 4).perform(); + } + benchmark::DoNotOptimize(select.active_index()); + } + state.SetItemsProcessed(state.iterations() * kInnerIterations); +} +BENCHMARK(SelectCall_SetByIndex); + +// --- SelectCall::perform() set by option string --- +// Exercises the string lookup path through index_of(). + +static void SelectCall_SetByOption(benchmark::State &state) { + BenchSelect select; + setup_select(select, "test_select", {"off", "still", "move", "still+move"}); + + const char *options[] = {"off", "still", "move", "still+move"}; + + for (auto _ : state) { + for (int i = 0; i < kInnerIterations; i++) { + select.make_call().set_option(options[i % 4]).perform(); + } + benchmark::DoNotOptimize(select.active_index()); + } + state.SetItemsProcessed(state.iterations() * kInnerIterations); +} +BENCHMARK(SelectCall_SetByOption); + +// --- SelectCall::perform() next with cycling --- +// Exercises the navigation path through active_index_. + +static void SelectCall_NextCycle(benchmark::State &state) { + BenchSelect select; + setup_select(select, "test_select", {"off", "still", "move", "still+move"}); + + for (auto _ : state) { + for (int i = 0; i < kInnerIterations; i++) { + select.make_call().select_next(true).perform(); + } + benchmark::DoNotOptimize(select.active_index()); + } + state.SetItemsProcessed(state.iterations() * kInnerIterations); +} +BENCHMARK(SelectCall_NextCycle); + +// --- SelectCall with 10 options (string lookup) --- +// Worst-case string matching with more options. + +static void SelectCall_SetByOption_10Options(benchmark::State &state) { + BenchSelect select; + setup_select( + select, "test_select", + {"off", "still", "move", "still+move", "custom1", "custom2", "custom3", "custom4", "custom5", "custom6"}); + + // Pick options spread across the list to exercise different search depths + const char *picks[] = {"off", "custom3", "custom6", "move"}; + + for (auto _ : state) { + for (int i = 0; i < kInnerIterations; i++) { + select.make_call().set_option(picks[i % 4]).perform(); + } + benchmark::DoNotOptimize(select.active_index()); + } + state.SetItemsProcessed(state.iterations() * kInnerIterations); +} +BENCHMARK(SelectCall_SetByOption_10Options); + +} // namespace esphome::benchmarks diff --git a/tests/benchmarks/components/select/benchmark.yaml b/tests/benchmarks/components/select/benchmark.yaml new file mode 100644 index 0000000000..d336a348a0 --- /dev/null +++ b/tests/benchmarks/components/select/benchmark.yaml @@ -0,0 +1 @@ +select: diff --git a/tests/benchmarks/components/switch/__init__.py b/tests/benchmarks/components/switch/__init__.py new file mode 100644 index 0000000000..b08f67a095 --- /dev/null +++ b/tests/benchmarks/components/switch/__init__.py @@ -0,0 +1,5 @@ +from tests.testing_helpers import ComponentManifestOverride + + +def override_manifest(manifest: ComponentManifestOverride) -> None: + manifest.enable_codegen() diff --git a/tests/benchmarks/components/switch/bench_switch.cpp b/tests/benchmarks/components/switch/bench_switch.cpp new file mode 100644 index 0000000000..d948f080ad --- /dev/null +++ b/tests/benchmarks/components/switch/bench_switch.cpp @@ -0,0 +1,137 @@ +#include + +#include "esphome/components/switch/switch.h" + +namespace esphome::benchmarks { + +// Inner iteration count to amortize CodSpeed instrumentation overhead. +static constexpr int kInnerIterations = 2000; + +// Minimal Switch for benchmarking — write_state() publishes directly. +class BenchSwitch : public switch_::Switch { + public: + void configure(const char *name) { this->configure_entity_(name, 0x12345678, 0); } + + protected: + void write_state(bool state) override { this->publish_state(state); } +}; + +// --- Switch::publish_state() alternating --- +// Forces state change every call, exercising the full publish path. + +static void SwitchPublish_Alternating(benchmark::State &state) { + BenchSwitch sw; + sw.configure("test_switch"); + sw.set_restore_mode(switch_::SWITCH_ALWAYS_OFF); + sw.publish_state(false); + + for (auto _ : state) { + for (int i = 0; i < kInnerIterations; i++) { + sw.publish_state(i % 2 == 0); + } + benchmark::DoNotOptimize(sw.state); + } + state.SetItemsProcessed(state.iterations() * kInnerIterations); +} +BENCHMARK(SwitchPublish_Alternating); + +// --- Switch::publish_state() no change --- +// Tests the deduplication fast path in publish_dedup_. + +static void SwitchPublish_NoChange(benchmark::State &state) { + BenchSwitch sw; + sw.configure("test_switch"); + sw.set_restore_mode(switch_::SWITCH_ALWAYS_OFF); + sw.publish_state(true); + + for (auto _ : state) { + for (int i = 0; i < kInnerIterations; i++) { + sw.publish_state(true); + } + benchmark::DoNotOptimize(sw.state); + } + state.SetItemsProcessed(state.iterations() * kInnerIterations); +} +BENCHMARK(SwitchPublish_NoChange); + +// --- Switch::publish_state() with callback --- +// Measures callback dispatch overhead on state changes. + +static void SwitchPublish_WithCallback(benchmark::State &state) { + BenchSwitch sw; + sw.configure("test_switch"); + sw.set_restore_mode(switch_::SWITCH_ALWAYS_OFF); + + uint64_t callback_count = 0; + sw.add_on_state_callback([&callback_count](bool) { callback_count++; }); + sw.publish_state(false); + + for (auto _ : state) { + for (int i = 0; i < kInnerIterations; i++) { + sw.publish_state(i % 2 == 0); + } + benchmark::DoNotOptimize(callback_count); + } + state.SetItemsProcessed(state.iterations() * kInnerIterations); +} +BENCHMARK(SwitchPublish_WithCallback); + +// --- Switch::turn_on() / turn_off() --- +// The front-end call path: turn_on → write_state → publish_state. + +static void SwitchTurnOn(benchmark::State &state) { + BenchSwitch sw; + sw.configure("test_switch"); + sw.set_restore_mode(switch_::SWITCH_ALWAYS_OFF); + sw.publish_state(false); + + for (auto _ : state) { + for (int i = 0; i < kInnerIterations; i++) { + sw.turn_on(); + } + benchmark::DoNotOptimize(sw.state); + } + state.SetItemsProcessed(state.iterations() * kInnerIterations); +} +BENCHMARK(SwitchTurnOn); + +// --- Switch::toggle() alternating --- +// Exercises the toggle path which reads current state to determine target. + +static void SwitchToggle(benchmark::State &state) { + BenchSwitch sw; + sw.configure("test_switch"); + sw.set_restore_mode(switch_::SWITCH_ALWAYS_OFF); + sw.publish_state(false); + + for (auto _ : state) { + for (int i = 0; i < kInnerIterations; i++) { + sw.toggle(); + } + benchmark::DoNotOptimize(sw.state); + } + state.SetItemsProcessed(state.iterations() * kInnerIterations); +} +BENCHMARK(SwitchToggle); + +// --- Switch::publish_state() inverted --- +// Verifies the inversion path doesn't add significant overhead. + +static void SwitchPublish_Inverted(benchmark::State &state) { + BenchSwitch sw; + sw.configure("test_switch"); + sw.set_restore_mode(switch_::SWITCH_ALWAYS_OFF); + sw.set_inverted(true); + sw.publish_state(false); + + for (auto _ : state) { + for (int i = 0; i < kInnerIterations; i++) { + sw.publish_state(i % 2 == 0); + } + benchmark::DoNotOptimize(sw.state); + } + state.SetItemsProcessed(state.iterations() * kInnerIterations); +} +BENCHMARK(SwitchPublish_Inverted); + +} // namespace esphome::benchmarks diff --git a/tests/benchmarks/components/switch/benchmark.yaml b/tests/benchmarks/components/switch/benchmark.yaml new file mode 100644 index 0000000000..c637b3dc89 --- /dev/null +++ b/tests/benchmarks/components/switch/benchmark.yaml @@ -0,0 +1 @@ +switch: diff --git a/tests/benchmarks/components/text_sensor/__init__.py b/tests/benchmarks/components/text_sensor/__init__.py new file mode 100644 index 0000000000..b08f67a095 --- /dev/null +++ b/tests/benchmarks/components/text_sensor/__init__.py @@ -0,0 +1,5 @@ +from tests.testing_helpers import ComponentManifestOverride + + +def override_manifest(manifest: ComponentManifestOverride) -> None: + manifest.enable_codegen() diff --git a/tests/benchmarks/components/text_sensor/bench_text_sensor.cpp b/tests/benchmarks/components/text_sensor/bench_text_sensor.cpp new file mode 100644 index 0000000000..0ac88f79c1 --- /dev/null +++ b/tests/benchmarks/components/text_sensor/bench_text_sensor.cpp @@ -0,0 +1,108 @@ +#include + +#include "esphome/components/text_sensor/text_sensor.h" + +namespace esphome::text_sensor::benchmarks { + +static constexpr int kInnerIterations = 2000; + +// --- publish_state(const char *) with short string, value changes each time --- +// Exercises: memcmp check (mismatch), string assign, callback dispatch. + +static void TextSensorPublish_Short_Changing(benchmark::State &state) { + TextSensor sensor; + + // Pre-populate with different short strings + const char *values[] = {"192.168.1.1", "192.168.1.2", "192.168.1.3", "192.168.1.4"}; + + for (auto _ : state) { + for (int i = 0; i < kInnerIterations; i++) { + sensor.publish_state(values[i % 4]); + } + benchmark::DoNotOptimize(sensor.state); + } + state.SetItemsProcessed(state.iterations() * kInnerIterations); +} +BENCHMARK(TextSensorPublish_Short_Changing); + +// --- publish_state(const char *) with short string, same value (dedup path) --- +// Exercises: memcmp check (match), skips string assign. + +static void TextSensorPublish_Short_NoChange(benchmark::State &state) { + TextSensor sensor; + sensor.publish_state("192.168.1.100"); + + for (auto _ : state) { + for (int i = 0; i < kInnerIterations; i++) { + sensor.publish_state("192.168.1.100"); + } + benchmark::DoNotOptimize(sensor.state); + } + state.SetItemsProcessed(state.iterations() * kInnerIterations); +} +BENCHMARK(TextSensorPublish_Short_NoChange); + +// --- publish_state with longer string (firmware version, MAC address) --- +// Exercises: memcmp on longer strings, string assign with potential realloc. + +static void TextSensorPublish_Long_Changing(benchmark::State &state) { + TextSensor sensor; + + const char *values[] = { + "2025.12.0-dev (Jan 15 2025, 10:30:00)", + "2025.12.1-dev (Feb 20 2025, 14:45:00)", + "2025.12.2-dev (Mar 10 2025, 08:15:00)", + "2025.12.3-dev (Apr 5 2025, 16:00:00)", + }; + + for (auto _ : state) { + for (int i = 0; i < kInnerIterations; i++) { + sensor.publish_state(values[i % 4]); + } + benchmark::DoNotOptimize(sensor.state); + } + state.SetItemsProcessed(state.iterations() * kInnerIterations); +} +BENCHMARK(TextSensorPublish_Long_Changing); + +// --- publish_state with callback --- +// Measures callback dispatch overhead for text sensors. + +static void TextSensorPublish_WithCallback(benchmark::State &state) { + TextSensor sensor; + + uint64_t callback_count = 0; + sensor.add_on_state_callback([&callback_count](const std::string &) { callback_count++; }); + + const char *values[] = {"192.168.1.1", "192.168.1.2", "192.168.1.3", "192.168.1.4"}; + + for (auto _ : state) { + for (int i = 0; i < kInnerIterations; i++) { + sensor.publish_state(values[i % 4]); + } + benchmark::DoNotOptimize(callback_count); + } + state.SetItemsProcessed(state.iterations() * kInnerIterations); +} +BENCHMARK(TextSensorPublish_WithCallback); + +// --- publish_state(const char *, size_t) direct --- +// The lowest-level overload, avoids strlen. + +static void TextSensorPublish_WithLen(benchmark::State &state) { + TextSensor sensor; + + static constexpr const char *values[] = {"192.168.1.1", "192.168.1.2", "192.168.1.3", "192.168.1.4"}; + static constexpr size_t lens[] = {11, 11, 11, 11}; + + for (auto _ : state) { + for (int i = 0; i < kInnerIterations; i++) { + sensor.publish_state(values[i % 4], lens[i % 4]); + } + benchmark::DoNotOptimize(sensor.state); + } + state.SetItemsProcessed(state.iterations() * kInnerIterations); +} +BENCHMARK(TextSensorPublish_WithLen); + +} // namespace esphome::text_sensor::benchmarks diff --git a/tests/benchmarks/components/text_sensor/benchmark.yaml b/tests/benchmarks/components/text_sensor/benchmark.yaml new file mode 100644 index 0000000000..7bcb7de1c8 --- /dev/null +++ b/tests/benchmarks/components/text_sensor/benchmark.yaml @@ -0,0 +1 @@ +text_sensor: diff --git a/tests/benchmarks/core/bench_scheduler.cpp b/tests/benchmarks/core/bench_scheduler.cpp index 9357734cc8..214fe0e4b8 100644 --- a/tests/benchmarks/core/bench_scheduler.cpp +++ b/tests/benchmarks/core/bench_scheduler.cpp @@ -8,7 +8,24 @@ namespace esphome::benchmarks { // Inner iteration count to amortize CodSpeed instrumentation overhead. // Without this, the ~60ns per-iteration valgrind start/stop cost dominates // sub-microsecond benchmarks. -static constexpr int kInnerIterations = 2000; +// Must be divisible by all batch sizes used below (3, 10) to avoid +// pool imbalance at iteration boundaries that causes spurious malloc. +static constexpr int kInnerIterations = 2100; + +// Warm the scheduler pool by registering and replacing items twice. +// The first batch allocates fresh items; the second batch cancels them and +// populates the recycling pool with the cancelled items from the first batch. +static void warm_pool(Scheduler &scheduler, Component *component, int batch_size, uint32_t delay) { + uint32_t now = millis(); + for (int i = 0; i < batch_size; i++) { + scheduler.set_timeout(component, static_cast(i), delay, []() {}); + } + scheduler.call(++now); + for (int i = 0; i < batch_size; i++) { + scheduler.set_timeout(component, static_cast(i), delay, []() {}); + } + scheduler.call(++now); +} // --- Scheduler fast path: no work to do --- @@ -83,11 +100,21 @@ static void Scheduler_SetTimeout(benchmark::State &state) { Scheduler scheduler; Component dummy_component; + // Register 3 timeouts then call() — realistic worst case where multiple + // components schedule in the same loop iteration. Keeps item count within + // the recycling pool (MAX_POOL_SIZE=5) to avoid spurious malloc/free. + static constexpr int kBatchSize = 3; + static_assert(kInnerIterations % kBatchSize == 0, "kInnerIterations must be divisible by kBatchSize"); + warm_pool(scheduler, &dummy_component, kBatchSize, 1000); for (auto _ : state) { + uint32_t now = millis(); for (int i = 0; i < kInnerIterations; i++) { - scheduler.set_timeout(&dummy_component, static_cast(i % 5), 1000, []() {}); + scheduler.set_timeout(&dummy_component, static_cast(i % kBatchSize), 1000, []() {}); + if ((i + 1) % kBatchSize == 0) { + scheduler.call(++now); + } } - scheduler.process_to_add(); + scheduler.call(++now); benchmark::DoNotOptimize(scheduler); } state.SetItemsProcessed(state.iterations() * kInnerIterations); @@ -99,22 +126,22 @@ BENCHMARK(Scheduler_SetTimeout); static void Scheduler_SetInterval(benchmark::State &state) { Scheduler scheduler; Component dummy_component; - // Number of distinct interval keys; controls how many unique timers exist - // simultaneously and the drain cadence for process_to_add(). - static constexpr int kKeyCount = 5; + // Register 3 intervals then call() — realistic worst case where multiple + // components schedule in the same loop iteration. Keeps item count within + // the recycling pool (MAX_POOL_SIZE=5) to avoid spurious malloc/free. + static constexpr int kBatchSize = 3; + static_assert(kInnerIterations % kBatchSize == 0, "kInnerIterations must be divisible by kBatchSize"); + warm_pool(scheduler, &dummy_component, kBatchSize, 1000); for (auto _ : state) { + uint32_t now = millis(); for (int i = 0; i < kInnerIterations; i++) { - scheduler.set_interval(&dummy_component, static_cast(i % kKeyCount), 1000, []() {}); - // Drain to_add_ periodically to reflect production behavior where - // process_to_add() runs each main loop iteration. Without this, - // cancelled items accumulate in to_add_ causing O(n²) scan cost. - if ((i + 1) % kKeyCount == 0) { - scheduler.process_to_add(); + scheduler.set_interval(&dummy_component, static_cast(i % kBatchSize), 1000, []() {}); + if ((i + 1) % kBatchSize == 0) { + scheduler.call(++now); } } - // Final drain in case kInnerIterations is not a multiple of 5 - scheduler.process_to_add(); + scheduler.call(++now); benchmark::DoNotOptimize(scheduler); } state.SetItemsProcessed(state.iterations() * kInnerIterations); @@ -128,16 +155,79 @@ static void Scheduler_Defer(benchmark::State &state) { Component dummy_component; // defer() is Component::defer which calls set_timeout(delay=0). - // Call set_timeout directly since defer() is protected. + // Component::defer(func) passes nullptr as the name, which skips + // cancel_item_locked_ entirely — matching production behavior where + // defers are anonymous fire-and-forget callbacks. + static constexpr int kBatchSize = 3; + static_assert(kInnerIterations % kBatchSize == 0, "kInnerIterations must be divisible by kBatchSize"); + warm_pool(scheduler, &dummy_component, kBatchSize, 0); for (auto _ : state) { + uint32_t now = millis(); for (int i = 0; i < kInnerIterations; i++) { - scheduler.set_timeout(&dummy_component, static_cast(i % 5), 0, []() {}); + scheduler.set_timeout(&dummy_component, static_cast(nullptr), 0, []() {}); + if ((i + 1) % kBatchSize == 0) { + scheduler.call(++now); + } } - scheduler.process_to_add(); + scheduler.call(++now); benchmark::DoNotOptimize(scheduler); } state.SetItemsProcessed(state.iterations() * kInnerIterations); } BENCHMARK(Scheduler_Defer); +// --- Scheduler: defer with same ID (cancel-and-replace pattern) --- + +static void Scheduler_Defer_SameID(benchmark::State &state) { + Scheduler scheduler; + Component dummy_component; + + // Measures defer with a fixed numeric ID — each call cancels the previous + // pending defer before adding the new one. This is the pattern used by + // components that defer work but want to coalesce rapid updates. + static constexpr int kBatchSize = 3; + static_assert(kInnerIterations % kBatchSize == 0, "kInnerIterations must be divisible by kBatchSize"); + warm_pool(scheduler, &dummy_component, kBatchSize, 0); + for (auto _ : state) { + uint32_t now = millis(); + for (int i = 0; i < kInnerIterations; i++) { + scheduler.set_timeout(&dummy_component, static_cast(0), 0, []() {}); + if ((i + 1) % kBatchSize == 0) { + scheduler.call(++now); + } + } + scheduler.call(++now); + benchmark::DoNotOptimize(scheduler); + } + state.SetItemsProcessed(state.iterations() * kInnerIterations); +} +BENCHMARK(Scheduler_Defer_SameID); + +// --- Scheduler: set_timeout with batch size exceeding pool (cliff test) --- + +static void Scheduler_SetTimeout_ExceedPool(benchmark::State &state) { + Scheduler scheduler; + Component dummy_component; + + // Register 10 timeouts then call() — exceeds MAX_POOL_SIZE=5 to measure + // the performance cliff when the recycling pool is exhausted and items + // must be malloc'd/freed. + static constexpr int kBatchSize = 10; + static_assert(kInnerIterations % kBatchSize == 0, "kInnerIterations must be divisible by kBatchSize"); + warm_pool(scheduler, &dummy_component, kBatchSize, 1000); + for (auto _ : state) { + uint32_t now = millis(); + for (int i = 0; i < kInnerIterations; i++) { + scheduler.set_timeout(&dummy_component, static_cast(i % kBatchSize), 1000, []() {}); + if ((i + 1) % kBatchSize == 0) { + scheduler.call(++now); + } + } + scheduler.call(++now); + benchmark::DoNotOptimize(scheduler); + } + state.SetItemsProcessed(state.iterations() * kInnerIterations); +} +BENCHMARK(Scheduler_SetTimeout_ExceedPool); + } // namespace esphome::benchmarks diff --git a/tests/benchmarks/stubs/esphome/components/bluetooth_proxy/bluetooth_proxy.h b/tests/benchmarks/stubs/esphome/components/bluetooth_proxy/bluetooth_proxy.h new file mode 100644 index 0000000000..0934e0d4ed --- /dev/null +++ b/tests/benchmarks/stubs/esphome/components/bluetooth_proxy/bluetooth_proxy.h @@ -0,0 +1,38 @@ +// Stub for benchmark builds — provides the minimal interface that +// api_connection.cpp needs when USE_BLUETOOTH_PROXY is defined, +// without pulling in ESP32 BLE dependencies. +#pragma once + +#include "esphome/components/api/api_pb2.h" + +namespace esphome { +namespace api { +class APIConnection; +} // namespace api + +namespace bluetooth_proxy { + +class BluetoothProxy { + public: + api::APIConnection *get_api_connection() const { return nullptr; } + void subscribe_api_connection(api::APIConnection *conn, uint32_t flags) {} + void unsubscribe_api_connection(api::APIConnection *conn) {} + void bluetooth_device_request(const api::BluetoothDeviceRequest &msg) {} + void bluetooth_gatt_read(const api::BluetoothGATTReadRequest &msg) {} + void bluetooth_gatt_write(const api::BluetoothGATTWriteRequest &msg) {} + void bluetooth_gatt_read_descriptor(const api::BluetoothGATTReadDescriptorRequest &msg) {} + 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 send_connections_free(api::APIConnection *conn) {} + void bluetooth_scanner_set_mode(bool active) {} + void bluetooth_set_connection_params(const api::BluetoothSetConnectionParamsRequest &msg) {} + uint32_t get_feature_flags() const { return 0; } + void get_bluetooth_mac_address_pretty(char *buf) const { buf[0] = '\0'; } +}; + +// NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables) +extern BluetoothProxy *global_bluetooth_proxy; + +} // namespace bluetooth_proxy +} // namespace esphome diff --git a/tests/component_tests/display/__init__.py b/tests/component_tests/display/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/component_tests/display/test_display_metadata.py b/tests/component_tests/display/test_display_metadata.py new file mode 100644 index 0000000000..e569754494 --- /dev/null +++ b/tests/component_tests/display/test_display_metadata.py @@ -0,0 +1,81 @@ +"""Tests for display component metadata functions.""" + +from unittest.mock import patch + +from esphome.components.display import ( + DisplayMetaData, + add_metadata, + get_all_display_metadata, + get_display_metadata, +) +from esphome.cpp_generator import MockObj + + +def test_add_metadata_with_string_id(): + """Test adding metadata with a plain string ID.""" + with patch("esphome.components.display.CORE.data", {}): + add_metadata("my_display", 320, 240, True) + meta = get_display_metadata("my_display") + assert meta == DisplayMetaData( + width=320, height=240, has_writer=True, has_hardware_rotation=False + ) + + +def test_add_metadata_with_mockobj_id(): + """Test adding metadata with a MockObj ID (converted via str()).""" + with patch("esphome.components.display.CORE.data", {}): + mock_id = MockObj("my_display_obj") + add_metadata(mock_id, 480, 320, False, has_hardware_rotation=True) + meta = get_display_metadata("my_display_obj") + assert meta == DisplayMetaData( + width=480, height=320, has_writer=False, has_hardware_rotation=True + ) + + +def test_add_metadata_hardware_rotation_default(): + """Test that has_hardware_rotation defaults to False.""" + with patch("esphome.components.display.CORE.data", {}): + add_metadata("disp", 128, 64, False) + meta = get_display_metadata("disp") + assert meta.has_hardware_rotation is False + + +def test_get_display_metadata_missing_returns_none(): + """Test that querying a non-existent ID returns None.""" + with patch("esphome.components.display.CORE.data", {}): + data = get_display_metadata("no_such_display") + assert data.width == 0 + assert data.height == 0 + assert data.has_writer is False + assert data.has_hardware_rotation is False + + +def test_add_multiple_displays(): + """Test adding metadata for multiple displays.""" + with patch("esphome.components.display.CORE.data", {}): + add_metadata("disp_a", 320, 240, True) + add_metadata("disp_b", 128, 64, False, has_hardware_rotation=True) + + all_meta = get_all_display_metadata() + assert len(all_meta) == 2 + assert all_meta["disp_a"] == DisplayMetaData(320, 240, True, False) + assert all_meta["disp_b"] == DisplayMetaData(128, 64, False, True) + + +def test_add_metadata_overwrites_existing(): + """Test that adding metadata for the same ID overwrites the previous entry.""" + with patch("esphome.components.display.CORE.data", {}): + add_metadata("disp", 320, 240, True) + add_metadata("disp", 640, 480, False, has_hardware_rotation=True) + meta = get_display_metadata("disp") + assert meta == DisplayMetaData(640, 480, False, True) + + +def test_metadata_is_frozen(): + """Test that DisplayMetaData instances are immutable (frozen dataclass).""" + meta = DisplayMetaData(320, 240, True, False) + try: + meta.width = 640 + assert False, "Expected FrozenInstanceError" + except AttributeError: + pass diff --git a/tests/component_tests/mipi_dsi/test_mipi_dsi_config.py b/tests/component_tests/mipi_dsi/test_mipi_dsi_config.py index 1ae8cc644e..119bbf7fea 100644 --- a/tests/component_tests/mipi_dsi/test_mipi_dsi_config.py +++ b/tests/component_tests/mipi_dsi/test_mipi_dsi_config.py @@ -133,6 +133,6 @@ def test_code_generation( assert "set_init_sequence({224, 1, 0, 225, 1, 147, 226, 1," in main_cpp assert "p4_nano->set_lane_bit_rate(1500.0f);" in main_cpp assert "p4_nano->set_rotation(display::DISPLAY_ROTATION_90_DEGREES);" in main_cpp - assert "p4_86->set_rotation(display::DISPLAY_ROTATION_0_DEGREES);" in main_cpp + assert "p4_86->set_rotation(display::DISPLAY_ROTATION_0_DEGREES);" not in main_cpp assert "custom_id->set_rotation(display::DISPLAY_ROTATION_180_DEGREES);" in main_cpp # assert "backlight_id = new light::LightState(mipi_dsi_dsibacklight_id);" in main_cpp diff --git a/tests/component_tests/mipi_spi/conftest.py b/tests/component_tests/mipi_spi/conftest.py index eddf0987d0..082a9e55f2 100644 --- a/tests/component_tests/mipi_spi/conftest.py +++ b/tests/component_tests/mipi_spi/conftest.py @@ -1,6 +1,7 @@ """Tests for mpip_spi configuration validation.""" from collections.abc import Callable, Generator +from unittest import mock import pytest @@ -12,6 +13,16 @@ from esphome.core import CORE from esphome.pins import gpio_pin_schema +@pytest.fixture(autouse=True) +def mock_spi_final_validate(): + """Mock spi.final_validate_device_schema since unit tests have no real SPI bus config.""" + with mock.patch( + "esphome.components.spi.final_validate_device_schema", + return_value=lambda config: None, + ): + yield + + @pytest.fixture def choose_variant_with_pins() -> Generator[Callable[[list], None]]: """ diff --git a/tests/component_tests/mipi_spi/test_display_metadata.py b/tests/component_tests/mipi_spi/test_display_metadata.py new file mode 100644 index 0000000000..c11c7816e4 --- /dev/null +++ b/tests/component_tests/mipi_spi/test_display_metadata.py @@ -0,0 +1,202 @@ +"""Tests for display metadata created by mipi_spi component.""" + +from collections.abc import Callable +from pathlib import Path + +from esphome.components.display import ( + DisplayMetaData, + get_all_display_metadata, + get_display_metadata, +) +from esphome.components.esp32 import ( + KEY_BOARD, + KEY_VARIANT, + VARIANT_ESP32, + VARIANT_ESP32S3, +) +from esphome.components.mipi_spi.display import ( + CONFIG_SCHEMA, + FINAL_VALIDATE_SCHEMA, + get_instance, +) +from esphome.const import PlatformFramework +from tests.component_tests.types import SetCoreConfigCallable + + +def validated_config(config): + """Run schema + final validation and return the validated config.""" + config = CONFIG_SCHEMA(config) + FINAL_VALIDATE_SCHEMA(config) + return config + + +def test_metadata_native_quad_default_test_card( + set_core_config: SetCoreConfigCallable, +) -> None: + """A quad-mode display with no explicit drawing gets a test card from final validation.""" + set_core_config( + PlatformFramework.ESP32_IDF, + platform_data={KEY_BOARD: "esp32-s3-devkitc-1", KEY_VARIANT: VARIANT_ESP32S3}, + ) + config = validated_config({"model": "JC3636W518"}) + get_instance(config) + meta = get_display_metadata(str(config["id"])) + assert meta is not None + assert meta.width == 360 + assert meta.height == 360 + # final validation auto-enables show_test_card when no drawing methods are configured + assert meta.has_writer is True + assert meta.has_hardware_rotation is True + + +def test_metadata_single_mode_with_dc_pin( + set_core_config: SetCoreConfigCallable, +) -> None: + """A single-mode display with no explicit drawing gets a test card from final validation.""" + set_core_config( + PlatformFramework.ESP32_IDF, + platform_data={KEY_BOARD: "esp32dev", KEY_VARIANT: VARIANT_ESP32}, + ) + config = validated_config( + { + "model": "ST7735", + "dc_pin": 18, + } + ) + get_instance(config) + meta = get_display_metadata(str(config["id"])) + assert meta is not None + assert meta.width == 128 + assert meta.height == 160 + assert meta.has_writer is True + assert meta.has_hardware_rotation is True + + +def test_metadata_custom_dimensions( + set_core_config: SetCoreConfigCallable, +) -> None: + """A custom model picks up explicit dimensions.""" + set_core_config( + PlatformFramework.ESP32_IDF, + platform_data={KEY_BOARD: "esp32dev", KEY_VARIANT: VARIANT_ESP32}, + ) + config = validated_config( + { + "model": "custom", + "dc_pin": 18, + "dimensions": {"width": 480, "height": 320}, + "init_sequence": [[0xA0, 0x01]], + } + ) + get_instance(config) + meta = get_display_metadata(str(config["id"])) + assert meta is not None + assert meta.width == 480 + assert meta.height == 320 + # final validation auto-enables show_test_card + assert meta.has_writer is True + assert meta.has_hardware_rotation is True + + +def test_metadata_with_test_card_has_writer( + set_core_config: SetCoreConfigCallable, +) -> None: + """When show_test_card is enabled, has_writer should be True.""" + set_core_config( + PlatformFramework.ESP32_IDF, + platform_data={KEY_BOARD: "esp32dev", KEY_VARIANT: VARIANT_ESP32}, + ) + config = validated_config( + { + "model": "custom", + "dc_pin": 18, + "dimensions": {"width": 240, "height": 240}, + "init_sequence": [[0xA0, 0x01]], + "show_test_card": True, + } + ) + get_instance(config) + meta = get_display_metadata(str(config["id"])) + assert meta is not None + assert meta.has_writer is True + + +def test_metadata_no_swap_xy_not_full_hardware_rotation( + set_core_config: SetCoreConfigCallable, +) -> None: + """A model that disables swap_xy should report has_hardware_rotation=False.""" + set_core_config( + PlatformFramework.ESP32_IDF, + platform_data={KEY_BOARD: "esp32-s3-devkitc-1", KEY_VARIANT: VARIANT_ESP32S3}, + ) + # JC3248W535 has swap_xy=cv.UNDEFINED -> transforms={mirror_x, mirror_y} only + config = validated_config({"model": "JC3248W535"}) + get_instance(config) + meta = get_display_metadata(str(config["id"])) + assert meta is not None + assert meta.has_hardware_rotation is False + + +def test_metadata_multiple_displays_independent( + set_core_config: SetCoreConfigCallable, +) -> None: + """Multiple displays each get their own metadata entry.""" + set_core_config( + PlatformFramework.ESP32_IDF, + platform_data={KEY_BOARD: "esp32dev", KEY_VARIANT: VARIANT_ESP32}, + ) + config_a = validated_config( + { + "id": "disp_a", + "model": "custom", + "dc_pin": 18, + "dimensions": {"width": 320, "height": 240}, + "init_sequence": [[0xA0, 0x01]], + } + ) + config_b = validated_config( + { + "id": "disp_b", + "model": "custom", + "dc_pin": 19, + "dimensions": {"width": 128, "height": 64}, + "init_sequence": [[0xA0, 0x01]], + } + ) + get_instance(config_a) + get_instance(config_b) + + all_meta = get_all_display_metadata() + # final validation auto-enables show_test_card for both + assert all_meta["disp_a"] == DisplayMetaData(320, 240, True, True) + assert all_meta["disp_b"] == DisplayMetaData(128, 64, True, True) + + +def test_metadata_via_code_generation_native( + generate_main: Callable[[str | Path], str], + component_fixture_path: Callable[[str], Path], +) -> None: + """Full code generation for native.yaml should produce correct metadata.""" + generate_main(component_fixture_path("native.yaml")) + all_meta = get_all_display_metadata() + # native.yaml: model JC3636W518 -> 360x360, no writer, full hardware rotation + assert len(all_meta) == 1 + meta = next(iter(all_meta.values())) + assert meta == DisplayMetaData( + width=360, height=360, has_writer=True, has_hardware_rotation=True + ) + + +def test_metadata_via_code_generation_lvgl( + generate_main: Callable[[str | Path], str], + component_fixture_path: Callable[[str], Path], +) -> None: + """Full code generation for lvgl.yaml should produce correct metadata.""" + generate_main(component_fixture_path("lvgl.yaml")) + all_meta = get_all_display_metadata() + # lvgl.yaml: model ST7735 -> 128x160, no writer (lvgl draws directly), full hw rotation + assert len(all_meta) == 1 + meta = next(iter(all_meta.values())) + assert meta == DisplayMetaData( + width=128, height=160, has_writer=False, has_hardware_rotation=True + ) diff --git a/tests/component_tests/mipi_spi/test_init.py b/tests/component_tests/mipi_spi/test_init.py index bae39d3879..4873892a8d 100644 --- a/tests/component_tests/mipi_spi/test_init.py +++ b/tests/component_tests/mipi_spi/test_init.py @@ -204,11 +204,6 @@ def test_transform_and_init_sequence_errors( r"extra keys not allowed @ data\['brightness'\]", id="brightness_not_supported", ), - pytest.param( - {"model": "T-DISPLAY-S3-PRO"}, - "PSRAM is required for this display", - id="psram_required", - ), ], ) def test_esp32s3_specific_errors( @@ -319,7 +314,7 @@ def test_native_generation( main_cpp = generate_main(component_fixture_path("native.yaml")) assert ( - "mipi_spi::MipiSpiBuffer()" + "mipi_spi::MipiSpiBuffer()" in main_cpp ) assert "set_init_sequence({240, 1, 8, 242" in main_cpp @@ -335,7 +330,7 @@ def test_lvgl_generation( main_cpp = generate_main(component_fixture_path("lvgl.yaml")) assert ( - "mipi_spi::MipiSpi();" + "mipi_spi::MipiSpi();" in main_cpp ) assert "set_init_sequence({1, 0, 10, 255, 177" in main_cpp diff --git a/tests/components/ags10/common.yaml b/tests/components/ags10/common.yaml index 0551871e59..8009c56295 100644 --- a/tests/components/ags10/common.yaml +++ b/tests/components/ags10/common.yaml @@ -4,3 +4,14 @@ sensor: tvoc: name: AGS10 TVOC update_interval: 60s + +button: + - platform: template + name: "Test AGS10 Actions" + on_press: + - ags10.set_zero_point: + id: ags10_1 + mode: CURRENT_VALUE + - ags10.new_i2c_address: + id: ags10_1 + address: 0x1A diff --git a/tests/components/at581x/common.yaml b/tests/components/at581x/common.yaml index 425be47c42..dfb3a9a05e 100644 --- a/tests/components/at581x/common.yaml +++ b/tests/components/at581x/common.yaml @@ -12,6 +12,11 @@ esphome: trigger_keep: 10s stage_gain: 3 power_consumption: 70uA + - at581x.settings: + id: waveradar + frequency: !lambda "return 5800;" + poweron_selfcheck_time: !lambda "return 2000;" + power_consumption: !lambda "return 70;" - at581x.reset: id: waveradar diff --git a/tests/components/dsmr/common.yaml b/tests/components/dsmr/common.yaml index d11ce37d59..962800d7b0 100644 --- a/tests/components/dsmr/common.yaml +++ b/tests/components/dsmr/common.yaml @@ -13,3 +13,4 @@ dsmr: request_pin: ${request_pin} request_interval: 20s receive_timeout: 100ms + thermal_mbus_id: 3 diff --git a/tests/components/esp32/dummy_signing_key.pem b/tests/components/esp32/dummy_signing_key.pem new file mode 100644 index 0000000000..a74231d590 --- /dev/null +++ b/tests/components/esp32/dummy_signing_key.pem @@ -0,0 +1,41 @@ +*** DO NOT USE THIS KEY...EVER *** +-----BEGIN RSA PRIVATE KEY----- +MIIG5AIBAAKCAYEA0J665DlxzUzzouzH96fxqXybEfFU7H1oSf2fUHwoNMgUG7Vc +SHxuFJkpsUnxg9br09/v5THOXfUj5t/Arog6FGiL7i0HXCYDMnSn2EzQWR+DY2Qj +C3YzTLvcOQ40gFjDWfzAheAMCQmc5xQeB3YmaXQf+fUWH/PfFs9Pm+L92YTv2XC1 +B2q5s8K8hUWghO472A+UMrreuDcltNJ+TbuSRHK0NQzKpKo0Vkl4HycczGDgpa8D +h68JL/BKVeJAjKxWd/xcj/FCk661ODXi0esB/mGQP3hAthWpwi+gdkWczWs1Ocr6 +VxKje1zFm9SEq+SmCViPY/Pu8Xs7steqz3b3JtRGtKQE0r3B+hBKI7aRudOZyz0s +kqoL1zYrAWmoTWBqa2tj1ACPqtr2LyHGt2aVrHRQGJf21mPYIy9GIOv+3v3GzIAK +az2B8Z93Bw1biwNZDr1SLNYfQVaJT1hQavmdlvwW8vqLUGDcQlk42yOF6nAmvAPu +Wzxf+QFEtJT6Am65AgMBAAECggGAB0d+mG+LscDtYGI4MQNGaqZLJ+NelfjjPm+v +0yhd48eWcggQPgQ/eA8HFiVRHMtPQ7+U2I+2Fm+zDr+AcuaUdjlWppsiHlxCMMzC +vYiinXV8yWdJVMFNVXBZpRECknbmbBmmYxV3/gm8lJCOYq7D9NqFMhzT5o4FGv/l +VHhlaKVblB/7ZRSbgbL6DoFpMjI42tdiUanVEyLzeR1+JDq3BhXlhVNar8ezl04t +d5LPDa+UrxtN+XpJTQeqpFgGbhImSxjzCjo0kbGiEx/DwWuFJxguIcDU25sM4g2+ +ivtn7N11U0oaqNwsz7p4cKAm8toJYxxXWZvKdj1kZvCZ+BtyH0/MtOa2Q6v91HOh +zY4KEl5wxQYnxJrgqevSm8rrC51tLOCidZ16cHba8sjrK69xysEazk43roHLFXDp +JpH7Zd8LETjFWGVfUz6vppzkt6mrJk0DNuMLk/UwpPHzW2pu1qDiHPCb9+ra1S9U +t55hT2TBFDcG/NmZZnyHQoh8METhAoHBAPIG0G8Cd4fmkvbgCRLzCIWsH6zGHS9o +80Rj9Gu93B+m/F9GtgyYuX+DKSdMdw3IJamUsBwofT2wynmkuJFhLtD1FmYtlsXf +TWp8g8CfFGrIXDvin5E3heyhvtFiOjXlw0Q8yMmQXr5LF0i3WyFCPQM20ugClB7N +CQBOVAfpVoRU1fA6UjjHibFRwi1b4bLV69QiERPCJfcny/DPkZpu7I1fiINmwzEb +O5mIFo5F4TQADEreWplXEmhEXIzDMFIwEQKBwQDcqimCcO3RSysZMQhhUfk8G19I +yRNwvi2fK5LiGCZMYjeYKqg1rBN4yCf9PTwaqBNRqXTg13Fc7zrOkSI+0oDa4FWI +/kMEztaUK+Kwd2NKc96aXHMBGF+1Sx7Ygnr9e2dyqDqRij2/qlQYY1EDz7cYldaX +YNrXcQQeNJbqydjRDYi+9bI+wDkrK/5PxE1sGmqS1RMKxoJCZmxNiQT3PmXM/oNR +Ev6N9CDklFtClWNcD0Uum+mxNJ53ldZDx4UI/CkCgcEA6R6BI3vX0FHaGureMp9f +BQoulEdbEzBeqPAyHJkKbn50Nf0xGt78RYL7X7v6LI8tH7N1Eho5z/L6g8KSeI2H +/4MiqRaeVEdrFPeMHDvd+aC1noUBt2komS2OU7XuZb3CoHZ/3A4wA9DmQ4dAwr8/ +b1oeOZVKQISzd9T6gYhSajIgwzwZuFESInaitvf6ZDxC49hQZJyr3u05NeFo2Lyh +Iuby4cZYmnMlrBN1zmImseSd8ntL/sjslPvLvVXAtFlRAoHBAIDuG5rPiOTE2sW5 +VIAoeUuZYq8QbX9uXxGlUAkyuw3eRUVvhyD1DduAd30Ljla05bTNIjFNMDtwvBd9 +zViPfiJk+RU2GspwYAfrLGSXHTifQu1GHxwAtcsjvT4b3ujEdckUakQnVbTrPH+T +Z/6mGwEOa3e/a559tj4/0/4TOc/L7J5GyILJpZ2H8uuAcww60xI/1QRywCEz3wve +hzw/BRQlkWyJgJpIjf+Af2IEDy327iExj/WuHPkaXzrzFNQPIQKBwAM6qeNOxrO3 +V91wg4+44FAsOda62fZ0GlCM7ETnEjLbFamtCKEcDNfijwTa54LcZ6yObyutD1RN +dhj4Z6QKuYnsE02agv9CtXdFEVEXaqj4pshdgVOwGK34OidT4yIJQGLrRAQ/JiGH +x6CoGUCNIAq5J08VdosLTD9qdn1zv8USCAP0ReKnRMndTzENLYz9G3nQyHgt5GzI +YoSRtrWnXrQp2Yn3epk74gFAJtKozWNV4Du35FJBjmSeMuRivonNMQ== +-----END RSA PRIVATE KEY----- +*** DO NOT USE THIS KEY...EVER *** diff --git a/tests/components/esp32/test-signed_ota.esp32-s3-idf.yaml b/tests/components/esp32/test-signed_ota.esp32-s3-idf.yaml new file mode 100644 index 0000000000..cf1a54cfa1 --- /dev/null +++ b/tests/components/esp32/test-signed_ota.esp32-s3-idf.yaml @@ -0,0 +1,10 @@ +esp32: + variant: esp32s3 + framework: + type: esp-idf + advanced: + signed_ota_verification: + signing_key: ../../components/esp32/dummy_signing_key.pem + signing_scheme: rsa3072 + +<<: !include common.yaml diff --git a/tests/components/esp32_ble_server/common.yaml b/tests/components/esp32_ble_server/common.yaml index 7fe0b2eb5f..4e34049038 100644 --- a/tests/components/esp32_ble_server/common.yaml +++ b/tests/components/esp32_ble_server/common.yaml @@ -69,3 +69,11 @@ esp32_ble_server: - ble_server.descriptor.set_value: id: test_change_descriptor value: !lambda return bytebuffer::ByteBuffer::wrap({0x03, 0x04, 0x05}).get_data(); + - ble_server.characteristic.set_value: + id: test_change_characteristic + value: + data: [0xfc, 0xef, 0xfe, 0x86] + - ble_server.descriptor.set_value: + id: test_change_descriptor + value: + data: [0x01, 0x02, 0x03] diff --git a/tests/components/esp8266/test.esp8266-ard.yaml b/tests/components/esp8266/test.esp8266-ard.yaml index c77218f7a3..ba70c1a6a4 100644 --- a/tests/components/esp8266/test.esp8266-ard.yaml +++ b/tests/components/esp8266/test.esp8266-ard.yaml @@ -14,3 +14,6 @@ esphome: assert(x == 95); x = clamp_at_most(x, 40); assert(x == 40); + - lambda: |- + float value = 0.0f; + sscanf("3.14", "%f", &value); diff --git a/tests/components/espnow/common.yaml b/tests/components/espnow/common.yaml index b724af54e0..bdc478ea03 100644 --- a/tests/components/espnow/common.yaml +++ b/tests/components/espnow/common.yaml @@ -29,6 +29,8 @@ espnow: data: !lambda 'return {0x01, 0x02, 0x03, 0x04, 0x05};' - espnow.broadcast: data: "Hello, World!" + - espnow.broadcast: + data: "it's a test" - espnow.broadcast: data: [0x01, 0x02, 0x03, 0x04, 0x05] - espnow.broadcast: diff --git a/tests/components/ethernet/test-w5500.esp32-idf.yaml b/tests/components/ethernet/test-w5500.esp32-idf.yaml index 36f1b5365f..f1551fef60 100644 --- a/tests/components/ethernet/test-w5500.esp32-idf.yaml +++ b/tests/components/ethernet/test-w5500.esp32-idf.yaml @@ -1 +1,20 @@ -<<: !include common-w5500.yaml +ethernet: + type: W5500 + clk_pin: 19 + mosi_pin: 21 + miso_pin: 23 + cs_pin: 18 + interrupt_pin: 36 + reset_pin: 22 + clock_speed: 10Mhz + manual_ip: + static_ip: 192.168.178.56 + gateway: 192.168.178.1 + subnet: 255.255.255.0 + domain: .local + mac_address: "02:AA:BB:CC:DD:01" + interface: spi2 + on_connect: + - logger.log: "Ethernet connected!" + on_disconnect: + - logger.log: "Ethernet disconnected!" diff --git a/tests/components/hdc2080/common.yaml b/tests/components/hdc2080/common.yaml new file mode 100644 index 0000000000..cb14cb183b --- /dev/null +++ b/tests/components/hdc2080/common.yaml @@ -0,0 +1,7 @@ +sensor: + - platform: hdc2080 + temperature: + name: Temperature + humidity: + name: Humidity + update_interval: 15s diff --git a/tests/components/hdc2080/test.esp32-idf.yaml b/tests/components/hdc2080/test.esp32-idf.yaml new file mode 100644 index 0000000000..b47e39c389 --- /dev/null +++ b/tests/components/hdc2080/test.esp32-idf.yaml @@ -0,0 +1,4 @@ +packages: + i2c: !include ../../test_build_components/common/i2c/esp32-idf.yaml + +<<: !include common.yaml diff --git a/tests/components/hdc2080/test.esp8266-ard.yaml b/tests/components/hdc2080/test.esp8266-ard.yaml new file mode 100644 index 0000000000..4a98b9388a --- /dev/null +++ b/tests/components/hdc2080/test.esp8266-ard.yaml @@ -0,0 +1,4 @@ +packages: + i2c: !include ../../test_build_components/common/i2c/esp8266-ard.yaml + +<<: !include common.yaml diff --git a/tests/components/hdc2080/test.rp2040-ard.yaml b/tests/components/hdc2080/test.rp2040-ard.yaml new file mode 100644 index 0000000000..319a7c71a6 --- /dev/null +++ b/tests/components/hdc2080/test.rp2040-ard.yaml @@ -0,0 +1,4 @@ +packages: + i2c: !include ../../test_build_components/common/i2c/rp2040-ard.yaml + +<<: !include common.yaml diff --git a/tests/components/htu21d/common.yaml b/tests/components/htu21d/common.yaml index ad4b23d460..126360b775 100644 --- a/tests/components/htu21d/common.yaml +++ b/tests/components/htu21d/common.yaml @@ -1,5 +1,6 @@ sensor: - platform: htu21d + id: htu21d_sensor i2c_id: i2c_bus model: htu21d temperature: @@ -9,3 +10,14 @@ sensor: heater: name: Heater update_interval: 15s + +button: + - platform: template + name: "Test HTU21D Actions" + on_press: + - htu21d.set_heater: + id: htu21d_sensor + status: true + - htu21d.set_heater_level: + id: htu21d_sensor + level: 5 diff --git a/tests/components/internal_temperature/test.ln882x-ard.yaml b/tests/components/internal_temperature/test.ln882x-ard.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/internal_temperature/test.ln882x-ard.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/internal_temperature/test.nrf52-adafruit.yaml b/tests/components/internal_temperature/test.nrf52-adafruit.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/internal_temperature/test.nrf52-adafruit.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/lvgl/lvgl-package.yaml b/tests/components/lvgl/lvgl-package.yaml index b168578a98..967fe51592 100644 --- a/tests/components/lvgl/lvgl-package.yaml +++ b/tests/components/lvgl/lvgl-package.yaml @@ -30,18 +30,63 @@ binary_sensor: return y; lvgl: + id: lvgl_id + rotation: 90 log_level: debug resume_on_input: true on_pause: - logger.log: LVGL is Paused + - logger.log: LVGL is Paused + - lvgl.display.set_rotation: 90 on_resume: - logger.log: LVGL has resumed + - logger.log: LVGL has resumed + - lvgl.display.set_rotation: + rotation: 0 + lvgl_id: lvgl_id + on_boot: - logger.log: LVGL has started - lvgl.indicator.update: id: meter_arc_indicator start_value: 0 end_value: 180 + on_invalidate_area: + logger.log: Invalidate area + on_resolution_change: + logger.log: Resolution changed + on_color_format_change: + logger.log: Color format changed + on_refr_request: + logger.log: Refresh request + on_refr_start: + logger.log: Refresh start + on_refr_ready: + logger.log: Refresh ready + on_render_start: + logger.log: Render start + on_render_ready: + logger.log: Render ready + on_flush_start: + logger.log: Flush start + on_flush_finish: + logger.log: Flush finish + on_flush_wait_start: + logger.log: Flush wait start + on_flush_wait_finish: + logger.log: Flush wait finish + on_update_layout_complete: + logger.log: Update layout complete + on_vsync: + logger.log: Vsync + on_vsync_request: + logger.log: Vsync request + on_screen_load_start: + logger.log: Screen load start + on_screen_load: + logger.log: Screen loaded + on_screen_unload: + logger.log: Screen unloaded + on_screen_unload_start: + logger.log: Screen unload start bg_color: light_blue bottom_layer: widgets: @@ -49,6 +94,7 @@ lvgl: bg_color: 0x000000 bg_opa: cover theme: + dark_mode: true obj: border_width: 1 @@ -232,7 +278,7 @@ lvgl: - roller: id: lv_roller visible_row_count: 2 - anim_time: 500ms + anim_duration: 500ms options: - Nov - Dec @@ -288,7 +334,9 @@ lvgl: - label: text: "Hello shiny day" text_color: 0xFFFFFF - align: bottom_mid + align_to: + id: hello_label + align: OUT_LEFT_TOP - label: id: setup_lambda_label # Test lambda in widget property during setup (LvContext) @@ -315,20 +363,27 @@ lvgl: align: top_left - container: align: center + anim_duration: 1s arc_opa: COVER arc_color: 0xFF0000 arc_rounded: false arc_width: 3 - anim_time: 1s + base_dir: auto bg_color: light_blue bg_grad_color: light_blue bg_grad_dir: hor + bg_grad_opa: cover bg_grad_stop: 128 bg_image_opa: transp bg_image_recolor: light_blue bg_image_recolor_opa: 50% + bg_main_opa: cover bg_main_stop: 0 bg_opa: 20% + blend_mode: normal + blur_backdrop: false + blur_quality: auto + blur_radius: 0 border_color: 0x00FF00 border_opa: cover border_post: true @@ -336,7 +391,15 @@ lvgl: border_width: 4 clip_corner: false color_filter_opa: transp + drop_shadow_color: 0x000000 + drop_shadow_offset_x: 5 + drop_shadow_offset_y: 5 + drop_shadow_opa: cover + drop_shadow_quality: precision + drop_shadow_radius: 10 + ext_click_area: 100px height: 50% + image_opa: cover image_recolor: light_blue image_recolor_opa: cover line_width: 10 @@ -344,6 +407,10 @@ lvgl: line_dash_gap: 10 line_rounded: false line_color: light_blue + margin_bottom: 4 + margin_left: 4 + margin_right: 4 + margin_top: 4 opa: cover opa_layered: cover outline_color: light_blue @@ -353,8 +420,12 @@ lvgl: pad_all: 10px pad_bottom: 10px pad_left: 10px + pad_radial: 0 pad_right: 10px pad_top: 10px + recolor: 0xFF0000 + recolor_opa: transp + rotary_sensitivity: 256 shadow_color: light_blue shadow_opa: cover shadow_spread: 5 @@ -366,6 +437,9 @@ lvgl: text_letter_space: 4 text_line_space: 4 text_opa: cover + text_outline_stroke_color: 0x000000 + text_outline_stroke_opa: cover + text_outline_stroke_width: 2 transform_rotation: 90 transform_height: 100 transform_pivot_x: 50% @@ -375,8 +449,10 @@ lvgl: transform_scale_y: 0.8 transform_skew_x: 10 transform_skew_y: 20 + transform_width: 100 shadow_offset_x: 3 shadow_offset_y: 3 + translate_radial: 0 translate_x: 10 translate_y: 10 max_height: 100 @@ -481,6 +557,7 @@ lvgl: image: src: cat_image align: top_left + bitmap_mask_src: alert on_click: - lvgl.widget.focus: spin_up - lvgl.widget.focus: next @@ -559,6 +636,76 @@ lvgl: logger.log: Button clicked on_long_press_repeat: logger.log: Button clicked + on_pressing: + logger.log: Button pressing + on_press_lost: + logger.log: Button press lost + on_single_click: + logger.log: Button single clicked + on_double_click: + logger.log: Button double clicked + on_triple_click: + logger.log: Button triple clicked + on_scroll_throw_begin: + logger.log: Scroll throw begin + on_gesture: + logger.log: Gesture detected + on_key: + logger.log: Key event + on_rotary: + logger.log: Rotary event + on_leave: + logger.log: Leave event + on_hit_test: + logger.log: Hit test + on_indev_reset: + logger.log: Indev reset + on_hover_over: + logger.log: Hover over + on_hover_leave: + logger.log: Hover leave + on_cover_check: + logger.log: Cover check + on_refr_ext_draw_size: + logger.log: Refr ext draw size + on_draw_main_begin: + logger.log: Draw main begin + on_draw_main: + logger.log: Draw main + on_draw_main_end: + logger.log: Draw main end + on_draw_post_begin: + logger.log: Draw post begin + on_draw_post: + logger.log: Draw post + on_draw_post_end: + logger.log: Draw post end + on_draw_task_add: + logger.log: Draw task add + on_insert: + logger.log: Insert event + on_refresh: + logger.log: Refresh event + on_state_change: + logger.log: State changed + on_create: + logger.log: Create event + on_delete: + logger.log: Delete event + on_child_change: + logger.log: Child changed + on_child_create: + logger.log: Child created + on_child_delete: + logger.log: Child deleted + on_size_change: + logger.log: Size changed + on_style_change: + logger.log: Style changed + on_layout_change: + logger.log: Layout changed + on_get_self_size: + logger.log: Get self size - led: id: lv_led color: 0x00FF00 @@ -1051,7 +1198,7 @@ lvgl: - ticks: width: 1 count: 61 - length: 20% + length: 20 radial_offset: 5 color: 0xFFFFFF major: @@ -1159,6 +1306,13 @@ image: type: BINARY transparency: chroma_key + - id: alert + file: $component_dir/logo-text.svg + type: grayscale + resize: 100x100 + invert_alpha: true + transparency: alpha_channel + color: - id: light_blue hex: "3340FF" diff --git a/tests/components/mcp23008/common.yaml b/tests/components/mcp23008/common.yaml index 4a407adfd8..7eeee409ff 100644 --- a/tests/components/mcp23008/common.yaml +++ b/tests/components/mcp23008/common.yaml @@ -1,6 +1,10 @@ mcp23008: - i2c_id: i2c_bus - id: mcp23008_hub + - i2c_id: i2c_bus + id: mcp23008_hub + - i2c_id: i2c_bus + id: mcp23008_hub_int + address: 0x21 + interrupt_pin: ${interrupt_pin} binary_sensor: - platform: gpio @@ -9,6 +13,12 @@ binary_sensor: mcp23xxx: mcp23008_hub number: 0 mode: INPUT + - platform: gpio + id: mcp23008_binary_sensor_int + pin: + mcp23xxx: mcp23008_hub_int + number: 0 + mode: INPUT switch: - platform: gpio diff --git a/tests/components/mcp23008/test.esp32-idf.yaml b/tests/components/mcp23008/test.esp32-idf.yaml index b47e39c389..8c3b341dce 100644 --- a/tests/components/mcp23008/test.esp32-idf.yaml +++ b/tests/components/mcp23008/test.esp32-idf.yaml @@ -1,3 +1,6 @@ +substitutions: + interrupt_pin: GPIO15 + packages: i2c: !include ../../test_build_components/common/i2c/esp32-idf.yaml diff --git a/tests/components/mcp23008/test.esp8266-ard.yaml b/tests/components/mcp23008/test.esp8266-ard.yaml index 4a98b9388a..69b243bfd8 100644 --- a/tests/components/mcp23008/test.esp8266-ard.yaml +++ b/tests/components/mcp23008/test.esp8266-ard.yaml @@ -1,3 +1,6 @@ +substitutions: + interrupt_pin: GPIO15 + packages: i2c: !include ../../test_build_components/common/i2c/esp8266-ard.yaml diff --git a/tests/components/mcp23008/test.rp2040-ard.yaml b/tests/components/mcp23008/test.rp2040-ard.yaml index 319a7c71a6..b8ad1e4792 100644 --- a/tests/components/mcp23008/test.rp2040-ard.yaml +++ b/tests/components/mcp23008/test.rp2040-ard.yaml @@ -1,3 +1,6 @@ +substitutions: + interrupt_pin: GPIO2 + packages: i2c: !include ../../test_build_components/common/i2c/rp2040-ard.yaml diff --git a/tests/components/mcp23017/common.yaml b/tests/components/mcp23017/common.yaml index 54a97e911f..8b1e32600e 100644 --- a/tests/components/mcp23017/common.yaml +++ b/tests/components/mcp23017/common.yaml @@ -1,6 +1,10 @@ mcp23017: - i2c_id: i2c_bus - id: mcp23017_hub + - i2c_id: i2c_bus + id: mcp23017_hub + - i2c_id: i2c_bus + id: mcp23017_hub_int + address: 0x21 + interrupt_pin: ${interrupt_pin} binary_sensor: - platform: gpio @@ -9,6 +13,12 @@ binary_sensor: mcp23xxx: mcp23017_hub number: 0 mode: INPUT + - platform: gpio + id: mcp23017_binary_sensor_int + pin: + mcp23xxx: mcp23017_hub_int + number: 0 + mode: INPUT switch: - platform: gpio diff --git a/tests/components/mcp23017/test.esp32-idf.yaml b/tests/components/mcp23017/test.esp32-idf.yaml index b47e39c389..8c3b341dce 100644 --- a/tests/components/mcp23017/test.esp32-idf.yaml +++ b/tests/components/mcp23017/test.esp32-idf.yaml @@ -1,3 +1,6 @@ +substitutions: + interrupt_pin: GPIO15 + packages: i2c: !include ../../test_build_components/common/i2c/esp32-idf.yaml diff --git a/tests/components/mcp23017/test.esp8266-ard.yaml b/tests/components/mcp23017/test.esp8266-ard.yaml index 4a98b9388a..69b243bfd8 100644 --- a/tests/components/mcp23017/test.esp8266-ard.yaml +++ b/tests/components/mcp23017/test.esp8266-ard.yaml @@ -1,3 +1,6 @@ +substitutions: + interrupt_pin: GPIO15 + packages: i2c: !include ../../test_build_components/common/i2c/esp8266-ard.yaml diff --git a/tests/components/mcp23017/test.rp2040-ard.yaml b/tests/components/mcp23017/test.rp2040-ard.yaml index 319a7c71a6..b8ad1e4792 100644 --- a/tests/components/mcp23017/test.rp2040-ard.yaml +++ b/tests/components/mcp23017/test.rp2040-ard.yaml @@ -1,3 +1,6 @@ +substitutions: + interrupt_pin: GPIO2 + packages: i2c: !include ../../test_build_components/common/i2c/rp2040-ard.yaml diff --git a/tests/components/mcp23s08/common.yaml b/tests/components/mcp23s08/common.yaml index 2170ae0459..327bf02ca9 100644 --- a/tests/components/mcp23s08/common.yaml +++ b/tests/components/mcp23s08/common.yaml @@ -2,3 +2,4 @@ mcp23s08: - id: mcp23s08_hub cs_pin: ${cs_pin} deviceaddress: 0 + interrupt_pin: ${interrupt_pin} diff --git a/tests/components/mcp23s08/test.esp32-idf.yaml b/tests/components/mcp23s08/test.esp32-idf.yaml index a3352cf880..eeb6941645 100644 --- a/tests/components/mcp23s08/test.esp32-idf.yaml +++ b/tests/components/mcp23s08/test.esp32-idf.yaml @@ -1,5 +1,6 @@ substitutions: cs_pin: GPIO5 + interrupt_pin: GPIO15 packages: spi: !include ../../test_build_components/common/spi/esp32-idf.yaml diff --git a/tests/components/mcp23s08/test.esp8266-ard.yaml b/tests/components/mcp23s08/test.esp8266-ard.yaml index 595f31046a..ffc40b3595 100644 --- a/tests/components/mcp23s08/test.esp8266-ard.yaml +++ b/tests/components/mcp23s08/test.esp8266-ard.yaml @@ -1,5 +1,6 @@ substitutions: cs_pin: GPIO15 + interrupt_pin: GPIO0 packages: spi: !include ../../test_build_components/common/spi/esp8266-ard.yaml diff --git a/tests/components/mcp23s08/test.rp2040-ard.yaml b/tests/components/mcp23s08/test.rp2040-ard.yaml index 79ea6ce90b..09b87ca3f8 100644 --- a/tests/components/mcp23s08/test.rp2040-ard.yaml +++ b/tests/components/mcp23s08/test.rp2040-ard.yaml @@ -1,5 +1,6 @@ substitutions: cs_pin: GPIO5 + interrupt_pin: GPIO2 packages: spi: !include ../../test_build_components/common/spi/rp2040-ard.yaml diff --git a/tests/components/mcp23s17/common.yaml b/tests/components/mcp23s17/common.yaml index a89beeb16b..150ecca325 100644 --- a/tests/components/mcp23s17/common.yaml +++ b/tests/components/mcp23s17/common.yaml @@ -2,3 +2,4 @@ mcp23s17: - id: mcp23s17_hub cs_pin: ${cs_pin} deviceaddress: 0 + interrupt_pin: ${interrupt_pin} diff --git a/tests/components/mcp23s17/test.esp32-idf.yaml b/tests/components/mcp23s17/test.esp32-idf.yaml index a3352cf880..eeb6941645 100644 --- a/tests/components/mcp23s17/test.esp32-idf.yaml +++ b/tests/components/mcp23s17/test.esp32-idf.yaml @@ -1,5 +1,6 @@ substitutions: cs_pin: GPIO5 + interrupt_pin: GPIO15 packages: spi: !include ../../test_build_components/common/spi/esp32-idf.yaml diff --git a/tests/components/mcp23s17/test.esp8266-ard.yaml b/tests/components/mcp23s17/test.esp8266-ard.yaml index 595f31046a..ffc40b3595 100644 --- a/tests/components/mcp23s17/test.esp8266-ard.yaml +++ b/tests/components/mcp23s17/test.esp8266-ard.yaml @@ -1,5 +1,6 @@ substitutions: cs_pin: GPIO15 + interrupt_pin: GPIO0 packages: spi: !include ../../test_build_components/common/spi/esp8266-ard.yaml diff --git a/tests/components/mcp23s17/test.rp2040-ard.yaml b/tests/components/mcp23s17/test.rp2040-ard.yaml index 79ea6ce90b..09b87ca3f8 100644 --- a/tests/components/mcp23s17/test.rp2040-ard.yaml +++ b/tests/components/mcp23s17/test.rp2040-ard.yaml @@ -1,5 +1,6 @@ substitutions: cs_pin: GPIO5 + interrupt_pin: GPIO2 packages: spi: !include ../../test_build_components/common/spi/rp2040-ard.yaml diff --git a/tests/components/media_player/common.yaml b/tests/components/media_player/common.yaml index 89600f70f6..88d04d0ff0 100644 --- a/tests/components/media_player/common.yaml +++ b/tests/components/media_player/common.yaml @@ -61,3 +61,8 @@ media_player: - media_player.volume_up: - media_player.volume_down: - media_player.volume_set: 50% + - media_player.enqueue: http://localhost/media.mp3 + - media_player.enqueue: !lambda 'return "http://localhost/media.mp3";' + - media_player.enqueue: + media_url: http://localhost/media.mp3 + announcement: true diff --git a/tests/components/mitsubishi_cn105/climate/mitsubishi_cn105_tests.cpp b/tests/components/mitsubishi_cn105/climate/mitsubishi_cn105_tests.cpp new file mode 100644 index 0000000000..7846a31193 --- /dev/null +++ b/tests/components/mitsubishi_cn105/climate/mitsubishi_cn105_tests.cpp @@ -0,0 +1,396 @@ +#include "../common.h" + +namespace esphome::mitsubishi_cn105::testing { + +struct TestContext { + MockUARTComponent uart; + uart::UARTDevice device{&uart}; + TestableMitsubishiCN105 sut{device}; + + TestContext() { this->sut.set_current_time(0); } +}; + +TEST(MitsubishiCN105Tests, InitSendsConnectPacket) { + auto ctx = TestContext{}; + + ctx.sut.set_current_time(123); + EXPECT_EQ(ctx.sut.state_, TestableMitsubishiCN105::State::NOT_CONNECTED); + EXPECT_TRUE(ctx.uart.tx.empty()); + EXPECT_FALSE(ctx.sut.write_timeout_start_ms_.has_value()); + + ctx.sut.initialize(); + + EXPECT_EQ(ctx.sut.state_, TestableMitsubishiCN105::State::CONNECTING); + EXPECT_THAT(ctx.uart.tx, ::testing::ElementsAre(0xFC, 0x5A, 0x01, 0x30, 0x02, 0xCA, 0x01, 0xA8)); + EXPECT_EQ(ctx.sut.write_timeout_start_ms_, std::optional{123}); +} + +TEST(MitsubishiCN105Tests, ConnectAndUpdateStatus) { + auto ctx = TestContext{}; + + ctx.sut.initialize(); + ctx.uart.tx.clear(); // Remove first connect packet bytes + + EXPECT_EQ(ctx.sut.state_, TestableMitsubishiCN105::State::CONNECTING); + EXPECT_EQ(ctx.sut.write_timeout_start_ms_, std::optional{0}); + EXPECT_FALSE(ctx.sut.status_update_start_ms_.has_value()); + + // Connect response + ctx.uart.push_rx({0xFC, 0x7A, 0x01, 0x30, 0x00, 0x55}); + + ctx.sut.set_current_time(200); + ASSERT_FALSE(ctx.sut.update()); + + // All bytes from UART should be consumed + EXPECT_TRUE(ctx.uart.rx.empty()); + // After successful connect we request status, first settings (0x02) + EXPECT_EQ(ctx.sut.state_, TestableMitsubishiCN105::State::UPDATING_STATUS); + EXPECT_THAT(ctx.uart.tx, ::testing::ElementsAre(0xFC, 0x42, 0x01, 0x30, 0x10, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x7B)); + EXPECT_EQ(ctx.sut.write_timeout_start_ms_, std::optional{200}); + EXPECT_FALSE(ctx.sut.status_update_start_ms_.has_value()); + + // Clear TX bytes. + ctx.uart.tx.clear(); + + // Settings response + ctx.uart.push_rx({0xFC, 0x62, 0x01, 0x30, 0x10, 0x02, 0x00, 0x00, 0x00, 0x08, 0x07, + 0x00, 0x00, 0x00, 0x00, 0x03, 0xB0, 0x00, 0x00, 0x00, 0x00, 0x99}); + + // Settings should still have initial values + EXPECT_FALSE(ctx.sut.status().power_on); + EXPECT_THAT(ctx.sut.status().target_temperature, ::testing::IsNan()); + EXPECT_EQ(ctx.sut.status().mode, MitsubishiCN105::Mode::UNKNOWN); + EXPECT_EQ(ctx.sut.status().fan_mode, MitsubishiCN105::FanMode::UNKNOWN); + + ctx.sut.set_current_time(300); + ASSERT_FALSE(ctx.sut.update()); + EXPECT_TRUE(ctx.uart.rx.empty()); + + // Check settings that we just read from received package + EXPECT_FALSE(ctx.sut.status().power_on); + EXPECT_EQ(ctx.sut.status().target_temperature, 24.0f); + EXPECT_EQ(ctx.sut.status().mode, MitsubishiCN105::Mode::AUTO); + EXPECT_EQ(ctx.sut.status().fan_mode, MitsubishiCN105::FanMode::AUTO); + + // Now fetch room temperature (0x03) + EXPECT_EQ(ctx.sut.state_, TestableMitsubishiCN105::State::UPDATING_STATUS); + EXPECT_THAT(ctx.uart.tx, ::testing::ElementsAre(0xFC, 0x42, 0x01, 0x30, 0x10, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x7A)); + EXPECT_EQ(ctx.sut.write_timeout_start_ms_, std::optional{300}); + EXPECT_FALSE(ctx.sut.status_update_start_ms_.has_value()); + + // Clear TX bytes. + ctx.uart.tx.clear(); + + // Room temperature response + ctx.uart.push_rx({0xFC, 0x62, 0x01, 0x30, 0x10, 0x03, 0x00, 0x00, 0x0B, 0x00, 0x00, + 0xAA, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xA5}); + + // Room temperature should still have initial value + EXPECT_THAT(ctx.sut.status().room_temperature, ::testing::IsNan()); + + ctx.sut.set_current_time(400); + EXPECT_FALSE(ctx.sut.is_status_initialized()); + ASSERT_TRUE(ctx.sut.update()); + EXPECT_TRUE(ctx.uart.rx.empty()); + EXPECT_TRUE(ctx.sut.is_status_initialized()); + + // Check room temperature we just read from received package + EXPECT_EQ(ctx.sut.status().room_temperature, 21.0f); + + EXPECT_TRUE(ctx.uart.tx.empty()); + EXPECT_EQ(ctx.sut.state_, TestableMitsubishiCN105::State::WAITING_FOR_SCHEDULED_STATUS_UPDATE); + EXPECT_FALSE(ctx.sut.write_timeout_start_ms_.has_value()); + EXPECT_EQ(ctx.sut.status_update_start_ms_, std::optional{400}); +} + +TEST(MitsubishiCN105Tests, NoResponseTriggersReconnect) { + auto ctx = TestContext{}; + + ctx.sut.initialize(); + ctx.uart.tx.clear(); // Remove first connect packet bytes + + // No response (no RX data), no retry yet + ASSERT_FALSE(ctx.sut.update()); + EXPECT_EQ(ctx.sut.state_, TestableMitsubishiCN105::State::CONNECTING); + EXPECT_TRUE(ctx.uart.tx.empty()); + EXPECT_EQ(ctx.sut.write_timeout_start_ms_, std::optional{0}); + + // Still no response after 1999ms, no retry yet + ctx.sut.set_current_time(1999); + ASSERT_FALSE(ctx.sut.update()); + EXPECT_EQ(ctx.sut.state_, TestableMitsubishiCN105::State::CONNECTING); + EXPECT_TRUE(ctx.uart.tx.empty()); + EXPECT_EQ(ctx.sut.write_timeout_start_ms_, std::optional{0}); + + // Stop waiting after 2s and retry connect + ctx.sut.set_current_time(2000); + ASSERT_FALSE(ctx.sut.update()); + EXPECT_EQ(ctx.sut.state_, TestableMitsubishiCN105::State::CONNECTING); + EXPECT_THAT(ctx.uart.tx, ::testing::ElementsAre(0xFC, 0x5A, 0x01, 0x30, 0x02, 0xCA, 0x01, 0xA8)); + EXPECT_EQ(ctx.sut.write_timeout_start_ms_, std::optional{2000}); +} + +TEST(MitsubishiCN105Tests, RxWatchdogLimitsProcessingPerUpdate) { + auto ctx = TestContext{}; + + ctx.sut.initialize(); + ctx.uart.tx.clear(); // Remove first connect packet bytes + + // RX noise/unexpected traffic + ctx.uart.push_rx({0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0A, 0x0B, 0x0C, 0x0D, 0x0E, + 0x0F, 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, 0x19, 0x1A, 0x1B, 0x1C, + 0x1D, 0x1E, 0x1F, 0x20, 0x21, 0x22, 0x23, 0x24, 0x25, 0x26, 0x27, 0x28, 0x29, 0x2A, + 0x2B, 0x2C, 0x2D, 0x2E, 0x2F, 0x30, 0x31, 0x32, 0x33, 0x34, 0x35, 0x36, 0x37, 0x38, + 0x39, 0x3A, 0x3B, 0x3C, 0x3D, 0x3E, 0x3F, 0x40, 0x41, 0x42, 0x43, 0x44, 0x45, 0x46}); + + // Make sure we have enough bytes in buffer. + ASSERT_GT(ctx.uart.rx.size(), 64); + + // No valid response, no state change expected + ASSERT_FALSE(ctx.sut.update()); + EXPECT_EQ(ctx.sut.state_, TestableMitsubishiCN105::State::CONNECTING); + EXPECT_TRUE(ctx.uart.tx.empty()); + + // Watchdog interrupts reading (max. 64 bytes at once) so we do not spend the whole loop draining UART + EXPECT_FALSE(ctx.uart.rx.empty()); + + // Next update will read remaining bytes, no state change expected + ASSERT_FALSE(ctx.sut.update()); + EXPECT_EQ(ctx.sut.state_, TestableMitsubishiCN105::State::CONNECTING); + EXPECT_TRUE(ctx.uart.tx.empty()); + EXPECT_TRUE(ctx.uart.rx.empty()); +} + +TEST(MitsubishiCN105Tests, ParserHandlesMixedRxStream) { + auto ctx = TestContext{}; + + ctx.sut.initialize(); + ctx.uart.tx.clear(); // Remove first connect packet bytes + + // Mixed RX stream with partial, malformed, and oversized frames to test parser robustness + ctx.uart.push_rx({// ───────────────────────────── + // Noise (no 0xFC) -> should be ignored via preamble reset + // ──────────────────────────── + 0x01, 0x02, 0x03, 0x04, 0x05, + + // ───────────────────────────── + // Partial frame (declares payload len=5, but we cut it short) + // Later bytes will eventually force checksum mismatch and reset + // ───────────────────────────── + 0xFC, 0x62, 0x01, 0x30, 0x05, 0xAA, 0xBB, + + // ───────────────────────────── + // Invalid header (header byte 3 should be 0x01, header byte 4 should be 0x30) + // Should reset quickly on header mismatch + // ───────────────────────────── + 0xFC, 0x62, 0xFF, 0xFF, 0x02, 0x01, 0x02, 0x00, + + // ───────────────────────────── + // Oversized length field (rejected by payload-too-large check at HEADER_LEN) + // ───────────────────────────── + 0xFC, 0x62, 0x01, 0x30, 0xFE, 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, 0x19, 0x1A, + 0x1B, 0x1C, 0x1D, 0x1E, 0x1F, + + // ───────────────────────────── + // Valid unknown-type frame (type=0x62), should be parsed successfully then ignored + // Frame: FC 62 01 30 02 AA BB 30 + // ───────────────────────────── + 0xFC, 0x62, 0x01, 0x30, 0x02, 0xAA, 0xBB, 0x30, + + // ───────────────────────────── + // Invalid checksum (should be rejected at checksum check) + // ───────────────────────────── + 0xFC, 0x62, 0x01, 0x30, 0x02, 0x10, 0x20, 0xFF, + + // ───────────────────────────── + // Back-to-back VALID frames (unknown type=0x62) to stress boundary handling. + // Frame A: FC 62 01 30 01 02 6C + // Frame B: FC 62 01 30 01 03 6B + // ───────────────────────────── + 0xFC, 0x62, 0x01, 0x30, 0x01, 0x02, 0x6C, 0xFC, 0x62, 0x01, 0x30, 0x01, 0x03, 0x6B, + + // ───────────────────────────── + // Trailing noise + // ───────────────────────────── + 0x55, 0x66, 0x77, 0x88}); + + // Drain RX - no valid response, no state change expected + int iterations = 0; + while (!ctx.uart.rx.empty() && iterations++ < 10) { + ASSERT_FALSE(ctx.sut.update()); + EXPECT_EQ(ctx.sut.state_, TestableMitsubishiCN105::State::CONNECTING); + EXPECT_TRUE(ctx.uart.tx.empty()); + } + + EXPECT_TRUE(ctx.uart.rx.empty()); +} + +TEST(MitsubishiCN105Tests, NextStatusUpdateAfterUpdateIntervalMilliseconds) { + auto ctx = TestContext{}; + + ctx.sut.set_update_interval(2000); + ctx.sut.set_current_time(80000); + + // No scheduled status update + EXPECT_FALSE(ctx.sut.status_update_start_ms_.has_value()); + + // Status update completed, schedule next status update + ctx.sut.state_ = TestableMitsubishiCN105::State::STATUS_UPDATED; + ctx.sut.set_state(TestableMitsubishiCN105::State::SCHEDULE_NEXT_STATUS_UPDATE); + + EXPECT_EQ(ctx.sut.state_, TestableMitsubishiCN105::State::WAITING_FOR_SCHEDULED_STATUS_UPDATE); + EXPECT_EQ(ctx.sut.status_update_start_ms_, std::optional{80000}); + + // Wait for update_interval (ms) before doing another status update + ASSERT_FALSE(ctx.sut.update()); + EXPECT_TRUE(ctx.uart.tx.empty()); + EXPECT_EQ(ctx.sut.state_, TestableMitsubishiCN105::State::WAITING_FOR_SCHEDULED_STATUS_UPDATE); + + ctx.sut.set_current_time(81999); + ASSERT_FALSE(ctx.sut.update()); + EXPECT_TRUE(ctx.uart.tx.empty()); + EXPECT_EQ(ctx.sut.state_, TestableMitsubishiCN105::State::WAITING_FOR_SCHEDULED_STATUS_UPDATE); + + ctx.sut.set_current_time(82000); + ASSERT_FALSE(ctx.sut.update()); + EXPECT_FALSE(ctx.uart.tx.empty()); + EXPECT_EQ(ctx.sut.state_, TestableMitsubishiCN105::State::UPDATING_STATUS); + EXPECT_FALSE(ctx.sut.status_update_start_ms_.has_value()); +} + +TEST(MitsubishiCN105Tests, DecodeStatusSettingsPackageTempEncodedA) { + auto ctx = TestContext{}; + + ctx.uart.push_rx( + {0xFC, 0x62, 0x01, 0x30, 0x0C, 0x02, 0x00, 0x00, 0x01, 0x03, 0x05, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x55}); + + ctx.sut.update(); + + EXPECT_TRUE(ctx.sut.status().power_on); + EXPECT_FALSE(ctx.sut.use_temperature_encoding_b_); + EXPECT_EQ(ctx.sut.status().target_temperature, 26.0f); + EXPECT_EQ(ctx.sut.status().mode, MitsubishiCN105::Mode::COOL); + EXPECT_EQ(ctx.sut.status().fan_mode, MitsubishiCN105::FanMode::QUIET); +} + +TEST(MitsubishiCN105Tests, DecodeStatusSettingsPackageTempEncodedB) { + auto ctx = TestContext{}; + + ctx.uart.push_rx( + {0xFC, 0x62, 0x01, 0x30, 0x0C, 0x02, 0x00, 0x00, 0x00, 0x07, 0x00, 0x06, 0x00, 0x00, 0x00, 0x00, 0xA5, 0xAD}); + + ctx.sut.update(); + + EXPECT_FALSE(ctx.sut.status().power_on); + EXPECT_TRUE(ctx.sut.use_temperature_encoding_b_); + EXPECT_EQ(ctx.sut.status().target_temperature, 18.5f); + EXPECT_EQ(ctx.sut.status().mode, MitsubishiCN105::Mode::FAN_ONLY); + EXPECT_EQ(ctx.sut.status().fan_mode, MitsubishiCN105::FanMode::SPEED_4); +} + +TEST(MitsubishiCN105Tests, DecodeStatusRoomTempPackageTempEncodedA) { + auto ctx = TestContext{}; + + ctx.uart.push_rx({0xFC, 0x62, 0x01, 0x30, 0x07, 0x03, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x5D}); + + ctx.sut.update(); + + EXPECT_EQ(ctx.sut.status().room_temperature, 16.0f); +} + +TEST(MitsubishiCN105Tests, DecodeStatusRoomTempPackageTempEncodedB) { + auto ctx = TestContext{}; + + ctx.uart.push_rx({0xFC, 0x62, 0x01, 0x30, 0x07, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0xBC, 0xA7}); + + ctx.sut.update(); + + EXPECT_EQ(ctx.sut.status().room_temperature, 30.0f); +} + +TEST(MitsubishiCN105Tests, ApplySettingsPowerOn) { + auto ctx = TestContext{}; + + ctx.sut.set_power(true); + ctx.sut.apply_settings(); + + EXPECT_THAT(ctx.uart.tx, ::testing::ElementsAre(0xFC, 0x41, 0x01, 0x30, 0x10, 0x01, 0x01, 0x00, 0x01, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x7B)); +} + +TEST(MitsubishiCN105Tests, ApplySettingsTemperatureEncodedA) { + auto ctx = TestContext{}; + + ctx.sut.set_target_temperature(23.0f); + ctx.sut.apply_settings(); + + EXPECT_THAT(ctx.uart.tx, ::testing::ElementsAre(0xFC, 0x41, 0x01, 0x30, 0x10, 0x01, 0x04, 0x00, 0x00, 0x00, 0x08, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x71)); +} + +TEST(MitsubishiCN105Tests, ApplySettingsTemperatureEncodedB) { + auto ctx = TestContext{}; + + ctx.sut.use_temperature_encoding_b_ = true; + ctx.sut.set_target_temperature(26.0f); + ctx.sut.apply_settings(); + + EXPECT_THAT(ctx.uart.tx, ::testing::ElementsAre(0xFC, 0x41, 0x01, 0x30, 0x10, 0x01, 0x04, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xB4, 0x00, 0xC5)); +} + +TEST(MitsubishiCN105Tests, ApplyModeCool) { + auto ctx = TestContext{}; + + ctx.sut.set_mode(MitsubishiCN105::Mode::COOL); + ctx.sut.apply_settings(); + + EXPECT_THAT(ctx.uart.tx, ::testing::ElementsAre(0xFC, 0x41, 0x01, 0x30, 0x10, 0x01, 0x02, 0x00, 0x00, 0x03, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x78)); +} + +TEST(MitsubishiCN105Tests, ApplyFanModeSpeed1) { + auto ctx = TestContext{}; + + ctx.sut.set_fan_mode(MitsubishiCN105::FanMode::SPEED_1); + ctx.sut.apply_settings(); + + EXPECT_THAT(ctx.uart.tx, ::testing::ElementsAre(0xFC, 0x41, 0x01, 0x30, 0x10, 0x01, 0x08, 0x00, 0x00, 0x00, 0x00, + 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x73)); +} + +TEST(MitsubishiCN105Tests, WriteInterruptsWaitingForNextStatusUpdate) { + auto ctx = TestContext{}; + + // Waiting for next scheduled status update + ctx.sut.state_ = TestableMitsubishiCN105::State::STATUS_UPDATED; + ctx.sut.set_state(TestableMitsubishiCN105::State::SCHEDULE_NEXT_STATUS_UPDATE); + EXPECT_EQ(ctx.sut.state_, TestableMitsubishiCN105::State::WAITING_FOR_SCHEDULED_STATUS_UPDATE); + + // Nothing to do in update (rx empty, no timeout) + ASSERT_FALSE(ctx.sut.update()); + EXPECT_TRUE(ctx.uart.tx.empty()); + + // Write new values + ctx.sut.use_temperature_encoding_b_ = true; + ctx.sut.set_power(false); + ctx.sut.set_target_temperature(25.0f); + ctx.sut.set_mode(MitsubishiCN105::Mode::HEAT); + ctx.sut.set_fan_mode(MitsubishiCN105::FanMode::AUTO); + + // Waiting for next status update must be interrupted and new values send to AC + ASSERT_FALSE(ctx.sut.update()); + EXPECT_EQ(ctx.sut.state_, TestableMitsubishiCN105::State::APPLYING_SETTINGS); + EXPECT_THAT(ctx.uart.tx, ::testing::ElementsAre(0xFC, 0x41, 0x01, 0x30, 0x10, 0x01, 0x0F, 0x00, 0x00, 0x01, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xB2, 0x00, 0xBB)); + + // Write ACK response + ctx.uart.push_rx({0xFC, 0x61, 0x01, 0x30, 0x10, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x5E}); + ASSERT_FALSE(ctx.sut.update()); + EXPECT_EQ(ctx.sut.state_, TestableMitsubishiCN105::State::WAITING_FOR_SCHEDULED_STATUS_UPDATE); +} + +} // namespace esphome::mitsubishi_cn105::testing diff --git a/tests/components/mitsubishi_cn105/common.cpp b/tests/components/mitsubishi_cn105/common.cpp new file mode 100644 index 0000000000..50993c5c2c --- /dev/null +++ b/tests/components/mitsubishi_cn105/common.cpp @@ -0,0 +1,7 @@ +#include "common.h" + +namespace esphome::mitsubishi_cn105 { + +uint32_t get_loop_time_ms() { return testing::TestableMitsubishiCN105::test_loop_time_ms; } + +} // namespace esphome::mitsubishi_cn105 diff --git a/tests/components/mitsubishi_cn105/common.h b/tests/components/mitsubishi_cn105/common.h new file mode 100644 index 0000000000..0862d64fa7 --- /dev/null +++ b/tests/components/mitsubishi_cn105/common.h @@ -0,0 +1,58 @@ +#pragma once + +#include +#include +#include +#include +#include +#include +#include "esphome/components/uart/uart_component.h" +#include "esphome/components/mitsubishi_cn105/mitsubishi_cn105.h" + +namespace esphome::mitsubishi_cn105::testing { + +class MockUARTComponent : public uart::UARTComponent { + public: + std::vector tx; + std::vector rx; + + void push_rx(std::initializer_list data) { this->rx.insert(this->rx.end(), data.begin(), data.end()); } + + // UARTComponent + void write_array(const uint8_t *data, size_t len) override { this->tx.insert(this->tx.end(), data, data + len); } + + bool read_array(uint8_t *data, size_t len) override { + if (this->rx.size() < len) { + return false; + } + + std::copy(this->rx.begin(), this->rx.begin() + len, data); + this->rx.erase(this->rx.begin(), this->rx.begin() + len); + return true; + } + + size_t available() override { return this->rx.size(); } + + MOCK_METHOD(bool, peek_byte, (uint8_t * data), (override)); + MOCK_METHOD(uart::UARTFlushResult, flush, (), (override)); + MOCK_METHOD(void, check_logger_conflict, (), (override)); +}; + +class TestableMitsubishiCN105 : public MitsubishiCN105 { + public: + using MitsubishiCN105::MitsubishiCN105; + using MitsubishiCN105::State; + using MitsubishiCN105::state_; + using MitsubishiCN105::write_timeout_start_ms_; + using MitsubishiCN105::status_update_start_ms_; + using MitsubishiCN105::use_temperature_encoding_b_; + + void set_state(State s) { this->set_state_(s); } + void apply_settings() { this->apply_settings_(); } + + static inline uint32_t test_loop_time_ms = 0; + + void set_current_time(uint32_t ms) { test_loop_time_ms = ms; } +}; + +} // namespace esphome::mitsubishi_cn105::testing diff --git a/tests/components/mitsubishi_cn105/common.yaml b/tests/components/mitsubishi_cn105/common.yaml new file mode 100644 index 0000000000..e885ceef81 --- /dev/null +++ b/tests/components/mitsubishi_cn105/common.yaml @@ -0,0 +1,4 @@ +climate: + - platform: mitsubishi_cn105 + name: "AC Test" + uart_id: uart_bus diff --git a/tests/components/mitsubishi_cn105/test.esp32-idf.yaml b/tests/components/mitsubishi_cn105/test.esp32-idf.yaml new file mode 100644 index 0000000000..5ce1861902 --- /dev/null +++ b/tests/components/mitsubishi_cn105/test.esp32-idf.yaml @@ -0,0 +1,4 @@ +packages: + uart_9600_even: !include ../../test_build_components/common/uart_9600_even/esp32-idf.yaml + +<<: !include common.yaml diff --git a/tests/components/mitsubishi_cn105/test.esp8266-ard.yaml b/tests/components/mitsubishi_cn105/test.esp8266-ard.yaml new file mode 100644 index 0000000000..a3f8cf43d4 --- /dev/null +++ b/tests/components/mitsubishi_cn105/test.esp8266-ard.yaml @@ -0,0 +1,4 @@ +packages: + uart_9600_even: !include ../../test_build_components/common/uart_9600_even/esp8266-ard.yaml + +<<: !include common.yaml diff --git a/tests/components/mitsubishi_cn105/test.rp2040-ard.yaml b/tests/components/mitsubishi_cn105/test.rp2040-ard.yaml new file mode 100644 index 0000000000..7c1f4f41e2 --- /dev/null +++ b/tests/components/mitsubishi_cn105/test.rp2040-ard.yaml @@ -0,0 +1,4 @@ +packages: + uart_9600_even: !include ../../test_build_components/common/uart_9600_even/rp2040-ard.yaml + +<<: !include common.yaml diff --git a/tests/components/pca9554/common.yaml b/tests/components/pca9554/common.yaml index 9e5e7f3342..82a88b90aa 100644 --- a/tests/components/pca9554/common.yaml +++ b/tests/components/pca9554/common.yaml @@ -3,6 +3,11 @@ pca9554: i2c_id: i2c_bus pin_count: 8 address: 0x3F + - id: pca9554_hub_int + i2c_id: i2c_bus + pin_count: 8 + address: 0x3E + interrupt_pin: ${interrupt_pin} binary_sensor: - platform: gpio diff --git a/tests/components/pca9554/test.esp32-idf.yaml b/tests/components/pca9554/test.esp32-idf.yaml index b47e39c389..8c3b341dce 100644 --- a/tests/components/pca9554/test.esp32-idf.yaml +++ b/tests/components/pca9554/test.esp32-idf.yaml @@ -1,3 +1,6 @@ +substitutions: + interrupt_pin: GPIO15 + packages: i2c: !include ../../test_build_components/common/i2c/esp32-idf.yaml diff --git a/tests/components/pca9554/test.esp8266-ard.yaml b/tests/components/pca9554/test.esp8266-ard.yaml index 4a98b9388a..69b243bfd8 100644 --- a/tests/components/pca9554/test.esp8266-ard.yaml +++ b/tests/components/pca9554/test.esp8266-ard.yaml @@ -1,3 +1,6 @@ +substitutions: + interrupt_pin: GPIO15 + packages: i2c: !include ../../test_build_components/common/i2c/esp8266-ard.yaml diff --git a/tests/components/pca9554/test.rp2040-ard.yaml b/tests/components/pca9554/test.rp2040-ard.yaml index 319a7c71a6..b8ad1e4792 100644 --- a/tests/components/pca9554/test.rp2040-ard.yaml +++ b/tests/components/pca9554/test.rp2040-ard.yaml @@ -1,3 +1,6 @@ +substitutions: + interrupt_pin: GPIO2 + packages: i2c: !include ../../test_build_components/common/i2c/rp2040-ard.yaml diff --git a/tests/components/pcf8574/common.yaml b/tests/components/pcf8574/common.yaml index 09fa33164e..8a26b93015 100644 --- a/tests/components/pcf8574/common.yaml +++ b/tests/components/pcf8574/common.yaml @@ -3,6 +3,11 @@ pcf8574: i2c_id: i2c_bus address: 0x21 pcf8575: false + - id: pcf8574_hub_int + i2c_id: i2c_bus + address: 0x22 + pcf8575: false + interrupt_pin: ${interrupt_pin} binary_sensor: - platform: gpio diff --git a/tests/components/pcf8574/test.esp32-idf.yaml b/tests/components/pcf8574/test.esp32-idf.yaml index b47e39c389..8c3b341dce 100644 --- a/tests/components/pcf8574/test.esp32-idf.yaml +++ b/tests/components/pcf8574/test.esp32-idf.yaml @@ -1,3 +1,6 @@ +substitutions: + interrupt_pin: GPIO15 + packages: i2c: !include ../../test_build_components/common/i2c/esp32-idf.yaml diff --git a/tests/components/pcf8574/test.esp8266-ard.yaml b/tests/components/pcf8574/test.esp8266-ard.yaml index 4a98b9388a..69b243bfd8 100644 --- a/tests/components/pcf8574/test.esp8266-ard.yaml +++ b/tests/components/pcf8574/test.esp8266-ard.yaml @@ -1,3 +1,6 @@ +substitutions: + interrupt_pin: GPIO15 + packages: i2c: !include ../../test_build_components/common/i2c/esp8266-ard.yaml diff --git a/tests/components/pcf8574/test.rp2040-ard.yaml b/tests/components/pcf8574/test.rp2040-ard.yaml index 319a7c71a6..b8ad1e4792 100644 --- a/tests/components/pcf8574/test.rp2040-ard.yaml +++ b/tests/components/pcf8574/test.rp2040-ard.yaml @@ -1,3 +1,6 @@ +substitutions: + interrupt_pin: GPIO2 + packages: i2c: !include ../../test_build_components/common/i2c/rp2040-ard.yaml diff --git a/tests/components/pi4ioe5v6408/common.yaml b/tests/components/pi4ioe5v6408/common.yaml index 2344622081..77a77fa3e4 100644 --- a/tests/components/pi4ioe5v6408/common.yaml +++ b/tests/components/pi4ioe5v6408/common.yaml @@ -1,7 +1,11 @@ pi4ioe5v6408: - i2c_id: i2c_bus - id: pi4ioe1 - address: 0x44 + - i2c_id: i2c_bus + id: pi4ioe1 + address: 0x44 + - i2c_id: i2c_bus + id: pi4ioe1_int + address: 0x45 + interrupt_pin: ${interrupt_pin} switch: - platform: gpio @@ -16,3 +20,8 @@ binary_sensor: pin: pi4ioe5v6408: pi4ioe1 number: 1 + - platform: gpio + id: sensor1_int + pin: + pi4ioe5v6408: pi4ioe1_int + number: 1 diff --git a/tests/components/pi4ioe5v6408/test.esp32-idf.yaml b/tests/components/pi4ioe5v6408/test.esp32-idf.yaml index 9a4779d822..a6eb3c1cb1 100644 --- a/tests/components/pi4ioe5v6408/test.esp32-idf.yaml +++ b/tests/components/pi4ioe5v6408/test.esp32-idf.yaml @@ -1,6 +1,7 @@ substitutions: i2c_sda: GPIO21 i2c_scl: GPIO22 + interrupt_pin: GPIO15 packages: i2c: !include ../../test_build_components/common/i2c/esp32-idf.yaml diff --git a/tests/components/pi4ioe5v6408/test.rp2040-ard.yaml b/tests/components/pi4ioe5v6408/test.rp2040-ard.yaml index 3429a2952a..cd6fef3042 100644 --- a/tests/components/pi4ioe5v6408/test.rp2040-ard.yaml +++ b/tests/components/pi4ioe5v6408/test.rp2040-ard.yaml @@ -1,6 +1,7 @@ substitutions: i2c_sda: GPIO4 i2c_scl: GPIO5 + interrupt_pin: GPIO2 packages: i2c: !include ../../test_build_components/common/i2c/rp2040-ard.yaml diff --git a/tests/components/socket/test_wake_loop_threadsafe.py b/tests/components/socket/test_wake_loop_threadsafe.py deleted file mode 100644 index 0434b3e1b5..0000000000 --- a/tests/components/socket/test_wake_loop_threadsafe.py +++ /dev/null @@ -1,127 +0,0 @@ -import pytest - -from esphome.components import socket -from esphome.const import ( - KEY_CORE, - KEY_TARGET_PLATFORM, - PLATFORM_BK72XX, - PLATFORM_ESP32, - PLATFORM_ESP8266, - PLATFORM_LN882X, - PLATFORM_RTL87XX, -) -from esphome.core import CORE - - -def _setup_platform(platform=PLATFORM_ESP8266) -> None: - """Set up CORE.data with a platform for testing.""" - CORE.data[KEY_CORE] = {KEY_TARGET_PLATFORM: platform} - - -def test_require_wake_loop_threadsafe__first_call() -> None: - """Test that first call sets up define and consumes socket.""" - _setup_platform() - CORE.config = {"wifi": True} - socket.require_wake_loop_threadsafe() - - # Verify CORE.data was updated - assert CORE.data[socket.KEY_WAKE_LOOP_THREADSAFE_REQUIRED] is True - - # Verify the define was added - assert any(d.name == "USE_WAKE_LOOP_THREADSAFE" for d in CORE.defines) - - -def test_require_wake_loop_threadsafe__idempotent() -> None: - """Test that subsequent calls are idempotent.""" - # Set up initial state as if already called - CORE.data[socket.KEY_WAKE_LOOP_THREADSAFE_REQUIRED] = True - CORE.config = {"ethernet": True} - - # Call again - should not raise or fail - socket.require_wake_loop_threadsafe() - - # Verify state is still True - assert CORE.data[socket.KEY_WAKE_LOOP_THREADSAFE_REQUIRED] is True - - # Define should not be added since flag was already True - assert not any(d.name == "USE_WAKE_LOOP_THREADSAFE" for d in CORE.defines) - - -def test_require_wake_loop_threadsafe__multiple_calls() -> None: - """Test that multiple calls only set up once.""" - _setup_platform() - # Call three times - CORE.config = {"openthread": True} - socket.require_wake_loop_threadsafe() - socket.require_wake_loop_threadsafe() - socket.require_wake_loop_threadsafe() - - # Verify CORE.data was set - assert CORE.data[socket.KEY_WAKE_LOOP_THREADSAFE_REQUIRED] is True - - # Verify the define was added (only once, but we can just check it exists) - assert any(d.name == "USE_WAKE_LOOP_THREADSAFE" for d in CORE.defines) - - -def test_require_wake_loop_threadsafe__no_networking() -> None: - """Test that wake loop is NOT configured when no networking is configured.""" - # Set up config without any networking components - CORE.config = {"esphome": {"name": "test"}, "logger": {}} - - # Call require_wake_loop_threadsafe - socket.require_wake_loop_threadsafe() - - # Verify CORE.data flag was NOT set (since has_networking returns False) - assert socket.KEY_WAKE_LOOP_THREADSAFE_REQUIRED not in CORE.data - - # Verify the define was NOT added - assert not any(d.name == "USE_WAKE_LOOP_THREADSAFE" for d in CORE.defines) - - -def test_require_wake_loop_threadsafe__no_networking_does_not_consume_socket() -> None: - """Test that no socket is consumed when no networking is configured.""" - # Set up config without any networking components - CORE.config = {"logger": {}} - - # Track initial socket consumer state - initial_udp = CORE.data.get(socket.KEY_SOCKET_CONSUMERS_UDP, {}) - - # Call require_wake_loop_threadsafe - socket.require_wake_loop_threadsafe() - - # Verify no socket was consumed - udp_consumers = CORE.data.get(socket.KEY_SOCKET_CONSUMERS_UDP, {}) - assert "socket.wake_loop_threadsafe" not in udp_consumers - assert udp_consumers == initial_udp - - -@pytest.mark.parametrize( - "platform", - [PLATFORM_ESP32, PLATFORM_BK72XX, PLATFORM_RTL87XX, PLATFORM_LN882X], -) -def test_require_wake_loop_threadsafe__fast_select_no_udp_socket( - platform: str, -) -> None: - """Test that fast select platforms use task notifications instead of UDP socket.""" - _setup_platform(platform) - CORE.config = {"wifi": True} - socket.require_wake_loop_threadsafe() - - # Verify the define was added - assert CORE.data[socket.KEY_WAKE_LOOP_THREADSAFE_REQUIRED] is True - assert any(d.name == "USE_WAKE_LOOP_THREADSAFE" for d in CORE.defines) - - # Verify no UDP socket was consumed (fast select platforms use FreeRTOS task notifications) - udp_consumers = CORE.data.get(socket.KEY_SOCKET_CONSUMERS_UDP, {}) - assert "socket.wake_loop_threadsafe" not in udp_consumers - - -def test_require_wake_loop_threadsafe__non_fast_select_consumes_udp_socket() -> None: - """Test that platforms without fast select consume a UDP socket for wake notifications.""" - _setup_platform(PLATFORM_ESP8266) - CORE.config = {"wifi": True} - socket.require_wake_loop_threadsafe() - - # Verify UDP socket was consumed - udp_consumers = CORE.data.get(socket.KEY_SOCKET_CONSUMERS_UDP, {}) - assert udp_consumers.get("socket.wake_loop_threadsafe") == 1 diff --git a/tests/components/time/common.yaml b/tests/components/time/common.yaml index 465be045db..cd258c7aa6 100644 --- a/tests/components/time/common.yaml +++ b/tests/components/time/common.yaml @@ -6,5 +6,9 @@ api: time: - platform: homeassistant + on_time: + - seconds: "0,10,20,30,40,50" + then: + - logger.log: "CronTrigger fired (every 10 seconds)" - platform: sntp id: sntp_time diff --git a/tests/components/time/posix_tz_parser.cpp b/tests/components/time/posix_tz_parser.cpp index b7cf2a4afa..440eea608d 100644 --- a/tests/components/time/posix_tz_parser.cpp +++ b/tests/components/time/posix_tz_parser.cpp @@ -758,6 +758,115 @@ TEST(PosixTzParser, EpochToLocalDstTransition) { EXPECT_EQ(local.tm_isdst, 1); } +// ============================================================================ +// Leap year edge cases for closed-form year arithmetic +// ============================================================================ + +TEST(PosixTzParser, EpochToLocalLeapYear2000) { + // 2000 is a leap year (divisible by 400) + ParsedTimezone tz; + ASSERT_TRUE(parse_posix_tz("UTC0", tz)); + + // Feb 29, 2000 12:00:00 UTC + time_t epoch = make_utc(2000, 2, 29, 12); + struct tm local; + ASSERT_TRUE(epoch_to_local_tm(epoch, tz, &local)); + EXPECT_EQ(local.tm_year, 100); // 2000 + EXPECT_EQ(local.tm_mon, 1); // February + EXPECT_EQ(local.tm_mday, 29); + EXPECT_EQ(local.tm_hour, 12); +} + +TEST(PosixTzParser, EpochToLocalNonLeapYear2100) { + // 2100 is NOT a leap year (divisible by 100 but not 400) + ParsedTimezone tz; + ASSERT_TRUE(parse_posix_tz("UTC0", tz)); + + // Mar 1, 2100 00:00:00 UTC — the day after what would be Feb 29 + time_t epoch = make_utc(2100, 3, 1); + struct tm local; + ASSERT_TRUE(epoch_to_local_tm(epoch, tz, &local)); + EXPECT_EQ(local.tm_year, 200); // 2100 + EXPECT_EQ(local.tm_mon, 2); // March + EXPECT_EQ(local.tm_mday, 1); + + // Feb 28, 2100 23:59:59 UTC — last second of February (no Feb 29) + epoch = make_utc(2100, 2, 28, 23, 59, 59); + ASSERT_TRUE(epoch_to_local_tm(epoch, tz, &local)); + EXPECT_EQ(local.tm_year, 200); + EXPECT_EQ(local.tm_mon, 1); // February + EXPECT_EQ(local.tm_mday, 28); +} + +TEST(PosixTzParser, EpochToLocalLeapYear2400) { + // 2400 is a leap year (divisible by 400) + ParsedTimezone tz; + ASSERT_TRUE(parse_posix_tz("UTC0", tz)); + + time_t epoch = make_utc(2400, 2, 29, 6); + struct tm local; + ASSERT_TRUE(epoch_to_local_tm(epoch, tz, &local)); + EXPECT_EQ(local.tm_year, 500); // 2400 + EXPECT_EQ(local.tm_mon, 1); // February + EXPECT_EQ(local.tm_mday, 29); + EXPECT_EQ(local.tm_hour, 6); +} + +TEST(PosixTzParser, EpochToLocalNewYearBoundaries) { + // Test year boundary — last second of 2099 and first second of 2100 + ParsedTimezone tz; + ASSERT_TRUE(parse_posix_tz("UTC0", tz)); + struct tm local; + + // Dec 31, 2099 23:59:59 UTC + time_t epoch = make_utc(2099, 12, 31, 23, 59, 59); + ASSERT_TRUE(epoch_to_local_tm(epoch, tz, &local)); + EXPECT_EQ(local.tm_year, 199); // 2099 + EXPECT_EQ(local.tm_mon, 11); // December + EXPECT_EQ(local.tm_mday, 31); + + // Jan 1, 2100 00:00:00 UTC + epoch = make_utc(2100, 1, 1); + ASSERT_TRUE(epoch_to_local_tm(epoch, tz, &local)); + EXPECT_EQ(local.tm_year, 200); // 2100 + EXPECT_EQ(local.tm_mon, 0); // January + EXPECT_EQ(local.tm_mday, 1); +} + +TEST(PosixTzParser, EpochToLocalDstAcrossCenturyBoundary) { + // DST transition in year 2100 (non-leap) with US Eastern rules + ParsedTimezone tz; + ASSERT_TRUE(parse_posix_tz("EST5EDT,M3.2.0/2,M11.1.0/2", tz)); + + // July 4, 2100 16:00 UTC = 12:00 EDT + time_t epoch = make_utc(2100, 7, 4, 16); + struct tm local; + ASSERT_TRUE(epoch_to_local_tm(epoch, tz, &local)); + EXPECT_EQ(local.tm_hour, 12); + EXPECT_EQ(local.tm_isdst, 1); + + // Jan 15, 2100 10:00 UTC = 05:00 EST + epoch = make_utc(2100, 1, 15, 10); + ASSERT_TRUE(epoch_to_local_tm(epoch, tz, &local)); + EXPECT_EQ(local.tm_hour, 5); + EXPECT_EQ(local.tm_isdst, 0); +} + +TEST(PosixTzParser, EpochToLocalFarFutureYear5000) { + // Year 5000 — days/365 estimate overshoots by ~2 years due to leap days, + // requiring multiple correction steps in days_to_year. + ParsedTimezone tz; + ASSERT_TRUE(parse_posix_tz("UTC0", tz)); + + time_t epoch = make_utc(5000, 6, 15, 12); + struct tm local; + ASSERT_TRUE(epoch_to_local_tm(epoch, tz, &local)); + EXPECT_EQ(local.tm_year, 3100); // 5000 + EXPECT_EQ(local.tm_mon, 5); // June + EXPECT_EQ(local.tm_mday, 15); + EXPECT_EQ(local.tm_hour, 12); +} + // ============================================================================ // Verification against libc // ============================================================================ diff --git a/tests/components/tm1637/common.yaml b/tests/components/tm1637/common.yaml index 8d01e29877..b6debc055d 100644 --- a/tests/components/tm1637/common.yaml +++ b/tests/components/tm1637/common.yaml @@ -5,3 +5,5 @@ display: intensity: 3 lambda: |- it.print("1234"); + static const uint8_t buf[] = {0x3f, 0x06, 0x5b, 0x4f | 0x80}; + it.set_buffer(buf, sizeof(buf)); diff --git a/tests/integration/conftest.py b/tests/integration/conftest.py index b652b4174c..7c85bf753c 100644 --- a/tests/integration/conftest.py +++ b/tests/integration/conftest.py @@ -198,6 +198,13 @@ async def yaml_config(request: pytest.FixtureRequest, unused_tcp_port: int) -> s ' - "-g" # Add debug symbols', ) + # Replace external component path placeholder if present + if "EXTERNAL_COMPONENT_PATH" in content: + external_components_path = str( + Path(__file__).parent / "fixtures" / "external_components" + ) + content = content.replace("EXTERNAL_COMPONENT_PATH", external_components_path) + return content diff --git a/tests/integration/fixtures/multi_click_trigger.yaml b/tests/integration/fixtures/multi_click_trigger.yaml new file mode 100644 index 0000000000..3bd53d594c --- /dev/null +++ b/tests/integration/fixtures/multi_click_trigger.yaml @@ -0,0 +1,105 @@ +esphome: + name: test-multi-click + +host: +api: + batch_delay: 0ms + services: + - service: run_all_tests + then: + # Prime the binary sensor with an initial OFF state. + # trigger_on_initial_state defaults to false, so the first + # state change from unknown won't fire callbacks. + - binary_sensor.template.publish: + id: test_button + state: false + - delay: 50ms + + # Test 1: Single click (ON < 50ms, OFF >= 30ms) + - binary_sensor.template.publish: + id: test_button + state: true + - delay: 20ms + - binary_sensor.template.publish: + id: test_button + state: false + # Wait for single click trigger (30ms) + cooldown (100ms) + margin + - delay: 200ms + + # Test 2: Double click (ON < 50ms, OFF < 25ms, ON < 50ms, OFF >= 25ms) + - binary_sensor.template.publish: + id: test_button + state: true + - delay: 20ms + - binary_sensor.template.publish: + id: test_button + state: false + - delay: 15ms + - binary_sensor.template.publish: + id: test_button + state: true + - delay: 20ms + - binary_sensor.template.publish: + id: test_button + state: false + # Wait for double click trigger (25ms) + cooldown (100ms) + margin + - delay: 200ms + + # Test 3: Long press (ON >= 80ms) + - binary_sensor.template.publish: + id: test_button + state: true + - delay: 100ms + - binary_sensor.template.publish: + id: test_button + state: false + +logger: + level: VERBOSE + +globals: + - id: single_click_count + type: int + initial_value: "0" + - id: double_click_count + type: int + initial_value: "0" + - id: long_press_count + type: int + initial_value: "0" + +binary_sensor: + - platform: template + name: "Test Button" + id: test_button + on_multi_click: + # Single press + - timing: + - ON for at most 50ms + - OFF for at least 30ms + invalid_cooldown: 100ms + then: + - lambda: |- + id(single_click_count) += 1; + ESP_LOGI("multi_click_test", "SINGLE_CLICK count=%d", id(single_click_count)); + + # Double press + - timing: + - ON for at most 50ms + - OFF for at most 25ms + - ON for at most 50ms + - OFF for at least 25ms + invalid_cooldown: 100ms + then: + - lambda: |- + id(double_click_count) += 1; + ESP_LOGI("multi_click_test", "DOUBLE_CLICK count=%d", id(double_click_count)); + + # Long press + - timing: + - ON for at least 80ms + invalid_cooldown: 100ms + then: + - lambda: |- + id(long_press_count) += 1; + ESP_LOGI("multi_click_test", "LONG_PRESS count=%d", id(long_press_count)); diff --git a/tests/integration/fixtures/uart_mock_modbus.yaml b/tests/integration/fixtures/uart_mock_modbus.yaml index 3ff7ab01bd..da36da4de1 100644 --- a/tests/integration/fixtures/uart_mock_modbus.yaml +++ b/tests/integration/fixtures/uart_mock_modbus.yaml @@ -22,6 +22,8 @@ uart_mock: baud_rate: 9600 rx_full_threshold: 120 rx_timeout: 2 + # auto_start must be false to avoid races: the test presses the + # "Start Scenario" button only after subscribing to states. auto_start: false debug: responses: @@ -46,7 +48,7 @@ modbus: modbus_controller: - address: 1 id: modbus_controller_ok - max_cmd_retries: 0 + max_cmd_retries: 2 update_interval: 1s - address: 2 id: modbus_controller_slow @@ -89,4 +91,4 @@ button: name: "Start Scenario" id: start_scenario_btn on_press: - - lambda: 'id(virtual_uart_dev).start_scenario();' + - lambda: "id(virtual_uart_dev).start_scenario();" diff --git a/tests/integration/fixtures/uart_mock_modbus_no_threshold.yaml b/tests/integration/fixtures/uart_mock_modbus_no_threshold.yaml index e3e8c8c8da..9bc4dc50e9 100644 --- a/tests/integration/fixtures/uart_mock_modbus_no_threshold.yaml +++ b/tests/integration/fixtures/uart_mock_modbus_no_threshold.yaml @@ -22,6 +22,8 @@ uart: uart_mock: - id: virtual_uart_dev baud_rate: 9600 + # auto_start must be false to avoid races: the test presses the + # "Start Scenario" button only after subscribing to states. auto_start: false debug: on_tx: @@ -40,7 +42,8 @@ uart_mock: 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00}; - uart_mock.inject_rx: # Second USB packet: rest of response (staged with 40ms latency) delay: 40ms - data: !lambda return{0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, + data: + !lambda return{0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x42,0x6F,0xCC,0xCD,0x43,0x7C,0xB8,0x10,0x3D,0x38,0x51,0xEC, 0x43,0x81,0x1B,0xE7,0x3B,0x03,0x12,0x6F,0x50,0x1B}; @@ -61,4 +64,4 @@ button: name: "Start Scenario" id: start_scenario_btn on_press: - - lambda: 'id(virtual_uart_dev).start_scenario();' + - lambda: "id(virtual_uart_dev).start_scenario();" diff --git a/tests/integration/fixtures/uart_mock_modbus_server.yaml b/tests/integration/fixtures/uart_mock_modbus_server.yaml new file mode 100644 index 0000000000..b657a6fd21 --- /dev/null +++ b/tests/integration/fixtures/uart_mock_modbus_server.yaml @@ -0,0 +1,124 @@ +esphome: + name: uart-mock-modbus-server-test + +host: +api: +logger: + level: VERBOSE + +external_components: + - source: + type: local + path: EXTERNAL_COMPONENT_PATH + +# Dummy uart entry to satisfy modbus's DEPENDENCIES = ["uart"] +# The actual UART bus used is the uart_mock component below +uart: + baud_rate: 115200 + port: /dev/null + +uart_mock: + - id: virtual_uart_dev + baud_rate: 9600 + rx_full_threshold: 120 + rx_timeout: 2 + auto_start: false + debug: + injections: + - delay: 100ms + inject_rx: [0x01, 0x03, 0x00, 0x03, 0x00, 0x01, 0x74, 0x0A] # Read holding register 3 on device 1 (basic_read) + - delay: 100ms + # Read holding register 7 on device 2 + # Reply from device 2 + # Read holding register 5 on device 1 (read_after_peer_response) + inject_rx: + [ + 0x02, + 0x03, + 0x00, + 0x07, + 0x00, + 0x01, + 0x35, + 0xF8, + 0x02, + 0x03, + 0x02, + 0x00, + 0xF0, + 0xFC, + 0x00, + 0x01, + 0x03, + 0x00, + 0x05, + 0x00, + 0x01, + 0x94, + 0x0B, + ] + - delay: 100ms + inject_rx: [0x02, 0x03, 0x00, 0x07, 0x00, 0x01, 0x35, 0xF8] # Read holding register 7 on device 2, with no response + - delay: 100ms + # Read holding register 7 on device 2, with no response + # Read holding register A on device 1 (read_after_peer_timeout) + inject_rx: + [ + 0x02, + 0x03, + 0x00, + 0x07, + 0x00, + 0x01, + 0x35, + 0xF8, + 0x01, + 0x03, + 0x00, + 0x0A, + 0x00, + 0x01, + 0xA4, + 0x08, + ] + +modbus: + uart_id: virtual_uart_dev + role: server + +modbus_controller: + - address: 1 + server_registers: + - address: 0x03 + value_type: U_WORD + read_lambda: |- + id(basic_read).publish_state(1); + return 1; + - address: 0x05 + value_type: U_WORD + read_lambda: |- + id(read_after_peer_response).publish_state(1); + return 1; + - address: 0x0A + value_type: U_WORD + read_lambda: |- + id(read_after_peer_timeout).publish_state(1); + return 1; + +sensor: + - platform: template + name: "basic_read" + id: basic_read + - platform: template + name: "read_after_peer_response" + id: read_after_peer_response + - platform: template + name: "read_after_peer_timeout" + id: read_after_peer_timeout + +button: + - platform: template + name: "Start Scenario" + id: start_scenario_btn + on_press: + - lambda: "id(virtual_uart_dev).start_scenario();" diff --git a/tests/integration/fixtures/uart_mock_modbus_server_controller.yaml b/tests/integration/fixtures/uart_mock_modbus_server_controller.yaml new file mode 100644 index 0000000000..f0f2c56a36 --- /dev/null +++ b/tests/integration/fixtures/uart_mock_modbus_server_controller.yaml @@ -0,0 +1,180 @@ +esphome: + name: uart-mock-modbus-server-contro + +host: +api: +logger: + level: VERBOSE + +external_components: + - source: + type: local + path: EXTERNAL_COMPONENT_PATH + +# Dummy uart entry to satisfy modbus's DEPENDENCIES = ["uart"] +# The actual UART bus used is the uart_mock component below +uart: + baud_rate: 115200 + port: /dev/null + +uart_mock: + - id: virtual_uart_server + baud_rate: 9600 + # auto_start must be true for loopback fixtures: the modbus controller + # polls on its update_interval immediately at boot, so the uart_mock + # forwarding must already be active or early requests are lost and + # generate modbus warnings. + auto_start: true + debug: + on_tx: + - then: + - uart_mock.inject_rx: + id: virtual_uart_controller + data: !lambda return data; + - id: virtual_uart_controller + baud_rate: 9600 + auto_start: true # See comment on virtual_uart_server above + debug: + on_tx: + - then: + - uart_mock.inject_rx: + id: virtual_uart_server + data: !lambda return data; + +modbus: + - uart_id: virtual_uart_server + id: virtual_modbus_server + role: server + - uart_id: virtual_uart_controller + id: virtual_modbus_controller + role: client + turnaround_time: 10ms + +modbus_controller: + - address: 1 + modbus_id: virtual_modbus_controller + update_interval: 1s + id: modbus_controller_1 + + - address: 1 + modbus_id: virtual_modbus_server + id: modbus_server_1 + server_registers: + - address: 0x01 + value_type: U_WORD + read_lambda: return 99; + - address: 0x03 + value_type: S_WORD + read_lambda: return -99; + - address: 0x05 + value_type: U_DWORD + read_lambda: return 16909060; + - address: 0x08 + value_type: S_DWORD + read_lambda: return -16909060; + - address: 0x0B + value_type: U_DWORD_R + read_lambda: return 67305985; + - address: 0x0E + value_type: S_DWORD_R + read_lambda: return -67305985; + - address: 0x11 + value_type: U_QWORD + read_lambda: return 72623859790382856; + - address: 0x16 + value_type: S_QWORD + read_lambda: return -72623859790382856; + - address: 0x1B + value_type: U_QWORD_R + read_lambda: return 578437695752307201; + - address: 0x20 + value_type: S_QWORD_R + read_lambda: return -578437695752307201; + - address: 0x25 + value_type: FP32 + read_lambda: return 3.14; + - address: 0x28 + value_type: FP32_R + read_lambda: return 3.14; + +sensor: + - platform: modbus_controller + modbus_controller_id: modbus_controller_1 + name: "reg_u_word" + address: 0x01 + register_type: holding + value_type: U_WORD + - platform: modbus_controller + modbus_controller_id: modbus_controller_1 + name: "reg_s_word" + address: 0x03 + register_type: holding + value_type: S_WORD + - platform: modbus_controller + modbus_controller_id: modbus_controller_1 + name: "reg_u_dword" + address: 0x05 + register_type: holding + value_type: U_DWORD + - platform: modbus_controller + modbus_controller_id: modbus_controller_1 + name: "reg_s_dword" + address: 0x08 + register_type: holding + value_type: S_DWORD + - platform: modbus_controller + modbus_controller_id: modbus_controller_1 + name: "reg_u_dword_r" + address: 0x0B + register_type: holding + value_type: U_DWORD_R + - platform: modbus_controller + modbus_controller_id: modbus_controller_1 + name: "reg_s_dword_r" + address: 0x0E + register_type: holding + value_type: S_DWORD_R + - platform: modbus_controller + modbus_controller_id: modbus_controller_1 + name: "reg_u_qword" + address: 0x11 + register_type: holding + value_type: U_QWORD + - platform: modbus_controller + modbus_controller_id: modbus_controller_1 + name: "reg_s_qword" + address: 0x16 + register_type: holding + value_type: S_QWORD + - platform: modbus_controller + modbus_controller_id: modbus_controller_1 + name: "reg_u_qword_r" + address: 0x1B + register_type: holding + value_type: U_QWORD_R + - platform: modbus_controller + modbus_controller_id: modbus_controller_1 + name: "reg_s_qword_r" + address: 0x20 + register_type: holding + value_type: S_QWORD_R + - platform: modbus_controller + modbus_controller_id: modbus_controller_1 + name: "reg_fp32" + address: 0x25 + register_type: holding + value_type: FP32 + - platform: modbus_controller + modbus_controller_id: modbus_controller_1 + name: "reg_fp32_r" + address: 0x28 + register_type: holding + value_type: FP32_R + +button: + - platform: template + name: "Start Scenario" + id: start_scenario_btn + on_press: + - lambda: "id(virtual_uart_server).start_scenario();" + - lambda: "id(virtual_uart_controller).start_scenario();" diff --git a/tests/integration/fixtures/uart_mock_modbus_server_controller_multiple.yaml b/tests/integration/fixtures/uart_mock_modbus_server_controller_multiple.yaml new file mode 100644 index 0000000000..7ec67b03db --- /dev/null +++ b/tests/integration/fixtures/uart_mock_modbus_server_controller_multiple.yaml @@ -0,0 +1,118 @@ +esphome: + name: uart-mock-modbus-server-mult + +host: +api: +logger: + level: VERBOSE + +external_components: + - source: + type: local + path: EXTERNAL_COMPONENT_PATH + +# Dummy uart entry to satisfy modbus's DEPENDENCIES = ["uart"] +# The actual UART bus used is the uart_mock component below +uart: + baud_rate: 115200 + port: /dev/null + +uart_mock: + - id: virtual_uart_server + baud_rate: 9600 + # auto_start must be true for loopback fixtures: the modbus controller + # polls on its update_interval immediately at boot, so the uart_mock + # forwarding must already be active or early requests are lost and + # generate modbus warnings. + auto_start: true + debug: + on_tx: + - then: + - uart_mock.inject_rx: + id: virtual_uart_controller + data: !lambda return data; + - uart_mock.inject_rx: + id: virtual_uart_server_2 + data: !lambda return data; + - id: virtual_uart_server_2 + baud_rate: 9600 + auto_start: true # See comment on virtual_uart_server above + debug: + on_tx: + - then: + - uart_mock.inject_rx: + id: virtual_uart_server + data: !lambda return data; + - uart_mock.inject_rx: + id: virtual_uart_controller + data: !lambda return data; + - id: virtual_uart_controller + baud_rate: 9600 + auto_start: true # See comment on virtual_uart_server above + debug: + on_tx: + - then: + - uart_mock.inject_rx: + id: virtual_uart_server + data: !lambda return data; + - uart_mock.inject_rx: + id: virtual_uart_server_2 + data: !lambda return data; + +modbus: + - uart_id: virtual_uart_server + id: virtual_modbus_server + role: server + - uart_id: virtual_uart_server_2 + id: virtual_modbus_server_2 + role: server + - uart_id: virtual_uart_controller + id: virtual_modbus_client + role: client + turnaround_time: 10ms + +modbus_controller: + - address: 1 + modbus_id: virtual_modbus_client + update_interval: 1s + id: modbus_controller_1 + - address: 2 + modbus_id: virtual_modbus_client + update_interval: 1s + id: modbus_controller_2 + + - address: 1 + modbus_id: virtual_modbus_server + server_registers: + - address: 0x01 + value_type: U_WORD + read_lambda: return 919; + - address: 2 + modbus_id: virtual_modbus_server_2 + server_registers: + - address: 0x01 + value_type: U_WORD + read_lambda: return 929; + +sensor: + - platform: modbus_controller + modbus_controller_id: modbus_controller_1 + name: "reg_u_word" + address: 0x01 + register_type: holding + value_type: U_WORD + - platform: modbus_controller + modbus_controller_id: modbus_controller_2 + name: "reg_u_word_2" + address: 0x01 + register_type: holding + value_type: U_WORD + +button: + - platform: template + name: "Start Scenario" + id: start_scenario_btn + on_press: + - lambda: "id(virtual_uart_server).start_scenario();" + - lambda: "id(virtual_uart_server_2).start_scenario();" + - lambda: "id(virtual_uart_controller).start_scenario();" diff --git a/tests/integration/fixtures/uart_mock_modbus_server_controller_write.yaml b/tests/integration/fixtures/uart_mock_modbus_server_controller_write.yaml new file mode 100644 index 0000000000..3edcc73f07 --- /dev/null +++ b/tests/integration/fixtures/uart_mock_modbus_server_controller_write.yaml @@ -0,0 +1,330 @@ +esphome: + name: uart-mock-modbus-srv-write + +host: +api: +logger: + level: VERBOSE + +external_components: + - source: + type: local + path: EXTERNAL_COMPONENT_PATH + +# Dummy uart entry to satisfy modbus's DEPENDENCIES = ["uart"] +# The actual UART bus used is the uart_mock component below +uart: + baud_rate: 115200 + port: /dev/null + +uart_mock: + - id: virtual_uart_server + baud_rate: 9600 + # auto_start must be true for loopback fixtures: the modbus controller + # polls on its update_interval immediately at boot, so the uart_mock + # forwarding must already be active or early requests are lost and + # generate modbus warnings. + auto_start: true + debug: + on_tx: + - then: + - uart_mock.inject_rx: + id: virtual_uart_controller + data: !lambda return data; + - id: virtual_uart_controller + baud_rate: 9600 + auto_start: true # See comment on virtual_uart_server above + debug: + on_tx: + - then: + - uart_mock.inject_rx: + id: virtual_uart_server + data: !lambda return data; + +globals: + - id: stored_u_word + type: uint16_t + initial_value: "11" + - id: stored_s_word + type: int16_t + initial_value: "-11" + - id: stored_u_dword + type: uint32_t + initial_value: "1001" + - id: stored_s_dword + type: int32_t + initial_value: "-1001" + - id: stored_u_dword_r + type: uint32_t + initial_value: "3003" + - id: stored_s_dword_r + type: int32_t + initial_value: "-3003" + - id: stored_u_qword + type: uint64_t + initial_value: "5005" + - id: stored_s_qword + type: int64_t + initial_value: "-5005" + - id: stored_u_qword_r + type: uint64_t + initial_value: "7007" + - id: stored_s_qword_r + type: int64_t + initial_value: "-7007" + - id: stored_fp32 + type: float + initial_value: "1.5" + - id: stored_fp32_r + type: float + initial_value: "2.5" + +modbus: + - uart_id: virtual_uart_server + id: virtual_modbus_server + role: server + - uart_id: virtual_uart_controller + id: virtual_modbus_controller + role: client + turnaround_time: 10ms + +modbus_controller: + - address: 1 + modbus_id: virtual_modbus_controller + update_interval: 2s + id: modbus_controller_1 + + - address: 1 + modbus_id: virtual_modbus_server + id: modbus_server_1 + server_registers: + - address: 0x01 + value_type: U_WORD + read_lambda: return id(stored_u_word); + write_lambda: id(stored_u_word) = x; return true; + - address: 0x03 + value_type: S_WORD + read_lambda: return id(stored_s_word); + write_lambda: id(stored_s_word) = x; return true; + - address: 0x05 + value_type: U_DWORD + read_lambda: return id(stored_u_dword); + write_lambda: id(stored_u_dword) = x; return true; + - address: 0x08 + value_type: S_DWORD + read_lambda: return id(stored_s_dword); + write_lambda: id(stored_s_dword) = x; return true; + - address: 0x0B + value_type: U_DWORD_R + read_lambda: return id(stored_u_dword_r); + write_lambda: id(stored_u_dword_r) = x; return true; + - address: 0x0E + value_type: S_DWORD_R + read_lambda: return id(stored_s_dword_r); + write_lambda: id(stored_s_dword_r) = x; return true; + - address: 0x11 + value_type: U_QWORD + read_lambda: return id(stored_u_qword); + write_lambda: id(stored_u_qword) = x; return true; + - address: 0x16 + value_type: S_QWORD + read_lambda: return id(stored_s_qword); + write_lambda: id(stored_s_qword) = x; return true; + - address: 0x1B + value_type: U_QWORD_R + read_lambda: return id(stored_u_qword_r); + write_lambda: id(stored_u_qword_r) = x; return true; + - address: 0x20 + value_type: S_QWORD_R + read_lambda: return id(stored_s_qword_r); + write_lambda: id(stored_s_qword_r) = x; return true; + - address: 0x25 + value_type: FP32 + read_lambda: return id(stored_fp32); + write_lambda: id(stored_fp32) = x; return true; + - address: 0x28 + value_type: FP32_R + read_lambda: return id(stored_fp32_r); + write_lambda: id(stored_fp32_r) = x; return true; + +sensor: + - platform: modbus_controller + modbus_controller_id: modbus_controller_1 + name: "reg_u_word" + address: 0x01 + register_type: holding + value_type: U_WORD + - platform: modbus_controller + modbus_controller_id: modbus_controller_1 + name: "reg_s_word" + address: 0x03 + register_type: holding + value_type: S_WORD + - platform: modbus_controller + modbus_controller_id: modbus_controller_1 + name: "reg_u_dword" + address: 0x05 + register_type: holding + value_type: U_DWORD + - platform: modbus_controller + modbus_controller_id: modbus_controller_1 + name: "reg_s_dword" + address: 0x08 + register_type: holding + value_type: S_DWORD + - platform: modbus_controller + modbus_controller_id: modbus_controller_1 + name: "reg_u_dword_r" + address: 0x0B + register_type: holding + value_type: U_DWORD_R + - platform: modbus_controller + modbus_controller_id: modbus_controller_1 + name: "reg_s_dword_r" + address: 0x0E + register_type: holding + value_type: S_DWORD_R + - platform: modbus_controller + modbus_controller_id: modbus_controller_1 + name: "reg_u_qword" + address: 0x11 + register_type: holding + value_type: U_QWORD + - platform: modbus_controller + modbus_controller_id: modbus_controller_1 + name: "reg_s_qword" + address: 0x16 + register_type: holding + value_type: S_QWORD + - platform: modbus_controller + modbus_controller_id: modbus_controller_1 + name: "reg_u_qword_r" + address: 0x1B + register_type: holding + value_type: U_QWORD_R + - platform: modbus_controller + modbus_controller_id: modbus_controller_1 + name: "reg_s_qword_r" + address: 0x20 + register_type: holding + value_type: S_QWORD_R + - platform: modbus_controller + modbus_controller_id: modbus_controller_1 + name: "reg_fp32" + address: 0x25 + register_type: holding + value_type: FP32 + - platform: modbus_controller + modbus_controller_id: modbus_controller_1 + name: "reg_fp32_r" + address: 0x28 + register_type: holding + value_type: FP32_R + +number: + - platform: modbus_controller + modbus_controller_id: modbus_controller_1 + name: "write_u_word" + address: 0x01 + register_type: holding + value_type: U_WORD + min_value: 0 + max_value: 65535 + - platform: modbus_controller + modbus_controller_id: modbus_controller_1 + name: "write_s_word" + address: 0x03 + register_type: holding + value_type: S_WORD + min_value: -16777215 + max_value: 16777215 + - platform: modbus_controller + modbus_controller_id: modbus_controller_1 + name: "write_u_dword" + address: 0x05 + register_type: holding + value_type: U_DWORD + min_value: 0 + max_value: 16777215 + - platform: modbus_controller + modbus_controller_id: modbus_controller_1 + name: "write_s_dword" + address: 0x08 + register_type: holding + value_type: S_DWORD + min_value: -16777215 + max_value: 16777215 + - platform: modbus_controller + modbus_controller_id: modbus_controller_1 + name: "write_u_dword_r" + address: 0x0B + register_type: holding + value_type: U_DWORD_R + min_value: 0 + max_value: 16777215 + - platform: modbus_controller + modbus_controller_id: modbus_controller_1 + name: "write_s_dword_r" + address: 0x0E + register_type: holding + value_type: S_DWORD_R + min_value: -16777215 + max_value: 16777215 + - platform: modbus_controller + modbus_controller_id: modbus_controller_1 + name: "write_u_qword" + address: 0x11 + register_type: holding + value_type: U_QWORD + min_value: 0 + max_value: 16777215 + - platform: modbus_controller + modbus_controller_id: modbus_controller_1 + name: "write_s_qword" + address: 0x16 + register_type: holding + value_type: S_QWORD + min_value: -16777215 + max_value: 16777215 + - platform: modbus_controller + modbus_controller_id: modbus_controller_1 + name: "write_u_qword_r" + address: 0x1B + register_type: holding + value_type: U_QWORD_R + min_value: 0 + max_value: 16777215 + - platform: modbus_controller + modbus_controller_id: modbus_controller_1 + name: "write_s_qword_r" + address: 0x20 + register_type: holding + value_type: S_QWORD_R + min_value: -16777215 + max_value: 16777215 + - platform: modbus_controller + modbus_controller_id: modbus_controller_1 + name: "write_fp32" + address: 0x25 + register_type: holding + value_type: FP32 + min_value: -16777215 + max_value: 16777215 + step: 0.01 + - platform: modbus_controller + modbus_controller_id: modbus_controller_1 + name: "write_fp32_r" + address: 0x28 + register_type: holding + value_type: FP32_R + min_value: -16777215 + max_value: 16777215 + step: 0.01 + +button: + - platform: template + name: "Start Scenario" + id: start_scenario_btn + on_press: + - lambda: "id(virtual_uart_server).start_scenario();" + - lambda: "id(virtual_uart_controller).start_scenario();" diff --git a/tests/integration/fixtures/uart_mock_modbus_timing.yaml b/tests/integration/fixtures/uart_mock_modbus_timing.yaml index f4cf0bde37..c670864085 100644 --- a/tests/integration/fixtures/uart_mock_modbus_timing.yaml +++ b/tests/integration/fixtures/uart_mock_modbus_timing.yaml @@ -22,6 +22,8 @@ uart_mock: baud_rate: 9600 rx_full_threshold: 120 rx_timeout: 2 + # auto_start must be false to avoid races: the test presses the + # "Start Scenario" button only after subscribing to states. auto_start: false debug: on_tx: @@ -61,4 +63,4 @@ button: name: "Start Scenario" id: start_scenario_btn on_press: - - lambda: 'id(virtual_uart_dev).start_scenario();' + - lambda: "id(virtual_uart_dev).start_scenario();" diff --git a/tests/integration/state_utils.py b/tests/integration/state_utils.py index 5792a8e804..d42b50ecdb 100644 --- a/tests/integration/state_utils.py +++ b/tests/integration/state_utils.py @@ -346,3 +346,109 @@ class SensorStateCollector: else: self._waiters.append((condition, future)) return future + + +class SensorTracker: + """Data-driven sensor state tracker with expected-value futures. + + Tracks sensor state updates and resolves futures when sensors report + specific expected values. Eliminates per-sensor future boilerplate. + + Usage:: + + tracker = SensorTracker(["reg_u_word", "reg_s_word"]) + futures = tracker.expect_all({"reg_u_word": 99, "reg_s_word": -99}) + # ... subscribe_states with tracker.on_state, start scenario ... + await tracker.await_all(futures) + """ + + def __init__(self, sensor_names: list[str]) -> None: + self.sensor_states: dict[str, list[float]] = {name: [] for name in sensor_names} + self.key_to_sensor: dict[int, str] = {} + self._expectations: dict[str, list[tuple[object, asyncio.Future]]] = {} + + _ANY = object() # Sentinel: match any value + + def expect(self, name: str, value: object) -> asyncio.Future: + """Register an expected value for *name* and return a future for it.""" + future: asyncio.Future = asyncio.get_running_loop().create_future() + self._expectations.setdefault(name, []).append((value, future)) + return future + + def expect_any(self, name: str) -> asyncio.Future: + """Register a future that resolves on *any* state update for *name*.""" + return self.expect(name, self._ANY) + + def expect_all(self, expected: dict[str, object]) -> dict[str, asyncio.Future]: + """Call ``expect`` for every entry and return a dict of futures.""" + return {name: self.expect(name, value) for name, value in expected.items()} + + def on_state(self, state: EntityState) -> None: + """State callback suitable for ``subscribe_states``.""" + if not isinstance(state, SensorState) or state.missing_state: + return + sensor_name = self.key_to_sensor.get(state.key) + if not sensor_name or sensor_name not in self.sensor_states: + return + self.sensor_states[sensor_name].append(state.state) + for expected_value, future in self._expectations.get(sensor_name, []): + if not future.done() and ( + expected_value is self._ANY or state.state == expected_value + ): + future.set_result(True) + break + + async def await_change( + self, future: asyncio.Future, name: str, timeout: float = 2.0 + ) -> None: + """Wait for a sensor future to resolve; fail the test on timeout.""" + try: + await asyncio.wait_for(future, timeout=timeout) + except TimeoutError: + import pytest + + pytest.fail( + f"Timeout waiting for {name} change. Received sensor states:\n" + f" {name}: {self.sensor_states[name]}\n" + ) + + async def await_must_not_change( + self, future: asyncio.Future, name: str, timeout: float = 2.0 + ) -> None: + """Assert a sensor future does NOT resolve within the timeout.""" + try: + await asyncio.wait_for(future, timeout=timeout) + except TimeoutError: + return # Expected + import pytest + + pytest.fail( + f"{name} change should not have been triggered, but was. " + f"Received sensor states:\n {name}: {self.sensor_states[name]}\n" + ) + + async def await_all( + self, futures: dict[str, asyncio.Future], timeout: float = 2.0 + ) -> None: + """Await every future in *futures*, failing with per-sensor diagnostics.""" + for name, future in futures.items(): + await self.await_change(future, name, timeout=timeout) + + async def setup_and_start_scenario(self, client) -> list: + """Wire up subscriptions, wait for initial states, press Start Scenario.""" + entities, _ = await client.list_entities_services() + self.key_to_sensor.update( + build_key_to_entity_mapping(entities, list(self.sensor_states.keys())) + ) + initial_state_helper = InitialStateHelper(entities) + client.subscribe_states(initial_state_helper.on_state_wrapper(self.on_state)) + try: + await initial_state_helper.wait_for_initial_states() + except TimeoutError: + import pytest + + pytest.fail("Timeout waiting for initial states") + start_btn = find_entity(entities, "start_scenario", ButtonInfo) + assert start_btn is not None, "Start Scenario button not found" + client.button_command(start_btn.key) + return entities diff --git a/tests/integration/test_multi_click_trigger.py b/tests/integration/test_multi_click_trigger.py new file mode 100644 index 0000000000..8a020dd18b --- /dev/null +++ b/tests/integration/test_multi_click_trigger.py @@ -0,0 +1,82 @@ +"""Integration test for on_multi_click binary sensor automation. + +Tests that on_multi_click correctly triggers for single click, double click, +and long press patterns using a template binary sensor with timing +orchestrated entirely in YAML. + +""" + +from __future__ import annotations + +import asyncio +import re + +import pytest + +from .types import APIClientConnectedFactory, RunCompiledFunction + + +@pytest.mark.asyncio +async def test_multi_click_trigger( + yaml_config: str, + run_compiled: RunCompiledFunction, + api_client_connected: APIClientConnectedFactory, +) -> None: + """Test that on_multi_click triggers for single, double, and long press patterns.""" + loop = asyncio.get_running_loop() + + single_click_pattern = re.compile(r"SINGLE_CLICK count=(\d+)") + double_click_pattern = re.compile(r"DOUBLE_CLICK count=(\d+)") + long_press_pattern = re.compile(r"LONG_PRESS count=(\d+)") + + single_click_future: asyncio.Future[int] = loop.create_future() + double_click_future: asyncio.Future[int] = loop.create_future() + long_press_future: asyncio.Future[int] = loop.create_future() + + def check_output(line: str) -> None: + """Check log output for multi-click trigger messages.""" + if m := single_click_pattern.search(line): + if not single_click_future.done(): + single_click_future.set_result(int(m.group(1))) + elif m := double_click_pattern.search(line): + if not double_click_future.done(): + double_click_future.set_result(int(m.group(1))) + elif (m := long_press_pattern.search(line)) and not long_press_future.done(): + long_press_future.set_result(int(m.group(1))) + + async with ( + run_compiled(yaml_config, line_callback=check_output), + api_client_connected() as client, + ): + _entities, services = await client.list_entities_services() + + test_service = next((s for s in services if s.name == "run_all_tests"), None) + assert test_service is not None, "run_all_tests service not found" + + # Kick off the entire test sequence (runs in YAML with delays) + await client.execute_service(test_service, {}) + + # Wait for all three triggers + try: + count = await asyncio.wait_for(single_click_future, timeout=5.0) + except TimeoutError: + pytest.fail( + "Timeout waiting for SINGLE_CLICK - on_multi_click did not trigger." + ) + assert count == 1, f"Expected single click count=1, got {count}" + + try: + count = await asyncio.wait_for(double_click_future, timeout=5.0) + except TimeoutError: + pytest.fail( + "Timeout waiting for DOUBLE_CLICK - on_multi_click did not trigger." + ) + assert count == 1, f"Expected double click count=1, got {count}" + + try: + count = await asyncio.wait_for(long_press_future, timeout=5.0) + except TimeoutError: + pytest.fail( + "Timeout waiting for LONG_PRESS - on_multi_click did not trigger." + ) + assert count == 1, f"Expected long press count=1, got {count}" diff --git a/tests/integration/test_uart_mock_ld2410.py b/tests/integration/test_uart_mock_ld2410.py index ce0e1bb7ec..88d6f2cbac 100644 --- a/tests/integration/test_uart_mock_ld2410.py +++ b/tests/integration/test_uart_mock_ld2410.py @@ -73,9 +73,16 @@ async def test_uart_mock_ld2410( ], ) - # Signal when we see recovery frame values + # Signal when we see ALL recovery frame values to avoid race where some + # arrive after the waiter fires but before we index into the lists recovery_received = collector.add_waiter( - lambda: pytest.approx(50.0) in collector.sensor_states["moving_distance"] + lambda: ( + pytest.approx(50.0) in collector.sensor_states["moving_distance"] + and pytest.approx(75.0) in collector.sensor_states["still_distance"] + and pytest.approx(100.0) in collector.sensor_states["moving_energy"] + and pytest.approx(80.0) in collector.sensor_states["still_energy"] + and pytest.approx(127.0) in collector.sensor_states["detection_distance"] + ) ) async with ( diff --git a/tests/integration/test_uart_mock_ld2450.py b/tests/integration/test_uart_mock_ld2450.py index b1aa2f6952..2469273e0a 100644 --- a/tests/integration/test_uart_mock_ld2450.py +++ b/tests/integration/test_uart_mock_ld2450.py @@ -83,11 +83,18 @@ async def test_uart_mock_ld2450( ], ) - # Signal when we see recovery frame values (target 1 distance ≈ 500mm) + # Signal when we see all recovery frame values + # Must wait for ALL values to avoid race where some arrive after the waiter fires recovery_received = collector.add_waiter( lambda: ( pytest.approx(500.0, abs=1.0) in collector.sensor_states["target_1_distance"] + and pytest.approx(300.0) in collector.sensor_states["target_1_x"] + and pytest.approx(400.0) in collector.sensor_states["target_1_y"] + and pytest.approx(30.0) in collector.sensor_states["target_1_speed"] + and pytest.approx(1.0) in collector.sensor_states["target_count"] + and pytest.approx(1.0) in collector.sensor_states["moving_target_count"] + and pytest.approx(0.0) in collector.sensor_states["still_target_count"] ) ) diff --git a/tests/integration/test_uart_mock_modbus.py b/tests/integration/test_uart_mock_modbus.py index e341d86f53..e8dfa1b822 100644 --- a/tests/integration/test_uart_mock_modbus.py +++ b/tests/integration/test_uart_mock_modbus.py @@ -14,15 +14,67 @@ test_uart_mock_modbus_no_threshold : from __future__ import annotations import asyncio -from pathlib import Path +from collections.abc import Callable +from dataclasses import dataclass -from aioesphomeapi import ButtonInfo, EntityState, SensorState +from aioesphomeapi import NumberInfo import pytest -from .state_utils import InitialStateHelper, build_key_to_entity_mapping, find_entity +from .state_utils import SensorTracker, find_entity from .types import APIClientConnectedFactory, RunCompiledFunction +@dataclass +class RegisterTestCase: + """Test parameters for a single modbus register write/read round-trip.""" + + initial_value: object + write_number_name: str + write_value: float + post_write_value: object + + +# --------------------------------------------------------------------------- +# Helpers +# --------------------------------------------------------------------------- + + +def _make_modbus_line_callback() -> tuple[Callable[[str], None], list[str], list[str]]: + """Return a (callback, error_lines, warning_lines) tuple for tracking modbus log output. + + Only captures bus-level modbus messages ([modbus:]), not modbus_controller + scheduling noise (e.g. "Duplicate modbus command found"). + """ + error_log_lines: list[str] = [] + warning_log_lines: list[str] = [] + + def line_callback(line: str) -> None: + if "[E][modbus:" in line: + error_log_lines.append(line) + if "[W][modbus:" in line: + warning_log_lines.append(line) + + return line_callback, error_log_lines, warning_log_lines + + +def _assert_no_modbus_errors( + error_log_lines: list[str], warning_log_lines: list[str] +) -> None: + assert len(error_log_lines) == 0, ( + "Expect no errors logged by the modbus mock, but got:\n" + + "\n".join(error_log_lines) + ) + assert len(warning_log_lines) == 0, ( + "Expect no warnings logged by the modbus mock, but got:\n" + + "\n".join(warning_log_lines) + ) + + +# --------------------------------------------------------------------------- +# Tests +# --------------------------------------------------------------------------- + + @pytest.mark.asyncio async def test_uart_mock_modbus( yaml_config: str, @@ -30,127 +82,41 @@ async def test_uart_mock_modbus( api_client_connected: APIClientConnectedFactory, ) -> None: """Test basic modbus data parsing.""" - # Replace external component path placeholder - external_components_path = str( - Path(__file__).parent / "fixtures" / "external_components" + + tracker = SensorTracker( + [ + "basic_register", + "delayed_response", + "late_response", + "no_response", + "exception_response", + ] ) - yaml_config = yaml_config.replace( - "EXTERNAL_COMPONENT_PATH", external_components_path - ) - - loop = asyncio.get_running_loop() - - # Track sensor state updates (after initial state is swallowed) - sensor_states: dict[str, list[float]] = { - "basic_register": [], - "delayed_response": [], - "late_response": [], - "no_response": [], - "exception_response": [], - } - - basic_register_changed = loop.create_future() - delayed_response_changed = loop.create_future() - late_response_changed = loop.create_future() - no_response_changed = loop.create_future() - exception_response_changed = loop.create_future() - - def on_state(state: EntityState) -> None: - if isinstance(state, SensorState) and not state.missing_state: - sensor_name = key_to_sensor.get(state.key) - if sensor_name and sensor_name in sensor_states: - sensor_states[sensor_name].append(state.state) - if ( - sensor_name == "basic_register" - and state.state == 259.0 - and not basic_register_changed.done() - ): - basic_register_changed.set_result(True) - elif ( - sensor_name == "delayed_response" - and state.state == 255.0 - and not delayed_response_changed.done() - ): - delayed_response_changed.set_result(True) - elif ( - sensor_name == "late_response" and not late_response_changed.done() - ): - late_response_changed.set_result(True) - elif sensor_name == "no_response" and not no_response_changed.done(): - no_response_changed.set_result(True) - elif ( - sensor_name == "exception_response" - and not exception_response_changed.done() - ): - exception_response_changed.set_result(True) + basic_register_changed = tracker.expect("basic_register", 259.0) + delayed_response_changed = tracker.expect("delayed_response", 255.0) + # late_response / no_response / exception_response: expect *any* value + # (these should never fire, so we use a permissive match via expect_any) + late_response_changed = tracker.expect_any("late_response") + no_response_changed = tracker.expect_any("no_response") + exception_response_changed = tracker.expect_any("exception_response") async with ( run_compiled(yaml_config), api_client_connected() as client, ): - entities, _ = await client.list_entities_services() + await tracker.setup_and_start_scenario(client) - # Build key mappings for all sensor types - all_names = list(sensor_states.keys()) - key_to_sensor = build_key_to_entity_mapping(entities, all_names) - - # Set up initial state helper - initial_state_helper = InitialStateHelper(entities) - client.subscribe_states(initial_state_helper.on_state_wrapper(on_state)) - - try: - await initial_state_helper.wait_for_initial_states() - except TimeoutError: - pytest.fail("Timeout waiting for initial states") - - # Start the UART mock scenario now that we're subscribed - start_btn = find_entity(entities, "start_scenario", ButtonInfo) - assert start_btn is not None, "Start Scenario button not found" - client.button_command(start_btn.key) - - try: - await asyncio.wait_for(delayed_response_changed, timeout=2.0) - except TimeoutError: - pytest.fail( - f"Timeout waiting for delayed_response change. Received sensor states:\n" - f" delayed_response: {sensor_states['delayed_response']}\n" - ) - - try: - await asyncio.wait_for(late_response_changed, timeout=2.0) - pytest.fail( - f"late_response change should not have been triggered, but was. Received sensor states:\n" - f" late_response: {sensor_states['late_response']}\n" - ) - except TimeoutError: - pass # Expected timeout since we never inject a response for late_response - - try: - await asyncio.wait_for(no_response_changed, timeout=2.0) - pytest.fail( - f"no_response change should not have been triggered, but was. Received sensor states:\n" - f" no_response: {sensor_states['no_response']}\n" - ) - except TimeoutError: - pass # Expected timeout since we never inject a response for no_response - - # Wait for basic register to be updated with successful parse - try: - await asyncio.wait_for(basic_register_changed, timeout=2.0) - except TimeoutError: - pytest.fail( - f"Timeout waiting for Basic Register change. Received sensor states:\n" - f" basic_register: {sensor_states['basic_register']}\n" - ) - - try: - await asyncio.wait_for(exception_response_changed, timeout=2.0) - pytest.fail( - f"exception_response change should not have been triggered, but was. Received sensor states:\n" - f" exception_response: {sensor_states['exception_response']}\n" - ) - except TimeoutError: - pass + await tracker.await_change(delayed_response_changed, "delayed_response") + await tracker.await_change(basic_register_changed, "basic_register") + # Run all "must not change" checks concurrently — each waits the full + # timeout, so sequential execution would multiply the wall time. + await asyncio.gather( + tracker.await_must_not_change(late_response_changed, "late_response"), + tracker.await_must_not_change(no_response_changed, "no_response"), + tracker.await_must_not_change( + exception_response_changed, "exception_response" + ), + ) @pytest.mark.asyncio @@ -159,69 +125,17 @@ async def test_uart_mock_modbus_timing( run_compiled: RunCompiledFunction, api_client_connected: APIClientConnectedFactory, ) -> None: - """Test basic modbus data parsing.""" - # Replace external component path placeholder - external_components_path = str( - Path(__file__).parent / "fixtures" / "external_components" - ) - yaml_config = yaml_config.replace( - "EXTERNAL_COMPONENT_PATH", external_components_path - ) + """Test modbus timing with multi-register SDM meter response.""" - loop = asyncio.get_running_loop() - - # Track sensor state updates (after initial state is swallowed) - sensor_states: dict[str, list[float]] = { - "sdm_voltage": [], - } - - voltage_changed = loop.create_future() - - def on_state(state: EntityState) -> None: - if isinstance(state, SensorState) and not state.missing_state: - sensor_name = key_to_sensor.get(state.key) - if sensor_name and sensor_name in sensor_states: - sensor_states[sensor_name].append(state.state) - # Check if this is a good voltage reading (243V) - if ( - sensor_name == "sdm_voltage" - and state.state > 200.0 - and not voltage_changed.done() - ): - voltage_changed.set_result(True) + tracker = SensorTracker(["sdm_voltage"]) + voltage_changed = tracker.expect_any("sdm_voltage") async with ( run_compiled(yaml_config), api_client_connected() as client, ): - entities, _ = await client.list_entities_services() - - # Build key mappings for all sensor types - all_names = list(sensor_states.keys()) - key_to_sensor = build_key_to_entity_mapping(entities, all_names) - - # Set up initial state helper - initial_state_helper = InitialStateHelper(entities) - client.subscribe_states(initial_state_helper.on_state_wrapper(on_state)) - - try: - await initial_state_helper.wait_for_initial_states() - except TimeoutError: - pytest.fail("Timeout waiting for initial states") - - # Start the UART mock scenario now that we're subscribed - start_btn = find_entity(entities, "start_scenario", ButtonInfo) - assert start_btn is not None, "Start Scenario button not found" - client.button_command(start_btn.key) - - # Wait for voltage to be updated with successful parse - try: - await asyncio.wait_for(voltage_changed, timeout=2.0) - except TimeoutError: - pytest.fail( - f"Timeout waiting for SDM voltage change. Received sensor states:\n" - f" sdm_voltage: {sensor_states['sdm_voltage']}\n" - ) + await tracker.setup_and_start_scenario(client) + await tracker.await_change(voltage_changed, "sdm_voltage") @pytest.mark.asyncio @@ -234,66 +148,187 @@ async def test_uart_mock_modbus_no_threshold( Without the 50ms fallback timeout, the chunked response with a 40ms gap between USB packets would cause a false timeout and CRC failure cascade. + Bus-level warnings (CRC failures, buffer clears) are expected during + chunked reassembly — the test only verifies the final value arrives. """ - # Replace external component path placeholder - external_components_path = str( - Path(__file__).parent / "fixtures" / "external_components" - ) - yaml_config = yaml_config.replace( - "EXTERNAL_COMPONENT_PATH", external_components_path - ) - loop = asyncio.get_running_loop() - - # Track sensor state updates (after initial state is swallowed) - sensor_states: dict[str, list[float]] = { - "sdm_voltage": [], - } - - voltage_changed = loop.create_future() - - def on_state(state: EntityState) -> None: - if isinstance(state, SensorState) and not state.missing_state: - sensor_name = key_to_sensor.get(state.key) - if sensor_name and sensor_name in sensor_states: - sensor_states[sensor_name].append(state.state) - # Check if this is a good voltage reading (243V) - if ( - sensor_name == "sdm_voltage" - and state.state > 200.0 - and not voltage_changed.done() - ): - voltage_changed.set_result(True) + tracker = SensorTracker(["sdm_voltage"]) + voltage_changed = tracker.expect_any("sdm_voltage") async with ( run_compiled(yaml_config), api_client_connected() as client, ): - entities, _ = await client.list_entities_services() + await tracker.setup_and_start_scenario(client) + await tracker.await_change(voltage_changed, "sdm_voltage") - # Build key mappings for all sensor types - all_names = list(sensor_states.keys()) - key_to_sensor = build_key_to_entity_mapping(entities, all_names) - # Set up initial state helper - initial_state_helper = InitialStateHelper(entities) - client.subscribe_states(initial_state_helper.on_state_wrapper(on_state)) +@pytest.mark.asyncio +@pytest.mark.xfail( + reason="Modbus parser cannot handle server responses from other devices on the bus. Fix tracked in PR #11969.", + strict=True, +) +async def test_uart_mock_modbus_server( + yaml_config: str, + run_compiled: RunCompiledFunction, + api_client_connected: APIClientConnectedFactory, +) -> None: + """Test modbus server parsing with peer traffic on a shared bus.""" - try: - await initial_state_helper.wait_for_initial_states() - except TimeoutError: - pytest.fail("Timeout waiting for initial states") + line_callback, error_log_lines, warning_log_lines = _make_modbus_line_callback() - # Start the UART mock scenario now that we're subscribed - start_btn = find_entity(entities, "start_scenario", ButtonInfo) - assert start_btn is not None, "Start Scenario button not found" - client.button_command(start_btn.key) + tracker = SensorTracker( + ["basic_read", "read_after_peer_response", "read_after_peer_timeout"] + ) + futures = tracker.expect_all( + { + "basic_read": 1, + "read_after_peer_response": 1, + "read_after_peer_timeout": 1, + } + ) - # Wait for voltage to be updated with successful parse - try: - await asyncio.wait_for(voltage_changed, timeout=2.0) - except TimeoutError: - pytest.fail( - f"Timeout waiting for SDM voltage change. Received sensor states:\n" - f" sdm_voltage: {sensor_states['sdm_voltage']}\n" + async with ( + run_compiled(yaml_config, line_callback=line_callback), + api_client_connected() as client, + ): + await tracker.setup_and_start_scenario(client) + await tracker.await_all(futures) + _assert_no_modbus_errors(error_log_lines, warning_log_lines) + + +@pytest.mark.asyncio +async def test_uart_mock_modbus_server_controller( + yaml_config: str, + run_compiled: RunCompiledFunction, + api_client_connected: APIClientConnectedFactory, +) -> None: + """Test server/controller functionality for all read register types.""" + + line_callback, error_log_lines, warning_log_lines = _make_modbus_line_callback() + + expected_values = { + "reg_u_word": 99, + "reg_s_word": -99, + "reg_u_dword": 16909060, + "reg_s_dword": -16909060, + "reg_u_dword_r": pytest.approx(67305985), + "reg_s_dword_r": pytest.approx(-67305985), + "reg_u_qword": pytest.approx(72623859790382856), + "reg_s_qword": pytest.approx(-72623859790382856), + "reg_u_qword_r": pytest.approx(578437695752307201), + "reg_s_qword_r": pytest.approx(-578437695752307201), + "reg_fp32": pytest.approx(3.14), + "reg_fp32_r": pytest.approx(3.14), + } + tracker = SensorTracker(list(expected_values.keys())) + futures = tracker.expect_all(expected_values) + + async with ( + run_compiled(yaml_config, line_callback=line_callback), + api_client_connected() as client, + ): + await tracker.setup_and_start_scenario(client) + await tracker.await_all(futures) + _assert_no_modbus_errors(error_log_lines, warning_log_lines) + + +@pytest.mark.asyncio +async def test_uart_mock_modbus_server_controller_write( + yaml_config: str, + run_compiled: RunCompiledFunction, + api_client_connected: APIClientConnectedFactory, +) -> None: + """Test server/controller write functionality for all register value types. + + Verifies that writing to modbus server registers via the controller updates + the server's stored values, which are then read back correctly on the next poll. + All 12 value types are tested: U/S_WORD, U/S_DWORD(_R), U/S_QWORD(_R), FP32(_R). + """ + + line_callback, error_log_lines, warning_log_lines = _make_modbus_line_callback() + + register_test_cases: dict[str, RegisterTestCase] = { + "reg_u_word": RegisterTestCase(11, "write_u_word", 42, 42), + "reg_s_word": RegisterTestCase(-11, "write_s_word", -42, -42), + "reg_u_dword": RegisterTestCase(1001, "write_u_dword", 2002, 2002), + "reg_s_dword": RegisterTestCase(-1001, "write_s_dword", -2002, -2002), + "reg_u_dword_r": RegisterTestCase(3003, "write_u_dword_r", 4004, 4004), + "reg_s_dword_r": RegisterTestCase(-3003, "write_s_dword_r", -4004, -4004), + "reg_u_qword": RegisterTestCase(5005, "write_u_qword", 6006, 6006), + "reg_s_qword": RegisterTestCase(-5005, "write_s_qword", -6006, -6006), + "reg_u_qword_r": RegisterTestCase(7007, "write_u_qword_r", 8008, 8008), + "reg_s_qword_r": RegisterTestCase(-7007, "write_s_qword_r", -8008, -8008), + "reg_fp32": RegisterTestCase( + pytest.approx(1.5, abs=0.01), + "write_fp32", + 3.14, + pytest.approx(3.14, abs=0.01), + ), + "reg_fp32_r": RegisterTestCase( + pytest.approx(2.5, abs=0.01), + "write_fp32_r", + 6.28, + pytest.approx(6.28, abs=0.01), + ), + } + + tracker = SensorTracker(list(register_test_cases.keys())) + + # Phase 1: expect initial baseline values + initial_futures = tracker.expect_all( + {name: case.initial_value for name, case in register_test_cases.items()} + ) + # Phase 2: expect post-write values (registered now so on_state can match them) + written_futures = tracker.expect_all( + {name: case.post_write_value for name, case in register_test_cases.items()} + ) + + async with ( + run_compiled(yaml_config, line_callback=line_callback), + api_client_connected() as client, + ): + entities = await tracker.setup_and_start_scenario(client) + + # Wait for initial baseline values to confirm the controller <-> server + # connection is working before issuing writes + await tracker.await_all(initial_futures, timeout=4.0) + + # Issue write commands for all register types + for case in register_test_cases.values(): + entity = find_entity(entities, case.write_number_name, NumberInfo) + assert entity is not None, ( + f"{case.write_number_name} number entity not found" ) + client.number_command(entity.key, case.write_value) + + # Wait for sensors to reflect the written values (round-trip write+read) + await tracker.await_all(written_futures, timeout=4.0) + _assert_no_modbus_errors(error_log_lines, warning_log_lines) + + +@pytest.mark.asyncio +@pytest.mark.xfail( + reason="Modbus parser cannot handle server responses from other devices on the bus. Fix tracked in PR #11969.", + strict=True, +) +async def test_uart_mock_modbus_server_controller_multiple( + yaml_config: str, + run_compiled: RunCompiledFunction, + api_client_connected: APIClientConnectedFactory, +) -> None: + """Test server/controller functionality with multiple servers.""" + + line_callback, error_log_lines, warning_log_lines = _make_modbus_line_callback() + + expected_values = {"reg_u_word": 919, "reg_u_word_2": 929} + tracker = SensorTracker(list(expected_values.keys())) + futures = tracker.expect_all(expected_values) + + async with ( + run_compiled(yaml_config, line_callback=line_callback), + api_client_connected() as client, + ): + await tracker.setup_and_start_scenario(client) + await tracker.await_all(futures) + _assert_no_modbus_errors(error_log_lines, warning_log_lines) diff --git a/tests/script/test_helpers.py b/tests/script/test_helpers.py index 28f111d758..948aabaa66 100644 --- a/tests/script/test_helpers.py +++ b/tests/script/test_helpers.py @@ -1037,7 +1037,7 @@ def test_get_all_dependencies_platform_component() -> None: with ( patch("esphome.loader.get_component") as mock_get_component, - patch("helpers.get_platform") as mock_get_platform, + patch("esphome.loader.get_platform") as mock_get_platform, ): mock_get_platform.return_value = platform_comp mock_get_component.return_value = None @@ -1061,7 +1061,7 @@ def test_get_all_dependencies_platform_component_with_dependencies() -> None: with ( patch("esphome.loader.get_component") as mock_get_component, - patch("helpers.get_platform") as mock_get_platform, + patch("esphome.loader.get_platform") as mock_get_platform, ): mock_get_platform.return_value = platform_comp mock_get_component.side_effect = lambda name: ( diff --git a/tests/unit_tests/components/test_esp8266.py b/tests/unit_tests/components/test_esp8266.py new file mode 100644 index 0000000000..318fd2d889 --- /dev/null +++ b/tests/unit_tests/components/test_esp8266.py @@ -0,0 +1,62 @@ +"""Tests for ESP8266 component.""" + +import pytest + +from esphome.components.esp8266 import lambdas_use_scanf_float +from esphome.core import Lambda +from esphome.types import ConfigType + + +@pytest.mark.parametrize( + ("src", "expected"), + [ + # Basic float formats + ('sscanf(buf, "%f", &v)', True), + ('sscanf(buf, "%F", &v)', True), + ('sscanf(buf, "%e", &v)', True), + ('sscanf(buf, "%E", &v)', True), + ('sscanf(buf, "%g", &v)', True), + ('sscanf(buf, "%G", &v)', True), + ('sscanf(buf, "%a", &v)', True), + ('sscanf(buf, "%A", &v)', True), + # With modifiers + ('sscanf(buf, "%lf", &v)', True), + ('sscanf(buf, "%Lf", &v)', True), + ('sscanf(buf, "%8lf", &v)', True), + ('sscanf(buf, "%*f")', True), + ('sscanf(buf, "%.2f", &v)', True), + # Mixed formats + ('sscanf(buf, "%d,%f", &a, &b)', True), + # fscanf and std::sscanf + ('fscanf(fp, "%f", &v)', True), + ('std::sscanf(buf, "%f", &v)', True), + # Multi-line + ('sscanf(buf,\n"%f", &v)', True), + # No float format + ('sscanf(buf, "%d", &v)', False), + ('sscanf(buf, "%s", s)', False), + # printf not scanf + ('printf("%f", val)', False), + # %f in a different statement after scanf + ('sscanf(buf, "%d", &x); printf("%f", val);', False), + # scanf %f in comment only + ('// sscanf(buf, "%f", &v)\nsscanf(buf, "%d", &x)', False), + ('/* sscanf(buf, "%f") */\nsscanf(buf, "%d", &x)', False), + ], +) +def test_lambdas_use_scanf_float(src: str, expected: bool) -> None: + """Test scanf float detection in lambda source.""" + config: ConfigType = {"test": [Lambda(src)]} + assert lambdas_use_scanf_float(config) is expected + + +def test_lambdas_use_scanf_float_no_lambdas() -> None: + """Test with config containing no lambdas.""" + config: ConfigType = {"key": "value", "list": [1, 2]} + assert lambdas_use_scanf_float(config) is False + + +def test_lambdas_use_scanf_float_nested() -> None: + """Test detection in deeply nested config.""" + config: ConfigType = {"a": {"b": {"c": [Lambda('sscanf(buf, "%f", &v)')]}}} + assert lambdas_use_scanf_float(config) is True diff --git a/tests/unit_tests/components/test_time.py b/tests/unit_tests/components/test_time.py new file mode 100644 index 0000000000..48988fb03f --- /dev/null +++ b/tests/unit_tests/components/test_time.py @@ -0,0 +1,80 @@ +"""Tests for time component cron expression parsing.""" + +from esphome.components.time import _parse_cron_part + + +def test_star_slash_seconds() -> None: + assert _parse_cron_part("*/10", 0, 60, {}) == {0, 10, 20, 30, 40, 50, 60} + + +def test_star_slash_minutes() -> None: + assert _parse_cron_part("*/5", 0, 59, {}) == { + 0, + 5, + 10, + 15, + 20, + 25, + 30, + 35, + 40, + 45, + 50, + 55, + } + + +def test_star_slash_hours() -> None: + assert _parse_cron_part("*/2", 0, 23, {}) == { + 0, + 2, + 4, + 6, + 8, + 10, + 12, + 14, + 16, + 18, + 20, + 22, + } + + +def test_star_slash_days_of_month() -> None: + """days_of_month starts at 1, not 0.""" + assert _parse_cron_part("*/5", 1, 31, {}) == {1, 6, 11, 16, 21, 26, 31} + + +def test_question_slash() -> None: + assert _parse_cron_part("?/10", 0, 60, {}) == {0, 10, 20, 30, 40, 50, 60} + + +def test_empty_offset_slash() -> None: + """Empty offset defaults to min_value.""" + assert _parse_cron_part("/10", 0, 60, {}) == {0, 10, 20, 30, 40, 50, 60} + + +def test_empty_offset_slash_nonzero_min() -> None: + """Empty offset defaults to min_value, not 0.""" + assert _parse_cron_part("/5", 1, 31, {}) == {1, 6, 11, 16, 21, 26, 31} + + +def test_numeric_offset_slash() -> None: + assert _parse_cron_part("5/10", 0, 60, {}) == {5, 15, 25, 35, 45, 55} + + +def test_star() -> None: + assert _parse_cron_part("*", 0, 59, {}) == set(range(0, 60)) + + +def test_question() -> None: + assert _parse_cron_part("?", 0, 59, {}) == set(range(0, 60)) + + +def test_range() -> None: + assert _parse_cron_part("1-5", 0, 59, {}) == {1, 2, 3, 4, 5} + + +def test_single_value() -> None: + assert _parse_cron_part("30", 0, 59, {}) == {30} diff --git a/tests/unit_tests/core/test_config.py b/tests/unit_tests/core/test_config.py index 474d31a90a..6fa8f7ed43 100644 --- a/tests/unit_tests/core/test_config.py +++ b/tests/unit_tests/core/test_config.py @@ -248,6 +248,24 @@ def test_area_id_hash_collision( ) +def test_area_singular_hash_collision( + yaml_file: Callable[[str], str], capsys: pytest.CaptureFixture[str] +) -> None: + """Test that area hash collisions between singular area: and areas: list are detected.""" + result = load_config_from_fixture( + yaml_file, "area_singular_hash_collision.yaml", FIXTURES_DIR + ) + assert result is None + + captured = capsys.readouterr() + assert ( + "Area ID 'd6ka' with hash 3082558663 collides with existing area ID 'test_2258'" + in captured.out + ) + # Error path should point to 'areas' (where the colliding entry is), not 'area' + assert "areas" in captured.out + + def test_device_duplicate_id( yaml_file: Callable[[str], str], capsys: pytest.CaptureFixture[str] ) -> None: diff --git a/tests/unit_tests/core/test_entity_helpers.py b/tests/unit_tests/core/test_entity_helpers.py index d6cbb8c6be..e79ff850f9 100644 --- a/tests/unit_tests/core/test_entity_helpers.py +++ b/tests/unit_tests/core/test_entity_helpers.py @@ -28,6 +28,7 @@ from esphome.core.entity_helpers import ( get_base_entity_object_id, register_device_class, register_icon, + register_unit_of_measurement, setup_device_class, setup_entity, setup_unit_of_measurement, @@ -925,6 +926,22 @@ def test_register_device_class_max_length() -> None: assert register_device_class("") == 0 +def test_register_unit_of_measurement_max_length() -> None: + """Test register_unit_of_measurement rejects units exceeding 63 characters.""" + # 63 chars should succeed + max_uom = "a" * 63 + idx = register_unit_of_measurement(max_uom) + assert idx > 0 + + # 64 chars should fail + too_long = "a" * 64 + with pytest.raises(ValueError, match="Unit of measurement string too long"): + register_unit_of_measurement(too_long) + + # Empty string returns 0 + assert register_unit_of_measurement("") == 0 + + @pytest.mark.asyncio async def test_setup_entity_with_entity_category( setup_test_environment: list[str], diff --git a/tests/unit_tests/fixtures/core/config/area_singular_hash_collision.yaml b/tests/unit_tests/fixtures/core/config/area_singular_hash_collision.yaml new file mode 100644 index 0000000000..6e137f5f6e --- /dev/null +++ b/tests/unit_tests/fixtures/core/config/area_singular_hash_collision.yaml @@ -0,0 +1,10 @@ +esphome: + name: test + area: + id: test_2258 + name: "Area 1" + areas: + - id: d6ka + name: "Area 2" + +host: diff --git a/tests/unit_tests/test_cpp_helpers.py b/tests/unit_tests/test_cpp_helpers.py index 5b6eed156f..a76ea21c23 100644 --- a/tests/unit_tests/test_cpp_helpers.py +++ b/tests/unit_tests/test_cpp_helpers.py @@ -1,8 +1,10 @@ +import logging from unittest.mock import Mock import pytest from esphome import const, cpp_helpers as ch +from esphome.cpp_helpers import ComponentSourcePool, register_component_source @pytest.mark.asyncio @@ -23,7 +25,7 @@ async def test_register_component(monkeypatch): app_mock = Mock(register_component_=Mock(return_value=var)) monkeypatch.setattr(ch, "App", app_mock) - core_mock = Mock(component_ids=["foo.bar"]) + core_mock = Mock(component_ids=["foo.bar"], data={}) monkeypatch.setattr(ch, "CORE", core_mock) add_mock = Mock() @@ -59,7 +61,7 @@ async def test_register_component__with_setup_priority(monkeypatch): app_mock = Mock(register_component_=Mock(return_value=var)) monkeypatch.setattr(ch, "App", app_mock) - core_mock = Mock(component_ids=["foo.bar"]) + core_mock = Mock(component_ids=["foo.bar"], data={}) monkeypatch.setattr(ch, "CORE", core_mock) add_mock = Mock() @@ -78,3 +80,88 @@ async def test_register_component__with_setup_priority(monkeypatch): assert add_mock.call_count == 4 app_mock.register_component_.assert_called_with(var) assert core_mock.component_ids == [] + + +def test_register_component_source_empty_name(monkeypatch: pytest.MonkeyPatch) -> None: + monkeypatch.setattr(ch, "CORE", Mock(data={})) + assert register_component_source("") == 0 + + +def test_register_component_source_deduplicates( + monkeypatch: pytest.MonkeyPatch, +) -> None: + monkeypatch.setattr(ch, "CORE", Mock(data={})) + idx1 = register_component_source("wifi") + idx2 = register_component_source("api") + idx3 = register_component_source("wifi") + assert idx1 == 1 + assert idx2 == 2 + assert idx3 == 1 # deduplicated + + +def test_generate_source_table_code_empty() -> None: + from esphome.cpp_helpers import _generate_source_table_code + + assert _generate_source_table_code("TBL", "lookup", {}) == "" + + +def test_generate_source_table_code_non_empty() -> None: + from esphome.cpp_helpers import _generate_source_table_code + + code = _generate_source_table_code("TBL", "lookup", {"wifi": 1, "api": 2}) + assert "PROGMEM" in code + assert "wifi" in code + assert "api" in code + assert "lookup" in code + assert "index == 0" in code + assert "progmem_read_ptr" in code + assert "index > 2" in code + + +@pytest.mark.asyncio +async def test_generate_component_source_table_empty_pool( + monkeypatch: pytest.MonkeyPatch, +) -> None: + """Test that _generate_component_source_table does nothing with an empty pool.""" + from esphome.cpp_helpers import _generate_component_source_table + + monkeypatch.setattr(ch, "CORE", Mock(data={})) + add_global_mock = Mock() + monkeypatch.setattr(ch, "add_global", add_global_mock) + await _generate_component_source_table() + add_global_mock.assert_not_called() + + +def test_register_component_source_overflow_warns( + monkeypatch: pytest.MonkeyPatch, caplog: pytest.LogCaptureFixture +) -> None: + # Pre-fill pool to max + pool = ComponentSourcePool( + sources={f"comp_{i}": i + 1 for i in range(0xFF)}, + table_registered=True, + ) + monkeypatch.setattr( + ch, "CORE", Mock(data={ch._COMPONENT_SOURCE_DOMAIN: pool}, testing_mode=False) + ) + with caplog.at_level(logging.WARNING): + idx = register_component_source("overflow_component") + assert idx == 0 + assert "Too many unique component source names" in caplog.text + assert "overflow_component" in caplog.text + + +def test_register_component_source_overflow_suppressed_in_testing_mode( + monkeypatch: pytest.MonkeyPatch, caplog: pytest.LogCaptureFixture +) -> None: + # Pre-fill pool to max + pool = ComponentSourcePool( + sources={f"comp_{i}": i + 1 for i in range(0xFF)}, + table_registered=True, + ) + monkeypatch.setattr( + ch, "CORE", Mock(data={ch._COMPONENT_SOURCE_DOMAIN: pool}, testing_mode=True) + ) + with caplog.at_level(logging.WARNING): + idx = register_component_source("overflow_component") + assert idx == 0 + assert "Too many unique component source names" not in caplog.text